2"""Configuration class :py:class:`Config` with deep-update, schema validation 
    5The :py:class:`Config` class implements a configuration that is based on 
    6structured dictionaries.  The configuration schema is defined in a dictionary 
    7structure and the configuration data is given in a dictionary structure. 
   16from ..compat 
import tomllib
 
   18__all__ = [
'Config', 
'UNSET', 
'SchemaIssue', 
'set_global_cfg', 
'get_global_cfg']
 
   20log = logging.getLogger(__name__)
 
   22CFG: 
"Config | None" = 
None 
   23"""Global config of the botdetection.""" 
   33        raise ValueError(
"Botdetection's config is not yet initialized.")
 
 
   39    """Class of ``False`` singleton""" 
 
   59    """Exception to store and/or raise a message from a schema issue.""" 
   61    def __init__(self, level: typing.Literal[
'warn', 
'invalid'], msg: str):
 
 
   66        return f
"[cfg schema {self.level}] {self.args[0]}" 
 
 
   70    """Base class used for configuration""" 
   75    def from_toml(cls, schema_file: pathlib.Path, cfg_file: pathlib.Path, deprecated: dict[str, str]) -> 
"Config":
 
   79        log.debug(
"load schema file: %s", schema_file)
 
   80        cfg = cls(cfg_schema=
toml_load(schema_file), deprecated=deprecated)
 
   81        if not cfg_file.exists():
 
   82            log.warning(
"missing config file: %s", cfg_file)
 
   87        log.debug(
"load config file: %s", cfg_file)
 
   90        is_valid, issue_list = cfg.validate(upd_cfg)
 
   91        for msg 
in issue_list:
 
   94            raise TypeError(f
"schema of {cfg_file} is invalid!")
 
 
   98    def __init__(self, cfg_schema: dict[str, typing.Any], deprecated: dict[str, str]):
 
   99        """Constructor of class Config. 
  101        :param cfg_schema: Schema of the configuration 
  102        :param deprecated: dictionary that maps deprecated configuration names to a messages 
  104        These values are needed for validation, see :py:obj:`validate`. 
  109        self.
cfg: dict[str, typing.Any] = copy.deepcopy(cfg_schema)
 
 
  115        """Validation of dictionary ``cfg`` on :py:obj:`Config.SCHEMA`. 
  116        Validation is done by :py:obj:`validate`.""" 
 
  120    def update(self, upd_cfg: dict[str, typing.Any]):
 
  121        """Update this configuration by ``upd_cfg``.""" 
 
  126        """Returns default value of field ``name`` in ``self.cfg_schema``.""" 
 
  129    def get(self, name: str, default: typing.Any = UNSET, replace: bool = 
True) -> typing.Any:
 
  130        """Returns the value to which ``name`` points in the configuration. 
  132        If there is no such ``name`` in the config and the ``default`` is 
  133        :py:obj:`UNSET`, a :py:obj:`KeyError` is raised. 
  137        val = parent.get(name.split(
'.')[-1], UNSET)
 
  143        if replace 
and isinstance(val, str):
 
 
  147    def set(self, name: str, val: typing.Any):
 
  148        """Set the value to which ``name`` points in the configuration. 
  150        If there is no such ``name`` in the config, a :py:obj:`KeyError` is 
  154        parent[name.split(
'.')[-1]] = val
 
 
  157        parent_name = 
'.'.join(name.split(
'.')[:-1])
 
  159            parent: dict[str, typing.Any] = 
value(parent_name, self.
cfg)
 
  162        if (parent 
is UNSET) 
or (
not isinstance(parent, dict)):
 
  163            raise KeyError(parent_name)
 
 
  166    def path(self, name: str, default: typing.Any = UNSET):
 
  167        """Get a :py:class:`pathlib.Path` object from a config string.""" 
  169        val = self.
