diff --git a/oxrlib/cache.py b/oxrlib/cache.py new file mode 100644 index 0000000000000000000000000000000000000000..d19703201152cf1b364f1a15b350fc441438a7c1 --- /dev/null +++ b/oxrlib/cache.py @@ -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} diff --git a/oxrlib/errors.py b/oxrlib/errors.py index 242d553f7a65a4dd3f87133c609607f2c09aef7f..37c59c8081a4050b0af1d0f0f99a1fa98d5254df 100644 --- a/oxrlib/errors.py +++ b/oxrlib/errors.py @@ -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 diff --git a/oxrlib/loaders.py b/oxrlib/loaders.py index 5c802b185725b325e1cc71bc1ed9843bf4abd35c..750ec3f58dde46f48b0e216b5348f8e112da27c2 100644 --- a/oxrlib/loaders.py +++ b/oxrlib/loaders.py @@ -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: diff --git a/tests/filecache/1200-12-31_USD_cache.json/.empty b/tests/filecache/1200-12-31_USD_cache.json/.empty new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/tests/test_FileCache.py b/tests/test_FileCache.py index d4e3ed651c7bbc16f66530e82f3ca2a04f459a65..4d5a17a98294e0467f2f8709aaff38417d21db07 100644 --- a/tests/test_FileCache.py +++ b/tests/test_FileCache.py @@ -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: + 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_file = dummycache.historical(date, base) - except oxrlib.errors.LoaderNoDataError: + 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"