| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292 | import jsonimport pathlibimport functoolsfrom .config_exceptions import key_not_implementedfrom .config_exceptions import bad_type_loadedfrom .config_exceptions import invalid_config_processorfrom .config_exceptions import bad_key_typefrom .config_exceptions import bad_value_typefrom .config_exceptions import config_file_not_existsfrom .config_exceptions import config_file_not_readableclass 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()
 |