|
|
@@ -0,0 +1,292 @@
|
|
|
+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 <str, str | int | float | bool>
|
|
|
+ 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()
|
|
|
+
|