summaryrefslogtreecommitdiff
path: root/src/home/config/config.py
diff options
context:
space:
mode:
authorEvgeny Zinoviev <me@ch1p.io>2023-05-31 09:22:00 +0300
committerEvgeny Zinoviev <me@ch1p.io>2023-06-10 02:07:23 +0300
commitf29e139cbb7e4a4d539cba6e894ef4a6acd312d6 (patch)
tree6246f126325c5c36fb573134a05f2771cd747966 /src/home/config/config.py
parent3e3753d726f8a02d98368f20f77dd9fa739e3d80 (diff)
WIP: big refactoring
Diffstat (limited to 'src/home/config/config.py')
-rw-r--r--src/home/config/config.py329
1 files changed, 245 insertions, 84 deletions
diff --git a/src/home/config/config.py b/src/home/config/config.py
index 4681685..aef9ee7 100644
--- a/src/home/config/config.py
+++ b/src/home/config/config.py
@@ -1,58 +1,256 @@
-import toml
import yaml
import logging
import os
+import pprint
-from os.path import join, isdir, isfile
-from typing import Optional, Any, MutableMapping
+from abc import ABC
+from cerberus import Validator, DocumentError
+from typing import Optional, Any, MutableMapping, Union
from argparse import ArgumentParser
-from ..util import parse_addr
+from enum import Enum, auto
+from os.path import join, isdir, isfile
+from ..util import Addr
+
+
+CONFIG_DIRECTORIES = (
+ join(os.environ['HOME'], '.config', 'homekit'),
+ '/etc/homekit'
+)
+
+class RootSchemaType(Enum):
+ DEFAULT = auto()
+ DICT = auto()
+ LIST = auto()
+
+
+class BaseConfigUnit(ABC):
+ _data: MutableMapping[str, Any]
+ _logger: logging.Logger
+ def __init__(self):
+ self._data = {}
+ self._logger = logging.getLogger(self.__class__.__name__)
+
+ def __getitem__(self, key):
+ return self._data[key]
+
+ def __setitem__(self, key, value):
+ raise NotImplementedError('overwriting config values is prohibited')
-def _get_config_path(name: str) -> str:
- formats = ['toml', 'yaml']
+ def __contains__(self, key):
+ return key in self._data
- dirname = join(os.environ['HOME'], '.config', name)
+ def load_from(self, path: str):
+ with open(path, 'r') as fd:
+ self._data = yaml.safe_load(fd)
- if isdir(dirname):
- for fmt in formats:
- filename = join(dirname, f'config.{fmt}')
- if isfile(filename):
- return filename
+ def get(self,
+ key: Optional[str] = None,
+ default=None):
+ if key is None:
+ return self._data
- raise IOError(f'config not found in {dirname}')
+ cur = self._data
+ pts = key.split('.')
+ for i in range(len(pts)):
+ k = pts[i]
+ if i < len(pts)-1:
+ if k not in cur:
+ raise KeyError(f'key {k} not found')
+ else:
+ return cur[k] if k in cur else default
+ cur = self._data[k]
- else:
- filenames = [join(os.environ['HOME'], '.config', f'{name}.{format}') for format in formats]
- for file in filenames:
- if isfile(file):
- return file
+ raise KeyError(f'option {key} not found')
- raise IOError(f'config not found')
+class ConfigUnit(BaseConfigUnit):
+ NAME = 'dumb'
+
+ def __init__(self, name=None, load=True):
+ super().__init__()
+
+ self._data = {}
+ self._logger = logging.getLogger(self.__class__.__name__)
+
+ if self.NAME != 'dumb' and load:
+ self.load_from(self.get_config_path())
+ self.validate()
+
+ elif name is not None:
+ self.NAME = name
+
+ @classmethod
+ def get_config_path(cls, name=None) -> str:
+ if name is None:
+ name = cls.NAME
+ if name is None:
+ raise ValueError('get_config_path: name is none')
+
+ for dirname in CONFIG_DIRECTORIES:
+ if isdir(dirname):
+ filename = join(dirname, f'{name}.yaml')
+ if isfile(filename):
+ return filename
+
+ raise IOError(f'\'{name}.yaml\' not found')
+
+ @staticmethod
+ def schema() -> Optional[dict]:
+ return None
+
+ def validate(self):
+ schema = self.schema()
+ if not schema:
+ self._logger.warning('validate: no schema')
+ return
+
+ if isinstance(self, AppConfigUnit):
+ schema['logging'] = {
+ 'type': 'dict',
+ 'schema': {
+ 'logging': {'type': 'bool'}
+ }
+ }
+
+ rst = RootSchemaType.DEFAULT
+ try:
+ if schema['type'] == 'dict':
+ rst = RootSchemaType.DICT
+ elif schema['type'] == 'list':
+ rst = RootSchemaType.LIST
+ elif schema['roottype'] == 'dict':
+ del schema['roottype']
+ rst = RootSchemaType.DICT
+ except KeyError:
+ pass
+
+ if rst == RootSchemaType.DICT:
+ v = Validator({'document': {
+ 'type': 'dict',
+ 'keysrules': {'type': 'string'},
+ 'valuesrules': schema
+ }})
+ result = v.validate({'document': self._data})
+ elif rst == RootSchemaType.LIST:
+ v = Validator({'document': schema})
+ result = v.validate({'document': self._data})
+ else:
+ v = Validator(schema)
+ result = v.validate(self._data)
+ # pprint.pprint(self._data)
+ if not result:
+ # pprint.pprint(v.errors)
+ raise DocumentError(f'{self.__class__.__name__}: failed to validate data:\n{pprint.pformat(v.errors)}')
+ try:
+ self.custom_validator(self._data)
+ except Exception as e:
+ raise DocumentError(f'{self.__class__.__name__}: {str(e)}')
+
+ @staticmethod
+ def custom_validator(data):
+ pass
-class ConfigStore:
- data: MutableMapping[str, Any]
+ def get_addr(self, key: str):
+ return Addr.fromstring(self.get(key))
+
+
+class AppConfigUnit(ConfigUnit):
+ _logging_verbose: bool
+ _logging_fmt: Optional[str]
+ _logging_file: Optional[str]
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(load=False, *args, **kwargs)
+ self._logging_verbose = False
+ self._logging_fmt = None
+ self._logging_file = None
+
+ def logging_set_fmt(self, fmt: str) -> None:
+ self._logging_fmt = fmt
+
+ def logging_get_fmt(self) -> Optional[str]:
+ try:
+ return self['logging']['default_fmt']
+ except KeyError:
+ return self._logging_fmt
+
+ def logging_set_file(self, file: str) -> None:
+ self._logging_file = file
+
+ def logging_get_file(self) -> Optional[str]:
+ try:
+ return self['logging']['file']
+ except KeyError:
+ return self._logging_file
+
+ def logging_set_verbose(self):
+ self._logging_verbose = True
+
+ def logging_is_verbose(self) -> bool:
+ try:
+ return bool(self['logging']['verbose'])
+ except KeyError:
+ return self._logging_verbose
+
+
+class TranslationUnit(BaseConfigUnit):
+ pass
+
+
+class Translation:
+ LANGUAGES = ('en', 'ru')
+ _langs: dict[str, TranslationUnit]
+
+ def __init__(self, name: str):
+ super().__init__()
+ self._langs = {}
+ for lang in self.LANGUAGES:
+ for dirname in CONFIG_DIRECTORIES:
+ if isdir(dirname):
+ filename = join(dirname, f'i18n-{lang}', f'{name}.yaml')
+ if lang in self._langs:
+ raise RuntimeError(f'{name}: translation unit for lang \'{lang}\' already loaded')
+ self._langs[lang] = TranslationUnit()
+ self._langs[lang].load_from(filename)
+ diff = set()
+ for data in self._langs.values():
+ diff ^= data.get().keys()
+ if len(diff) > 0:
+ raise RuntimeError(f'{name}: translation units have difference in keys: ' + ', '.join(diff))
+
+ def get(self, lang: str) -> TranslationUnit:
+ return self._langs[lang]
+
+
+class Config:
app_name: Optional[str]
+ app_config: AppConfigUnit
def __init__(self):
- self.data = {}
self.app_name = None
+ self.app_config = AppConfigUnit()
+
+ def load_app(self,
+ name: Optional[Union[str, AppConfigUnit, bool]] = None,
+ use_cli=True,
+ parser: ArgumentParser = None,
+ no_config=False):
+ global app_config
+
+ if issubclass(name, AppConfigUnit) or name == AppConfigUnit:
+ self.app_name = name.NAME
+ self.app_config = name()
+ app_config = self.app_config
+ else:
+ self.app_name = name if isinstance(name, str) else None
- def load(self, name: Optional[str] = None,
- use_cli=True,
- parser: ArgumentParser = None):
- self.app_name = name
-
- if (name is None) and (not use_cli):
+ if self.app_name is None and not use_cli:
raise RuntimeError('either config name must be none or use_cli must be True')
- log_default_fmt = False
- log_file = None
- log_verbose = False
- no_config = name is False
-
+ no_config = name is False or no_config
path = None
+
if use_cli:
if parser is None:
parser = ArgumentParser()
@@ -68,75 +266,38 @@ class ConfigStore:
path = args.config
if args.verbose:
- log_verbose = True
+ self.app_config.logging_set_verbose()
if args.log_file:
- log_file = args.log_file
+ self.app_config.logging_set_file(args.log_file)
if args.log_default_fmt:
- log_default_fmt = args.log_default_fmt
+ self.app_config.logging_set_fmt(args.log_default_fmt)
- if not no_config and path is None:
- path = _get_config_path(name)
+ if not isinstance(name, ConfigUnit):
+ if not no_config and path is None:
+ path = ConfigUnit.get_config_path(name=self.app_name)
- if no_config:
- self.data = {}
- else:
- if path.endswith('.toml'):
- self.data = toml.load(path)
- elif path.endswith('.yaml'):
- with open(path, 'r') as fd:
- self.data = yaml.safe_load(fd)
-
- if 'logging' in self:
- if not log_file and 'file' in self['logging']:
- log_file = self['logging']['file']
- if log_default_fmt and 'default_fmt' in self['logging']:
- log_default_fmt = self['logging']['default_fmt']
+ if not no_config:
+ self.app_config.load_from(path)
- setup_logging(log_verbose, log_file, log_default_fmt)
+ setup_logging(self.app_config.logging_is_verbose(),
+ self.app_config.logging_get_file(),
+ self.app_config.logging_get_fmt())
if use_cli:
return args
- def __getitem__(self, key):
- return self.data[key]
-
- def __setitem__(self, key, value):
- raise NotImplementedError('overwriting config values is prohibited')
-
- def __contains__(self, key):
- return key in self.data
-
- def get(self, key: str, default=None):
- cur = self.data
- pts = key.split('.')
- for i in range(len(pts)):
- k = pts[i]
- if i < len(pts)-1:
- if k not in cur:
- raise KeyError(f'key {k} not found')
- else:
- return cur[k] if k in cur else default
- cur = self.data[k]
- raise KeyError(f'option {key} not found')
-
- def get_addr(self, key: str):
- return parse_addr(self.get(key))
-
- def items(self):
- return self.data.items()
-
-config = ConfigStore()
+config = Config()
def is_development_mode() -> bool:
if 'HK_MODE' in os.environ and os.environ['HK_MODE'] == 'dev':
return True
- return ('logging' in config) and ('verbose' in config['logging']) and (config['logging']['verbose'] is True)
+ return ('logging' in config.app_config) and ('verbose' in config.app_config['logging']) and (config.app_config['logging']['verbose'] is True)
-def setup_logging(verbose=False, log_file=None, default_fmt=False):
+def setup_logging(verbose=False, log_file=None, default_fmt=None):
logging_level = logging.INFO
if is_development_mode() or verbose:
logging_level = logging.DEBUG