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 LoaderError(Exception): pass
|
||||||
|
class LoaderConfigurationError(LoaderError): pass
|
||||||
class LoaderNoDataError(LoaderError): pass
|
class LoaderNoDataError(LoaderError): pass
|
||||||
class LoaderBadRequestError(LoaderError): pass
|
class LoaderBadRequestError(LoaderError): pass
|
||||||
class LoaderSourceError(LoaderError): pass
|
class LoaderSourceError(LoaderError): pass
|
||||||
|
class CacheLoaderConfigurationError(CacheConfigurationError, LoaderConfigurationError): pass
|
||||||
class NoLoadersError(Exception): pass
|
class NoLoadersError(Exception): pass
|
||||||
|
|
|
@ -4,19 +4,20 @@ import io
|
||||||
import urllib.request
|
import urllib.request
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
|
|
||||||
from . import errors
|
from . import cache, errors
|
||||||
|
|
||||||
class FileCache:
|
class ReadCacheFile(cache.CacheFileBase):
|
||||||
def __init__(self, dir_path, filename_pattern):
|
ERRORS_MAP = [
|
||||||
self.dir_path = dir_path
|
(FileNotFoundError, errors.LoaderNoDataError),
|
||||||
self.pattern = filename_pattern
|
(OSError, errors.LoaderSourceError),
|
||||||
|
]
|
||||||
|
|
||||||
def historical(self, date, base):
|
|
||||||
path = self.dir_path / self.pattern.format(date=date.isoformat(), base=base)
|
class FileCache(cache.CacheBase):
|
||||||
try:
|
ConfigurationError = errors.CacheLoaderConfigurationError
|
||||||
return path.open()
|
|
||||||
except FileNotFoundError as error:
|
def open(self, path):
|
||||||
raise errors.LoaderNoDataError(path) from error
|
return ReadCacheFile(path)
|
||||||
|
|
||||||
|
|
||||||
class OXRAPIRequest:
|
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
|
import pytest
|
||||||
|
|
||||||
from . import relpath
|
from . import any_date, relpath
|
||||||
import oxrlib.errors
|
import oxrlib.errors
|
||||||
import oxrlib.loaders
|
import oxrlib.loaders
|
||||||
|
|
||||||
CACHE_PATH = relpath('filecache')
|
CACHE_PATH = relpath('filecache')
|
||||||
CACHE_PATTERN = '{date}_{base}_cache.json'
|
HISTORICAL_PATTERN = '{date}_{base}_cache.json'
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def dummycache():
|
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', [
|
@pytest.mark.parametrize('date,base', [
|
||||||
(datetime.date(1999, 2, 1), 'USD'),
|
(datetime.date(1999, 2, 1), 'USD'),
|
||||||
(datetime.date(1999, 3, 1), 'EUR'),
|
(datetime.date(1999, 3, 1), 'EUR'),
|
||||||
])
|
])
|
||||||
def test_cache_success(dummycache, date, base):
|
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:
|
with dummycache.historical(date, base) as cache_file:
|
||||||
assert pathlib.Path(cache_file.name) == expect_name
|
assert pathlib.Path(cache_file.name) == expect_name
|
||||||
|
|
||||||
@pytest.mark.parametrize('date,base', [
|
@pytest.mark.parametrize('date,base,exc_type', [
|
||||||
(datetime.date(1999, 2, 1), 'EUR'),
|
(datetime.date(1999, 2, 1), 'EUR', oxrlib.errors.LoaderNoDataError),
|
||||||
(datetime.date(1999, 3, 1), 'USD'),
|
(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:
|
try:
|
||||||
cache_file = dummycache.historical(date, base)
|
with dummycache.historical(date, base):
|
||||||
except oxrlib.errors.LoaderNoDataError:
|
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
|
pass
|
||||||
else:
|
else:
|
||||||
cache_file.close()
|
assert False, "CacheLoaderConfigurationError not raised"
|
||||||
assert False, "cache file found when unexpected"
|
|
||||||
|
|
Loading…
Reference in a new issue