|  | @@ -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()
 | 
	
		
			
				|  |  | +
 |