diff --git a/oxrlib/config.py b/oxrlib/config.py new file mode 100644 index 0000000000000000000000000000000000000000..d748861534c8e116a8ef1dc03ef6c4e65650a928 --- /dev/null +++ b/oxrlib/config.py @@ -0,0 +1,104 @@ +import argparse +import configparser +import datetime +import os.path +import pathlib + +from . import loaders + +HOME_PATH = pathlib.Path(os.path.expanduser('~')) +CONFFILE_SEED = """ +[Historical] +base=USD +""" + +def currency_code(s): + if not (len(s) == 3) and s.isalpha(): + raise ValueError("bad currency code: {!r}".format(s)) + return s.upper() + +def date_from(fmt_s): + def date_from_fmt(s): + return datetime.datetime.strptime(s, fmt_s).date + return date_from_fmt + +class Configuration: + DEFAULT_CONFIG_PATH = pathlib.Path(HOME_PATH, '.config', 'oxrlib.ini') + + def __init__(self, arglist): + argparser = self._build_argparser() + self.error = argparser.error + self.args = argparser.parse_args(arglist) + + if self.args.config_file is None: + self.args.config_file = [self.DEFAULT_CONFIG_PATH] + self.conffile = self._build_conffile() + conffile_paths = [path.as_posix() for path in self.args.config_file] + read_files = self.conffile.read(conffile_paths) + for expected_path, read_path in zip(conffile_paths, read_files): + if read_path != expected_path: + self.error("failed to read configuration file {!r}".format(expected_path)) + + try: + post_hook = getattr(self, '_post_hook_' + self.args.command) + except AttributeError: + pass + else: + post_hook() + + def _build_argparser(self): + prog_parser = argparse.ArgumentParser() + prog_parser.add_argument( + '--config-file', '-c', + action='append', type=pathlib.Path, + help="Path of a configuration file to read", + ) + subparsers = prog_parser.add_subparsers() + + hist_parser = subparsers.add_parser('historical', aliases=['hist']) + hist_parser.set_defaults(command='historical') + hist_parser.add_argument( + '--base', + help="Base currency (default USD)", + ) + hist_parser.add_argument( + 'date', + type=date_from('%Y-%m-%d'), metavar='YYYY-MM-DD', + ) + + return prog_parser + + def _build_conffile(self): + conffile = configparser.ConfigParser() + conffile.read_string(CONFFILE_SEED) + return conffile + + def _post_hook_historical(self): + if self.args.base is None: + self.args.base = self.conffile.get('Historical', 'base') + + def _build_cache_loader(self): + kwargs = dict(self.conffile.items('Cache')) + try: + kwargs['dir_path'] = kwargs.pop('directory') + except KeyError: + pass + return loaders.FileCache(**kwargs) + + def _build_oxrapi_loader(self): + kwargs = dict(self.conffile.items('OXR')) + return loaders.OXRAPIRequest(**kwargs) + + def get_loaders(self): + loader_chain = loaders.LoaderChain() + for build_func in [ + self._build_cache_loader, + self._build_oxrapi_loader, + ]: + try: + loader = build_func() + except (TypeError, ValueError, configparser.NoSectionError): + pass + else: + loader_chain.add_loader(loader) + return loader_chain diff --git a/tests/config_ini/full.ini b/tests/config_ini/full.ini new file mode 100644 index 0000000000000000000000000000000000000000..7ffc578ab3cf2d259f3eb0909aca806e9caff2f1 --- /dev/null +++ b/tests/config_ini/full.ini @@ -0,0 +1,10 @@ +[Cache] +directory = /tmp +historical = {date}_{base}_rates.json + +[OXR] +app_id = 1234567890abcdef1234567890abcdef +api_root = http://[100::]/oxrlibtest/ + +[Historical] +base = INI diff --git a/tests/config_ini/incomplete.ini b/tests/config_ini/incomplete.ini new file mode 100644 index 0000000000000000000000000000000000000000..7fc7852b490fdb6ab6c30c169b17f396d7a5e298 --- /dev/null +++ b/tests/config_ini/incomplete.ini @@ -0,0 +1,9 @@ +[Cache] +# No directory. +# FIXME: Write pattern-validating code, and then make this the section: +# directory = /tmp +# historical = rates.json + +[OXR] +# No app_id. +api_root = http://[100::]/oxrlibtest/ diff --git a/tests/test_Configuration.py b/tests/test_Configuration.py new file mode 100644 index 0000000000000000000000000000000000000000..00d5ddf7f76ca08f6a46e8719f77b03f56c70d57 --- /dev/null +++ b/tests/test_Configuration.py @@ -0,0 +1,44 @@ +import os + +import pytest + +from . import any_date, relpath + +import oxrlib.config +import oxrlib.loaders + +INI_DIR_PATH = relpath('config_ini') + +def config_from(ini_filename, arglist=None): + if arglist is None: + arglist = ['historical', any_date().isoformat()] + ini_path = INI_DIR_PATH / ini_filename + return oxrlib.config.Configuration(['--config-file', ini_path.as_posix()] + arglist) + +def test_full_config(): + config = config_from('full.ini') + loaders = config.get_loaders().loaders + assert type(loaders[0]) is oxrlib.loaders.FileCache + assert type(loaders[1]) is oxrlib.loaders.OXRAPIRequest + assert len(loaders) == 2 + +def test_incomplete_config(): + config = config_from('incomplete.ini') + assert not config.get_loaders().loaders + +def test_empty_config(): + config = config_from(os.devnull) + assert not config.get_loaders().loaders + +@pytest.mark.parametrize('ini_filename,expected_currency,use_switch', [ + (os.devnull, 'USD', False), + ('full.ini', 'INI', False), + ('full.ini', 'EUR', True), +]) +def test_historical_default_base(ini_filename, expected_currency, use_switch, any_date): + arglist = ['historical'] + if use_switch: + arglist.extend(['--base', expected_currency]) + arglist.append(any_date.isoformat()) + config = config_from(ini_filename, arglist) + assert config.args.base == expected_currency