diff --git a/.pylintrc b/.pylintrc index 4771a81..f3ad785 100644 --- a/.pylintrc +++ b/.pylintrc @@ -138,7 +138,8 @@ disable=print-statement, xreadlines-attribute, deprecated-sys-function, exception-escape, - comprehension-escape + comprehension-escape, + logging-fstring-interpolation # Enable the message, report, category or checker with the given id(s). You can # either give multiple identifier separated by comma (,) or put this option diff --git a/bff/__init__.py b/bff/__init__.py index bac994e..829bf0f 100644 --- a/bff/__init__.py +++ b/bff/__init__.py @@ -1,10 +1,16 @@ """All of bff' functions.""" +import logging + +from ._version import get_versions + from .fancy import ( concat_with_categories, get_peaks, idict, mem_usage_pd, parse_date, plot_history, plot_predictions, plot_series, plot_true_vs_pred, read_sql_by_chunks, sliding_window, value_2_list ) +from .config import FancyConfig + # Public object of the module. __all__ = [ 'concat_with_categories', @@ -19,8 +25,12 @@ 'read_sql_by_chunks', 'sliding_window', 'value_2_list', + 'FancyConfig', ] -from ._version import get_versions +# Logging configuration. +FORMAT = '%(asctime)-15s %(message)s' +logging.basicConfig(format=FORMAT) + __version__ = get_versions()['version'] del get_versions diff --git a/bff/config.py b/bff/config.py new file mode 100644 index 0000000..4a9fd54 --- /dev/null +++ b/bff/config.py @@ -0,0 +1,91 @@ +""" +FancyConfig, configuration loader. + +Tool to load the configuration from a configuration file (`config.yml`). +""" +from collections.abc import Mapping +import logging +from pathlib import Path +import pprint +import yaml + +LOGGER = logging.getLogger(__name__) + + +class FancyConfig(Mapping): + """ + Class to load the configuration file. + + This class behaves like a dictionary that loads a + configuration file in yaml format. + + If the configuration file does not exist, creates it from template. + + Examples + -------- + >>> config = FancyConfig() + >>> print(config) + { 'database': { 'host': '127.0.0.1', + 'name': 'porgs', + 'port': 3306, + 'pwd': 'bacca', + 'user': 'Chew'}, + 'env': 'prod', + 'imports': {'star_wars': ['ewok', 'bantha']}} + """ + + def __init__(self, path_config_to_load: Path = Path.home().joinpath('.config/fancyconfig.yml'), + default_config_path: Path = (Path(__file__).resolve() + .parent.joinpath('config.yml'))): + """ + Initialization of configuration. + + If the folder to store the configuration does not exist, create it. + If configuration file does not exist, copy it from default one. + + Parameters + ---------- + path_config_to_load : Path, default '~/.config/' + Directory to store the configuration file and load the configuration from. + default_config_path: Path, default 'config.yml' current directory. + Name of the configuration file. + """ + # Create config file if does not exist. + if not path_config_to_load.exists(): + LOGGER.info((f'Configuration file does not exist, ' + f'creating it from {default_config_path}')) + # Creating folder of configuration (parent of file). + path_config_to_load.parent.mkdir(parents=True, exist_ok=True) + # Copy the configuration file. + path_config_to_load.write_bytes(default_config_path.read_bytes()) + + with path_config_to_load.open(mode='r', encoding='utf-8') as yaml_config_file: + self._config = yaml.load(yaml_config_file, Loader=yaml.FullLoader) + + def __getitem__(self, item): + """Getter of the class.""" + try: + return self._config[item] + except KeyError: + LOGGER.error(f'Configuration for {item} does not exist.') + + def __iter__(self): + """Iterator of the class.""" + return iter(self._config) + + def __len__(self): + """Lenght of the config.""" + return len(self._config) + + def __str__(self): + """ + __str__ method. + + Pretty representation of the config. + """ + pretty = pprint.PrettyPrinter(indent=2) + return pretty.pformat(self._config) + + def __repr__(self): + """Representation of the config.""" + return f'{super().__repr__}\n{str(self._config)}' diff --git a/bff/config.yml b/bff/config.yml new file mode 100644 index 0000000..6e586bc --- /dev/null +++ b/bff/config.yml @@ -0,0 +1,16 @@ +# Configuration file example. + +# Database information +database: + user: Chew + pwd: bacca + host: 127.0.0.1 + port: 3306 + name: porgs + +imports: + star_wars: + - ewok + - bantha + +env: prod diff --git a/doc/source/config.rst b/doc/source/config.rst new file mode 100644 index 0000000..d50bf3b --- /dev/null +++ b/doc/source/config.rst @@ -0,0 +1,5 @@ +FancyConfig +=========== + +.. automodule:: bff.FancyConfig + :members: __init__ diff --git a/doc/source/index.rst b/doc/source/index.rst index 97caa98..9c30571 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -29,3 +29,5 @@ Contents fancy + config + diff --git a/setup.cfg b/setup.cfg index 3cf026c..6072735 100644 --- a/setup.cfg +++ b/setup.cfg @@ -18,6 +18,7 @@ exclude = versioneer.py, bff/_version.py, setup.py + doc [tool:pytest] testpaths = bff diff --git a/tests/test_config.py b/tests/test_config.py new file mode 100644 index 0000000..b6c6aad --- /dev/null +++ b/tests/test_config.py @@ -0,0 +1,79 @@ +# -*- coding: utf-8 -*- +"""Test of config module + +This module test the configuration loader +""" +import filecmp +from pathlib import Path +import unittest + +from bff.config import FancyConfig + + +class TestiFancyConfig(unittest.TestCase): + """ + Unittest of config module. + """ + + def test_create_config(self): + """ + Test of loading the config when it does not exists. + """ + # Default configuration + config_default = FancyConfig() + + default_path_ok = Path(__file__).resolve().parent.parent.joinpath('bff/config.yml') + default_path_ko = Path.home().resolve().joinpath('config.yml') + + dest = Path(__file__).resolve().parent.joinpath('config.yml') + config_a = FancyConfig(dest, default_path_ok) + + # Check if the file is correctly copied. + self.assertTrue(filecmp.cmp(dest, default_path_ok)) + + # Check if the file is not overridden. + path_conf_b = Path(__file__).resolve().parent.joinpath('conf_b.yml') + with path_conf_b.open(mode='w', encoding='utf-8') as f: + f.write('testfile: True') + config_b = FancyConfig(dest, path_conf_b) + + self.assertFalse(filecmp.cmp(dest, path_conf_b)) + # Check that the configurations are the same. + self.assertEqual(config_a, config_b) + + # Removes the configs. + dest.unlink() + path_conf_b.unlink() + + # Create a config that is not called config. + config_c = FancyConfig(path_conf_b, default_path_ok) + self.assertEqual(config_a, config_c) + + # Remove the files. + path_conf_b.unlink() + + # If default config path is wrong, should fail. + with self.assertRaises(FileNotFoundError): + FancyConfig(dest, default_path_ko) + + def test_access_config(self): + """ + Test the access of the configuration once loaded. + + Should be accessible as a property. + """ + default_path_ok = Path(__file__).resolve().parent.parent.joinpath('bff/config.yml') + dest = Path(__file__).resolve().parent.joinpath('config.yml') + config = FancyConfig(dest, default_path_ok) + + self.assertEqual(config['env'], 'prod') + self.assertEqual(config['database']['user'], 'Chew') + self.assertEqual(config['imports']['star_wars'], ['ewok', 'bantha']) + + # Check the error message using a mock. + with unittest.mock.patch('logging.Logger.error') as mock_logging: + config['error'] + mock_logging.assert_called_with('Configuration for error does not exist.') + + # Remove the file. + dest.unlink()