get(name, default)
 
  174        return pathlib.Path(str(val))
 
 
  176    def pyobj(self, name: str, default: typing.Any = UNSET):
 
  177        """Get python object referred by full qualiffied name (FQN) in the config 
  180        fqn = self.
get(name, default)
 
  185        (modulename, name) = str(fqn).rsplit(
'.', 1)
 
  186        m = __import__(modulename, {}, {}, [name], 0)
 
  187        return getattr(m, name)
 
 
 
  192        with open(file_name, 
"rb") 
as f:
 
  193            return tomllib.load(f)
 
  194    except tomllib.TOMLDecodeError 
as exc:
 
  195        msg = str(exc).replace(
'\t', 
'').replace(
'\n', 
' ')
 
  196        log.error(
"%s: %s", file_name, msg)
 
 
  203def value(name: str, data_dict: dict[str, typing.Any]):
 
  204    """Returns the value to which ``name`` points in the ``dat_dict``. 
  213        >>> value('foobar', data_dict) 
  215        >>> value('foo.bar', data_dict) 
  217        >>> value('foo.bar.xxx', data_dict) 
  223    for part 
in name.split(
'.'):
 
  224        if isinstance(ret_val, dict):
 
  225            ret_val = ret_val.get(part, UNSET)
 
 
  232    schema_dict: dict[str, typing.Any], data_dict: dict[str, typing.Any], deprecated: dict[str, str]
 
  233) -> tuple[bool, list[SchemaIssue]]:
 
  234    """Deep validation of dictionary in ``data_dict`` against dictionary in 
  235    ``schema_dict``.  Argument deprecated is a dictionary that maps deprecated 
  236    configuration names to a messages:: 
  239            "foo.bar" : "config 'foo.bar' is deprecated, use 'bar.foo'", 
  243    The function returns a python tuple ``(is_valid, issue_list)``: 
  246      A bool value indicating ``data_dict`` is valid or not. 
  249      A list of messages (:py:obj:`SchemaIssue`) from the validation:: 
  251          [schema warn] data_dict: deprecated 'fontlib.foo': <DEPRECATED['foo.bar']> 
  252          [schema invalid] data_dict: key unknown 'fontlib.foo' 
  253          [schema invalid] data_dict: type mismatch 'fontlib.foo': expected ..., is ... 
  255    If ``schema_dict`` or ``data_dict`` is not a dictionary type a 
  256    :py:obj:`SchemaIssue` is raised. 
  259    names: list[str] = []
 
  260    is_valid: bool = 
True 
  261    issue_list: list[SchemaIssue] = []
 
  263    if not isinstance(schema_dict, dict):
 
  264        raise SchemaIssue(
'invalid', 
"schema_dict is not a dict type")
 
  265    if not isinstance(data_dict, dict):
 
  266        raise SchemaIssue(
'invalid', f
"data_dict issue{'.'.join(names)} is not a dict type")
 
  268    is_valid, issue_list = 
_validate(names, issue_list, schema_dict, data_dict, deprecated)
 
  269    return is_valid, issue_list
 
 
  274    issue_list: list[SchemaIssue],
 
  275    schema_dict: dict[str, typing.Any],
 
  276    data_dict: dict[str, typing.Any],
 
  277    deprecated: dict[str, str],
 
  278) -> tuple[bool, list[SchemaIssue]]:
 
  282    data_value: dict[str, typing.Any]
 
  283    for key, data_value 
in data_dict.items():
 
  286        name = 
'.'.join(names)
 
  288        deprecated_msg = deprecated.get(name)
 
  291            issue_list.append(
SchemaIssue(
'warn', f
"data_dict '{name}': deprecated - {deprecated_msg}"))
 
  293        schema_value = 
value(name, schema_dict)
 
  295        if schema_value 
is UNSET:
 
  296            if not deprecated_msg:
 
  297                issue_list.append(
SchemaIssue(
'invalid', f
"data_dict '{name}': key unknown in schema_dict"))
 
  300        elif type(schema_value) != type(data_value):  
 
  304                    (f
"data_dict: type mismatch '{name}':" f
" expected {type(schema_value)}, is: {type(data_value)}"),
 
  309        elif isinstance(data_value, dict):
 
  310            _valid, _ = 
_validate(names, issue_list, schema_dict, data_value, deprecated)
 
  311            is_valid = is_valid 
and _valid
 
  314    return is_valid, issue_list
 
 
  317def dict_deepupdate(base_dict: dict[str, typing.Any], upd_dict: dict[str, typing.Any], names: list[str] | 
None = 
None):
 
  318    """Deep-update of dictionary in ``base_dict`` by dictionary in ``upd_dict``. 
  320    For each ``upd_key`` & ``upd_val`` pair in ``upd_dict``: 
  322    0. If types of ``base_dict[upd_key]`` and ``upd_val`` do not match raise a 
  325    1. If ``base_dict[upd_key]`` is a dict: recursively deep-update it by ``upd_val``. 
  327    2. If ``base_dict[upd_key]`` not exist: set ``base_dict[upd_key]`` from a 
  328       (deep-) copy of ``upd_val``. 
  330    3. If ``upd_val`` is a list, extend list in ``base_dict[upd_key]`` by the 
  333    4. If ``upd_val`` is a set, update set in ``base_dict[upd_key]`` by set in 
  337    if not isinstance(base_dict, dict):
 
  338        raise TypeError(
"argument 'base_dict' is not a dictionary type")
 
  339    if not isinstance(upd_dict, dict):
 
  340        raise TypeError(
"argument 'upd_dict' is not a dictionary type")
 
  345    for upd_key, upd_val 
