cache: Refactor out CacheBase from FileCache.
Now we can implement CacheWriter from the same base.
This commit is contained in:
parent
4cbd890992
commit
eba27c16ae
5 changed files with 102 additions and 23 deletions
64
oxrlib/cache.py
Normal file
64
oxrlib/cache.py
Normal file
|
@ -0,0 +1,64 @@
|
|||
import functools
|
||||
|
||||
from . import errors
|
||||
|
||||
class CacheFileBase:
|
||||
def __init__(self, path, *args, **kwargs):
|
||||
self.path = path
|
||||
self.open = functools.partial(path.open, *args, **kwargs)
|
||||
|
||||
def _translate_error(self, error, when):
|
||||
for orig_type, mapped_type in self.ERRORS_MAP:
|
||||
if isinstance(error, orig_type):
|
||||
raise mapped_type(self.path) from error
|
||||
raise error
|
||||
|
||||
def __enter__(self):
|
||||
try:
|
||||
self.open_file = self.open()
|
||||
except OSError as error:
|
||||
self._translate_error(error, 'enter')
|
||||
else:
|
||||
return self.open_file
|
||||
|
||||
def __exit__(self, exc_type, exc_value, exc_tb):
|
||||
try:
|
||||
self.open_file.close()
|
||||
except OSError as error:
|
||||
self._translate_error(error, 'exit')
|
||||
|
||||
|
||||
class CacheBase:
|
||||
ConfigurationError = errors.CacheConfigurationError
|
||||
|
||||
def __init__(self, dir_path, **kwargs):
|
||||
self.dir_path = dir_path
|
||||
self.fn_patterns = {}
|
||||
self.setup(**kwargs)
|
||||
|
||||
def setup(self, **kwargs):
|
||||
for method_name, pattern in kwargs.items():
|
||||
try:
|
||||
is_api_method = getattr(self, method_name).is_api_method
|
||||
except AttributeError:
|
||||
is_api_method = False
|
||||
if not is_api_method:
|
||||
raise errors.CacheConfigurationError(method_name)
|
||||
self.fn_patterns[method_name] = pattern
|
||||
|
||||
def _wrap_api_method(orig_func):
|
||||
@functools.wraps(orig_func)
|
||||
def api_method_wrapper(self, *args, **kwargs):
|
||||
try:
|
||||
fn_pattern = self.fn_patterns[orig_func.__name__]
|
||||
except KeyError:
|
||||
raise self.ConfigurationError(orig_func.__name__) from None
|
||||
pattern_kwargs = orig_func(self, *args, **kwargs)
|
||||
path = self.dir_path / fn_pattern.format(**pattern_kwargs)
|
||||
return self.open(path)
|
||||
api_method_wrapper.is_api_method = True
|
||||
return api_method_wrapper
|
||||
|
||||
@_wrap_api_method
|
||||
def historical(self, date, base):
|
||||
return {'date': date.isoformat(), 'base': base}
|
|
@ -1,5 +1,9 @@
|
|||
class CacheError(Exception): pass
|
||||
class CacheConfigurationError(CacheError): pass
|
||||
class LoaderError(Exception): pass
|
||||
class LoaderConfigurationError(LoaderError): pass
|
||||
class LoaderNoDataError(LoaderError): pass
|
||||
class LoaderBadRequestError(LoaderError): pass
|
||||
class LoaderSourceError(LoaderError): pass
|
||||
class CacheLoaderConfigurationError(CacheConfigurationError, LoaderConfigurationError): pass
|
||||
class NoLoadersError(Exception): pass
|
||||
|
|
|
@ -4,19 +4,20 @@ import io
|
|||
import urllib.request
|
||||
import urllib.parse
|
||||
|
||||
from . import errors
|
||||
from . import cache, errors
|
||||
|
||||
class FileCache:
|
||||
def __init__(self, dir_path, filename_pattern):
|
||||
self.dir_path = dir_path
|
||||
self.pattern = filename_pattern
|
||||
class ReadCacheFile(cache.CacheFileBase):
|
||||
ERRORS_MAP = [
|
||||
(FileNotFoundError, errors.LoaderNoDataError),
|
||||
(OSError, errors.LoaderSourceError),
|
||||
]
|
||||
|
||||
def historical(self, date, base):
|
||||
path = self.dir_path / self.pattern.format(date=date.isoformat(), base=base)
|
||||
try:
|
||||
return path.open()
|
||||
except FileNotFoundError as error:
|
||||
raise errors.LoaderNoDataError(path) from error
|
||||
|
||||
class FileCache(cache.CacheBase):
|
||||
ConfigurationError = errors.CacheLoaderConfigurationError
|
||||
|
||||
def open(self, path):
|
||||
return ReadCacheFile(path)
|
||||
|
||||
|
||||
class OXRAPIRequest:
|
||||
|
|
0
tests/filecache/1200-12-31_USD_cache.json/.empty
Normal file
0
tests/filecache/1200-12-31_USD_cache.json/.empty
Normal file
|
@ -3,35 +3,45 @@ import pathlib
|
|||
|
||||
import pytest
|
||||
|
||||
from . import relpath
|
||||
from . import any_date, relpath
|
||||
import oxrlib.errors
|
||||
import oxrlib.loaders
|
||||
|
||||
CACHE_PATH = relpath('filecache')
|
||||
CACHE_PATTERN = '{date}_{base}_cache.json'
|
||||
HISTORICAL_PATTERN = '{date}_{base}_cache.json'
|
||||
|
||||
@pytest.fixture
|
||||
def dummycache():
|
||||
return oxrlib.loaders.FileCache(CACHE_PATH, CACHE_PATTERN)
|
||||
cache = oxrlib.loaders.FileCache(CACHE_PATH)
|
||||
cache.setup(historical=HISTORICAL_PATTERN)
|
||||
return cache
|
||||
|
||||
@pytest.mark.parametrize('date,base', [
|
||||
(datetime.date(1999, 2, 1), 'USD'),
|
||||
(datetime.date(1999, 3, 1), 'EUR'),
|
||||
])
|
||||
def test_cache_success(dummycache, date, base):
|
||||
expect_name = CACHE_PATH / CACHE_PATTERN.format(date=date.isoformat(), base=base)
|
||||
expect_name = CACHE_PATH / HISTORICAL_PATTERN.format(date=date.isoformat(), base=base)
|
||||
with dummycache.historical(date, base) as cache_file:
|
||||
assert pathlib.Path(cache_file.name) == expect_name
|
||||
|
||||
@pytest.mark.parametrize('date,base', [
|
||||
(datetime.date(1999, 2, 1), 'EUR'),
|
||||
(datetime.date(1999, 3, 1), 'USD'),
|
||||
@pytest.mark.parametrize('date,base,exc_type', [
|
||||
(datetime.date(1999, 2, 1), 'EUR', oxrlib.errors.LoaderNoDataError),
|
||||
(datetime.date(1999, 3, 1), 'USD', oxrlib.errors.LoaderNoDataError),
|
||||
(datetime.date(1200, 12, 31), 'USD', oxrlib.errors.LoaderSourceError),
|
||||
])
|
||||
def test_cache_not_found(dummycache, date, base):
|
||||
def test_cache_read_error(dummycache, date, base, exc_type):
|
||||
try:
|
||||
cache_file = dummycache.historical(date, base)
|
||||
except oxrlib.errors.LoaderNoDataError:
|
||||
with dummycache.historical(date, base):
|
||||
assert False, "{e.__name__} not raised".format(e=exc_type)
|
||||
except exc_type:
|
||||
pass
|
||||
|
||||
def test_cache_unconfigured(any_date):
|
||||
cache = oxrlib.loaders.FileCache(CACHE_PATH)
|
||||
try:
|
||||
cache.historical(any_date, 'USD')
|
||||
except oxrlib.errors.CacheLoaderConfigurationError:
|
||||
pass
|
||||
else:
|
||||
cache_file.close()
|
||||
assert False, "cache file found when unexpected"
|
||||
assert False, "CacheLoaderConfigurationError not raised"
|
||||
|
|
Loading…
Reference in a new issue