From 3beccbf35e5bdc8d90788725dbf7bd86f798fbf0 Mon Sep 17 00:00:00 2001 From: Komisar Date: Wed, 15 Apr 2026 20:04:04 +0300 Subject: [PATCH] Add getall/getrawall for nested params, ROOT_KEY constant, tests and docs --- README.md | 53 ++++++++++--- doc/src.utils.config_manager.md | 92 +++++++++++++++------- requirements.txt | 1 + src/utils/config_manager/AGENTS.md | 19 ++++- src/utils/config_manager/config_manager.py | 78 ++++++++++++++++-- tests/test_config_manager.py | 41 ++++++++++ 6 files changed, 235 insertions(+), 49 deletions(-) create mode 100644 requirements.txt diff --git a/README.md b/README.md index 35fbb6e..05431c6 100644 --- a/README.md +++ b/README.md @@ -3,17 +3,12 @@ ## Требования - Python >3.10 -- Все консольные выводы дублируются в логах -## Структура +## Установка -- `app/` - точка входа (komAI.py) -- `src/` - исходный код -- `src/__init__.py` - централизованный API -- `config/` - YAML конфигурация -- `tests/` - юнит-тесты -- `modules/` - подключаемые модули -- `log/` - файлы логов +```bash +pip install -r requirements.txt +``` ## Быстрый старт @@ -21,8 +16,42 @@ python -m app.komAI ``` +## Структура + +- `app/` - точка входа приложения +- `src/` - исходный код +- `config/` - YAML конфигурация +- `modules/` - подключаемые модули +- `tests/` - юнит-тесты +- `log/` - файлы логов +- `doc/` - документация + ## Конфигурация -- Файл: `config/global.yaml` -- Параметры регистрируются модулями при инициализации -- Сохранение: `from src import save_config; save_config()` +Модуль `config_manager` управляет конфигурацией с регистрацией параметров. + +```python +import src.utils.config_manager +config = src.utils.config_manager.config + +# Регистрация параметра +config.register(name="app_name", val="komAI", desc="Наименование проекта", cat="app") + +# Получение значения +config.get("app_name", cat="app") + +# Сохранение в файл +config.save() +``` + +См. [doc/src.utils.config_manager.md](doc/src.utils.config_manager.md) для подробной документации. + +## Логирование + +Все консольные выводы дублируются в логах. Настройка логирования в `config/global.yaml`. + +## Тесты + +```bash +python -m tests.test_config_manager +``` diff --git a/doc/src.utils.config_manager.md b/doc/src.utils.config_manager.md index 79bf1ec..616f9a3 100644 --- a/doc/src.utils.config_manager.md +++ b/doc/src.utils.config_manager.md @@ -34,14 +34,15 @@ config.reset() ## Константы ```python -from src.utils.config_manager import DEFAULT_CONFIG_FILE, DEFAULT_CONFIG_ENV, DEFAULT_CATEGORY +from src.utils.config_manager import DEFAULT_CONFIG_FILE, DEFAULT_CONFIG_ENV, DEFAULT_CATEGORY, ROOT_KEY ``` | Константа | Значение | Описание | -|----------|----------|----------| +|----------|---------|----------| | `DEFAULT_CONFIG_FILE` | `"config/global.yaml"` | Путь к файлу конфигурации | | `DEFAULT_CONFIG_ENV` | `"KOMAI_CONFIG_FILE"` | Переменная окружения для переопределения пути | | `DEFAULT_CATEGORY` | `"global"` | Категория по умолчанию | +| `ROOT_KEY` | `"$root$"` | Ключ для корневого значения во вложенных структурах | ## Регистрация параметров @@ -49,11 +50,11 @@ from src.utils.config_manager import DEFAULT_CONFIG_FILE, DEFAULT_CONFIG_ENV, DE config.register( name="param_name", # Имя параметра (обязательно) val="value", # Текущее значение (обязательно) - default="def_value", # Значение по умолчанию - desc="Description", # Описание параметра + default="def_value", # Значение по умолчанию + desc="Description", # Описание параметра cat="category", # Категория (по умолчанию "global") env="ENV_VAR", # Переменная окружения - validator=int # Функция валидации + validator=int # Функция валидации ) ``` @@ -68,8 +69,11 @@ config.register( ```python # Получить значение параметра -value = config.get("param_name") # категория по умолчанию -value = config.get("param_name", cat="app") # категория app +value = config.get("param_name") # категория по умолчанию (global) +value = config.get("param_name", cat="app") # категория app + +# Получить по полному ключу (без подстановки категории) +value = config.getraw("app.param_name") # Получить описание параметра desc = config.get_description("param_name", cat="app") @@ -83,6 +87,34 @@ param = config.get_parameter("param_name", cat="app") ```python # Изменить значение параметра config.set("param_name", "new_value", cat="app") + +# Изменить по полному ключу +config.setraw("app.param_name", "new_value") +``` + +## Вложенные параметры + +Параметры с точками в имени создают вложенные структуры: + +```python +config.register(name="coder", val="llama-coder", cat="models") +config.register(name="coder.thinking", val="full", cat="models") +config.register(name="coder.temperature", val="0.3", cat="models") +config.register(name="chatter", val="gemmini", cat="models") +config.register(name="chatter.thinking", val="none", cat="models") +config.register(name="chatter.temperature", val="0.9", cat="models") + +# Получить корневое значение +config.get("coder", cat="models") # "llama-coder" + +# Получить все параметры группы с корневым значением +config.getall("coder", cat="models") +# {"$root$": "llama-coder", "thinking": "full", "temperature": "0.3"} + +# Получить все параметры категории в виде вложенного dict +config.getrawall("models") +# {"coder": {"$root$": "llama-coder", "thinking": "full", "temperature": "0.3"}, +# "chatter": {"$root$": "gemmini", "thinking": "none", "temperature": "0.9"}} ``` ## Сохранение и загрузка @@ -90,7 +122,7 @@ config.set("param_name", "new_value", cat="app") ```python # Сохранить конфигурацию в файл config.save() # использовать путь по умолчанию -config.save("/path/to/config.yaml") # сохранить в указанный файл +config.save("/path/to/config.yaml") # сохранить в указанный файл # Загрузить конфигурацию из файла config.load() # использовать путь по умолчанию @@ -100,6 +132,28 @@ config.load("/path/to/config.yaml") # загрузить из указанн config.reset() # очищает все параметры и перезагружает ``` +## Валидация + +```python +def validate_level(old_val, new_val): + valid_levels = ["DEBUG", "INFO", "WARNING", "ERROR"] + if new_val not in valid_levels: + raise ValueError(f"Invalid level: {new_val}") + +config.register( + name="level", + val="INFO", + default="INFO", + desc="Уровень логирования", + cat="logging", + validator=validate_level +) + +# При изменении значения вызывается валидатор +config.set("level", "DEBUG") # OK +config.set("level", "INVALID") # Ошибка валидации, значение не меняется +``` + ## Примеры ### Базовое использование @@ -121,28 +175,6 @@ config.register( print(config.get("name", cat="app")) # "komAI" ``` -### С валидатором - -```python -def validate_level(old_val, new_val): - valid_levels = ["DEBUG", "INFO", "WARNING", "ERROR"] - if new_val not in valid_levels: - raise ValueError(f"Invalid level: {new_val}") - -config.register( - name="level", - val="INFO", - default="INFO", - desc="Уровень логирования", - cat="logging", - validator=validate_level -) - -# При изменении значения вызывается валидатор -config.set("level", "DEBUG") # OK -config.set("level", "INVALID") # Ошибка валидации, значение не меняется -``` - ### Переопределение через env ```python diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..c1a201d --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +PyYAML>=6.0 diff --git a/src/utils/config_manager/AGENTS.md b/src/utils/config_manager/AGENTS.md index 246061d..8ee9166 100644 --- a/src/utils/config_manager/AGENTS.md +++ b/src/utils/config_manager/AGENTS.md @@ -12,6 +12,10 @@ config = src.utils.config_manager.config - `config.register(name, val, default, desc, cat, env, validator)` - register a parameter - `config.get(name, cat)` - get parameter value - `config.set(name, value, cat)` - set parameter value (triggers validator if present) +- `config.getraw(key)` - get by full key path (no cat substitution) +- `config.setraw(key, value)` - set by full key path +- `config.getall(name, cat)` - get root + all nested params as dict with `ROOT_KEY` +- `config.getrawall(cat)` - get all params under category as nested dict - `config.get_parameter(name, cat)` - get ConfigParameter object - `config.get_description(name, cat)` - get parameter description - `config.load(path?)` - load from file or env @@ -31,6 +35,19 @@ On failure: logs error via `logger.error()` and raises exception. Value unchange - `DEFAULT_CONFIG_FILE = "config/global.yaml"` - `DEFAULT_CONFIG_ENV = "KOMAI_CONFIG_FILE"` - `DEFAULT_CATEGORY = "global"` +- `ROOT_KEY = "$root$"` - key name for root value in nested getall/getrawall results + +## Nested Parameters + +Params with dots in name create nested structures: +```python +config.register(name="coder", val="llama-coder", cat="models") +config.register(name="coder.thinking", val="full", cat="models") + +config.get("coder", cat="models") # "llama-coder" +config.getall("coder", cat="models") # {"$root$": "llama-coder", "thinking": "full"} +config.getrawall("models") # {"coder": {"$root$": "llama-coder", "thinking": "full"}} +``` ## ConfigParameter Properties @@ -40,6 +57,6 @@ On failure: logs error via `logger.error()` and raises exception. Value unchange ## Implementation Notes - Single global instance created at module import (`config` object in `__init__.py`) -- All `__init__.py` files re-export only `config` +- `ROOT_KEY` exported from module for custom notation if needed - Validator called via property setter on `_val` attribute - `reset()` re-creates params dict and reloads from file diff --git a/src/utils/config_manager/config_manager.py b/src/utils/config_manager/config_manager.py index 1db5cc7..7bfb4a7 100644 --- a/src/utils/config_manager/config_manager.py +++ b/src/utils/config_manager/config_manager.py @@ -7,6 +7,7 @@ from typing import Callable, Optional DEFAULT_CONFIG_FILE = "config/global.yaml" DEFAULT_CONFIG_ENV = "KOMAI_CONFIG_FILE" DEFAULT_CATEGORY = "global" +ROOT_KEY = "$root$" logger = logging.getLogger(__name__) @@ -121,32 +122,97 @@ class ConfigManager: self._params[key] = param return param - def get(self, name: str, cat: str = DEFAULT_CATEGORY) -> Optional[str]: + def get(self, name: str, cat: str = None) -> Optional[str]: + cat = cat or DEFAULT_CATEGORY key = f"{cat}.{name}" param = self._params.get(key) if param is None: return None return param.val - def get_description(self, name: str, cat: str = DEFAULT_CATEGORY) -> Optional[str]: + def getraw(self, key: str): + param = self._params.get(key) + if param is None: + return None + return param.val + + def get_description(self, name: str, cat: str = None) -> Optional[str]: + cat = cat or DEFAULT_CATEGORY key = f"{cat}.{name}" param = self._params.get(key) if param is None: return None return param.desc - def get_parameter( - self, name: str, cat: str = DEFAULT_CATEGORY - ) -> Optional[ConfigParameter]: + def get_parameter(self, name: str, cat: str = None) -> Optional[ConfigParameter]: + cat = cat or DEFAULT_CATEGORY key = f"{cat}.{name}" return self._params.get(key) - def set(self, name: str, value, cat: str = DEFAULT_CATEGORY) -> None: + def set(self, name: str, value, cat: str = None) -> None: + cat = cat or DEFAULT_CATEGORY key = f"{cat}.{name}" param = self._params.get(key) if param is not None: param.val = value + def setraw(self, key: str, value) -> None: + param = self._params.get(key) + if param is not None: + param.val = value + + def getall(self, name: str, cat: str = None) -> Optional[dict]: + cat = cat or DEFAULT_CATEGORY + root_key = f"{cat}.{name}" + root_param = self._params.get(root_key) + if root_param is None: + return None + + result = {ROOT_KEY: root_param.val} + prefix = root_key + "." + + for key, param in self._params.items(): + if key.startswith(prefix): + nested_key = key[len(prefix) :] + parts = nested_key.split(".") + if len(parts) == 1: + result[parts[0]] = param.val + else: + current = result + for part in parts[:-1]: + if part not in current: + current[part] = {} + current = current[part] + current[parts[-1]] = param.val + + return result + + def getrawall(self, key: str) -> Optional[dict]: + if not key.endswith("."): + key = key + "." + result = {} + + for param_key, param in self._params.items(): + if not param_key.startswith(key): + continue + + nested_key = param_key[len(key) :] + parts = nested_key.split(".") + + if len(parts) == 1: + result[parts[0]] = param.val + else: + current = result + for i, part in enumerate(parts[:-1]): + if part not in current: + current[part] = {} + elif isinstance(current[part], str): + current[part] = {ROOT_KEY: current[part]} + current = current[part] + current[parts[-1]] = param.val + + return result if result else None + def load(self, path: str = None) -> dict: if path: self._config_path = Path(path) diff --git a/tests/test_config_manager.py b/tests/test_config_manager.py index 2d5810e..65c7e8c 100644 --- a/tests/test_config_manager.py +++ b/tests/test_config_manager.py @@ -98,6 +98,45 @@ def test_config_validator(): assert config.get("level", cat="test") == "DEBUG" +def test_config_getraw_setraw(): + config.reset() + config.register(name="path", val="val1", cat="model1") + config.register(name="opt.temperature", val="val2", cat="model1") + config.register(name="path", val="val3", cat="global") + + assert config.getraw("model1.path") == "val1" + assert config.getraw("model1.opt.temperature") == "val2" + assert config.getraw("global.path") == "val3" + + config.setraw("model1.path", "new_val") + assert config.getraw("model1.path") == "new_val" + + +def test_config_getall_getrawall(): + config.reset() + config.register(name="coder", val="llama-coder", cat="models") + config.register(name="coder.thinking", val="full", cat="models") + config.register(name="coder.temperature", val="0.3", cat="models") + config.register(name="chatter", val="gemmini", cat="models") + config.register(name="chatter.thinking", val="none", cat="models") + config.register(name="chatter.temperature", val="0.9", cat="models") + + assert config.get("coder", cat="models") == "llama-coder" + + all_coder = config.getall("coder", cat="models") + assert all_coder == { + "$root$": "llama-coder", + "thinking": "full", + "temperature": "0.3", + } + + rawall = config.getrawall("models") + assert rawall == { + "coder": {"$root$": "llama-coder", "thinking": "full", "temperature": "0.3"}, + "chatter": {"$root$": "gemmini", "thinking": "none", "temperature": "0.9"}, + } + + def run_tests(): tests = [ test_config_manager_register_and_get, @@ -108,6 +147,8 @@ def run_tests(): test_config_parameter_get_parameter, test_config_set_value, test_config_validator, + test_config_getraw_setraw, + test_config_getall_getrawall, ] passed = 0