in upd_dict.items():
 
  348        if isinstance(upd_val, dict):
 
  350            if upd_key 
in base_dict:
 
  352                if not isinstance(base_dict[upd_key], dict):
 
  353                    raise TypeError(f
"type mismatch {'.'.join(names)}: is not a dict type in base_dict")
 
  365                base_dict[upd_key] = copy.deepcopy(upd_val)  
 
  367        elif isinstance(upd_val, list):
 
  369            if upd_key 
in base_dict:
 
  372                if not isinstance(base_dict[upd_key], list):
 
  373                    raise TypeError(f
"type mismatch {'.'.join(names)}: is not a list type in base_dict")
 
  374                base_dict[upd_key].extend(upd_val)
 
  379                base_dict[upd_key] = copy.deepcopy(upd_val)  
 
  381        elif isinstance(upd_val, set):
 
  383            if upd_key 
in base_dict:
 
  385                if not isinstance(base_dict[upd_key], set):
 
  386                    raise TypeError(f
"type mismatch {'.'.join(names)}: is not a set type in base_dict")
 
  387                base_dict[upd_key].update(upd_val.copy())
 
  392                base_dict[upd_key] = upd_val.copy()
 
  397            base_dict[upd_key] = copy.copy(upd_val)
 
 
pyobj(self, str name, typing.Any default=UNSET)
 
"Config" from_toml(cls, pathlib.Path schema_file, pathlib.Path cfg_file, dict[str, str] deprecated)
 
path(self, str name, typing.Any default=UNSET)
 
dict[str, typing.Any] _get_parent_dict(self, str name)
 
update(self, dict[str, typing.Any] upd_cfg)
 
__init__(self, dict[str, typing.Any] cfg_schema, dict[str, str] deprecated)
 
set(self, str name, typing.Any val)
 
typing.Any __getitem__(self, str key)
 
typing.Any get(self, str name, typing.Any default=UNSET, bool replace=True)
 
dict[str, str] deprecated
 
validate(self, dict[str, typing.Any] cfg)
 
dict[str, typing.Any] cfg_schema
 
dict[str, typing.Any] cfg
 
__init__(self, typing.Literal['warn', 'invalid'] level, str msg)
 
set_global_cfg("Config" cfg)
 
tuple[bool, list[SchemaIssue]] validate(dict[str, typing.Any] schema_dict, dict[str, typing.Any] data_dict, dict[str, str] deprecated)
 
toml_load(str|pathlib.Path file_name)
 
tuple[bool, list[SchemaIssue]] _validate(list[str] names, list[SchemaIssue] issue_list, dict[str, typing.Any] schema_dict, dict[str, typing.Any] data_dict, dict[str, str] deprecated)
 
dict_deepupdate(dict[str, typing.Any] base_dict, dict[str, typing.Any] upd_dict, list[str]|None names=None)
 
"Config" get_global_cfg()
 
value(str name, dict[str, typing.Any] data_dict)