import json import pathlib import functools from .config_exceptions import key_not_implemented from .config_exceptions import bad_type_loaded from .config_exceptions import invalid_config_processor from .config_exceptions import bad_key_type from .config_exceptions import bad_value_type from .config_exceptions import config_file_not_exists from .config_exceptions import config_file_not_readable class config_getter: """ That is config getter, it is builded by config processor. It is read only and is usefull to getting keys from config. When an key is not definied in configuration file, then default value is returned. Keys could be accessed by "get(key: str)" method, as attribute of instance of that class or as dict keys. """ def __init__(self, content: dict) -> None: """ That create new instance of getter. It would be used only by config processor. Parameters ---------- content : dict Content of the config. """ self.__content = dict(content) @functools.cache def get(self, key: str) -> str | int | float | bool: """ That function return given key from config. Parameters ---------- key : str Name of the key to return. Returns ------- str | float | bool | int Value of the key. Raises ------ key_not_implemented When tried to load key which not exists. """ if not key in self.__content: raise key_not_implemented(key) return self.__content[key] def __getattr__(self, key: str) -> str | int | float | bool: """ That is alias of "get(key: str)" function for getting keys as attributes of that class instance. """ return self.get(key) def __getitem__(self, key: str) -> str | int | float | bool: """ That is alias of "get(key: str)" function for getting keys as keys of dict. """ return self.get(key) class config_processor: """ That class is processor for configuration. It load defaults config options, then config loader addng keys from config file. That class validating options, it key must exists in the defaults config, and also type of default config value must be same as loaded value for that key. """ def __init__(self) -> None: """ It create new config processor. Content is set to default. Adding new keys replace defaults values by new ones from config loader. """ self.__content = self.get_default_config() self.__validate_defaults() def __validate_defaults(self) -> None: """ That function validate dictionary of defaults config. It could find typical issues with configuration. Raises ------ bad_key_type When key in the configuration dict is not string. bad_value_type When value for key in the configuration is in invalid type. Valid types are str, int, float and bool. """ for key, value in self.get_default_config().items(): if type(key) is not str: raise bad_key_type(key) if type(value) is str or type(value) is int: continue if type(value) is float or type(value) is bool: continue raise bad_value_type(key, value) def add(self, key: str, value: str | int | float | bool) -> None: """ It load new key to config. Key must exists in the default config. It replace value from default configuration by new value, loaded from configuration file. Parameters ---------- key : str Name of the key from configuration. value : str | int | float | bool Value for that key. Raises ------ key_not_implemented When key not exists in the default configuration. bad_type_loaded When type loaded from configuration is different from type in default config for that key. """ if not key in self.get_default_config(): raise key_not_implemented(key) default = self.get_default_config()[key] if type(default) is not type(value): raise bad_type_loaded(key, type(default), type(value)) self.__content[key] = value def result(self) -> config_getter: """ It return new config getter. Config getter is object which make configuration read only. Returns ------- config_getter New config getter from configuration. """ return config_getter(self.__content) @functools.cache def get_default_config(self) -> dict: """ It return default configuration. It must be overwriten, it is abstract function. Returns ------- dict Default configuration to load. """ raise NotImplementedError() class config_loader: def __new__( cls, processor_class: type, location: pathlib.Path | None = None ) -> config_getter: """ This load configuration from config file. It require configuration processor which could handle options from file. Processor must be given as class, which is subclass of config_processor. That subclass overwrite defaults config. Parameters ---------- processor_class : type Subclass of config_processor with defaults config. location : pathlib.Path | None Location of config file, or none. When give none, then default location from that class is used. To overwrite default location create subclass of config_loader. Raises ------ invalid_config_processor When config processor is not valid subclass. config_file_not_exists When config file does not exists. config_file_not_readable When config file contains syntax error, or file is not readable because of filesystem error. Returns ------- config_getter Config getter created by config processor, and loaded from config file. """ if type(processor_class) is not type: raise invalid_config_processor(processor_class) if not issubclass(processor_class, config_processor): raise invalid_config_processor(processor_class) processor = processor_class() default = cls.get_default_location() if location is None: location = default if not location.exists() or not location.is_file(): raise config_file_not_exists(location) try: return cls.__load(processor, location) except Exception as error: raise config_file_not_readable(error) @classmethod def __load( cls, processor: config_processor, location: pathlib.Path ) -> config_getter: """ This function load config from configuration file. It open file, and adding all keys to the config processor. Finally it create config_getter to make config readonly. Parameters ---------- processor : config_processor Config processor to work with. location : pathlib.Path Location of the config file. Returns ------- config_getter Result of config file. """ with location.open() as handler: for key, value in json.loads(handler.read()).items(): processor.add(key, value) return processor.result() @classmethod @functools.cache def get_default_location(cls) -> pathlib.Path: """ It is default location of config file. It is abstract function and must being overwrite by subclass to make config loader useable. Returns ------- pathlib.Path Path of the default config file. """ raise NotImplementedError()