From b8f34faff746e053d3f11ed22c3d47c834455ae8 Mon Sep 17 00:00:00 2001 From: Komisar Date: Wed, 15 Apr 2026 18:44:47 +0300 Subject: [PATCH] Initial commit: add config_manager module with ConfigParameter and ConfigManager --- .env.example | 9 + .gitignore | 49 +++++ AGENTS.md | 32 ++++ CHECKLIST.md | 21 +++ README.md | 28 +++ app/README.md | 2 + config/README.md | 3 + config/global.yaml | 11 ++ doc/src.utils.config_manager.md | 172 +++++++++++++++++ log/README.md | 5 + modules/README.md | 3 + src/README.md | 7 + src/__init__.py | 0 src/utils/README.md | 3 + src/utils/__init__.py | 0 src/utils/config_manager/AGENTS.md | 45 +++++ src/utils/config_manager/README.md | 92 +++++++++ src/utils/config_manager/__init__.py | 3 + src/utils/config_manager/config_manager.py | 208 +++++++++++++++++++++ tests/README.md | 1 + tests/test_config_manager.py | 131 +++++++++++++ 21 files changed, 825 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 AGENTS.md create mode 100644 CHECKLIST.md create mode 100644 README.md create mode 100644 app/README.md create mode 100644 config/README.md create mode 100644 config/global.yaml create mode 100644 doc/src.utils.config_manager.md create mode 100644 log/README.md create mode 100644 modules/README.md create mode 100644 src/README.md create mode 100644 src/__init__.py create mode 100644 src/utils/README.md create mode 100644 src/utils/__init__.py create mode 100644 src/utils/config_manager/AGENTS.md create mode 100644 src/utils/config_manager/README.md create mode 100644 src/utils/config_manager/__init__.py create mode 100644 src/utils/config_manager/config_manager.py create mode 100644 tests/README.md create mode 100644 tests/test_config_manager.py diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..801c177 --- /dev/null +++ b/.env.example @@ -0,0 +1,9 @@ +# komAI environment variables + +# Path to config file (optional) +# KOMAI_CONFIG=config/global.yaml + +# Logging (optional) +# LOGGING_LOG_PATH=./log +# LOGGING_LOG_FILE=app.log +# LOGGING_LEVEL=INFO \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cbf9e80 --- /dev/null +++ b/.gitignore @@ -0,0 +1,49 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +*.egg + +# Virtual environments +venv/ +env/ +ENV/ +.venv/ + +# IDE +.idea/ +.vscode/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db + +# Log files (runtime) +log/*.log + +# Models (large binary files) +models/ + +# Environment +.env +.env.local + +.opencode/ diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..be0c4c9 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,32 @@ +# komAI Agent Guidelines + +## Entry Point +``` +python -m app.komAI +``` + +## Architecture +- `app/` - application entry point (komAI.py expected) +- `src/` - source code, `src/__init__.py` exposes centralized API +- `config/` - YAML configuration; `config/global.yaml` is the main config +- `modules/` - pluggable modules, configured via `global.modules` +- `log/` - runtime logs +- `tests/` - standalone unit tests (executable from CLI) + +## Config System +- Modules register parameters at initialization +- Save config: `from src import save_config; save_config()` + +## Logging +- All console output is duplicated to log files +- Log config in `config/global.yaml` (level, file, path) + +## Env Vars (see `.env.example`) +- `KOMAI_CONFIG` - path to config file (optional) +- `LOGGING_LOG_PATH`, `LOGGING_LOG_FILE`, `LOGGING_LEVEL` + +## Requirements +- Python >3.10 + +## No Code Yet +This repo is a scaffold. No `.py` source files exist yet. Do not assume any modules, classes, or APIs are implemented. \ No newline at end of file diff --git a/CHECKLIST.md b/CHECKLIST.md new file mode 100644 index 0000000..14e44e1 --- /dev/null +++ b/CHECKLIST.md @@ -0,0 +1,21 @@ +# komAI Development Checklist + +## Completed + +- [x] Создан модуль `src/utils/config_manager` для работы с конфигурацией + - Реализован `ConfigParameter` с поддержкой валидации + - Реализован `ConfigManager` с регистрацией, загрузкой, сохранением параметров + - Глобальный экземпляр `config` доступен при импорте модуля + - Поддержка категорий, описаний, переменных окружения + - 8/8 тестов проходят + - Документация: `doc/src.utils.config_manager.md`, `src/utils/config_manager/AGENTS.md` + +## In Progress + +## Pending + +- [ ] Создать приложение `app/komAI.py` как точку входа +- [ ] Реализовать модуль логирования +- [ ] Реализовать систему модулей (`modules/`) +- [ ] Настроить CI/CD +- [ ] Написать интеграционные тесты diff --git a/README.md b/README.md new file mode 100644 index 0000000..35fbb6e --- /dev/null +++ b/README.md @@ -0,0 +1,28 @@ +# komAI - локальный AI-ассистент + +## Требования + +- Python >3.10 +- Все консольные выводы дублируются в логах + +## Структура + +- `app/` - точка входа (komAI.py) +- `src/` - исходный код +- `src/__init__.py` - централизованный API +- `config/` - YAML конфигурация +- `tests/` - юнит-тесты +- `modules/` - подключаемые модули +- `log/` - файлы логов + +## Быстрый старт + +```bash +python -m app.komAI +``` + +## Конфигурация + +- Файл: `config/global.yaml` +- Параметры регистрируются модулями при инициализации +- Сохранение: `from src import save_config; save_config()` diff --git a/app/README.md b/app/README.md new file mode 100644 index 0000000..9fb14a0 --- /dev/null +++ b/app/README.md @@ -0,0 +1,2 @@ +# Приложения проекта + diff --git a/config/README.md b/config/README.md new file mode 100644 index 0000000..f26c41d --- /dev/null +++ b/config/README.md @@ -0,0 +1,3 @@ +# Каталог хранения конфигурационных файлов проекта. + +Глобальный конфигурационный файл: `global.yaml` diff --git a/config/global.yaml b/config/global.yaml new file mode 100644 index 0000000..9edbf39 --- /dev/null +++ b/config/global.yaml @@ -0,0 +1,11 @@ +#Уровень логирования +#logging.level: "INFO" +logging.level: "INFO" + +#Имя файла логов +#logging.log_file: "app.log" +logging.log_file: "app.log" + +#Путь к директории логов +#logging.log_path: "./log" +logging.log_path: "./log" diff --git a/doc/src.utils.config_manager.md b/doc/src.utils.config_manager.md new file mode 100644 index 0000000..79bf1ec --- /dev/null +++ b/doc/src.utils.config_manager.md @@ -0,0 +1,172 @@ +# ConfigManager + +Управление конфигурацией с регистрацией параметров и описаний. + +## Установка + +```python +import src.utils.config_manager +config = src.utils.config_manager.config +``` + +## Быстрый старт + +```python +import src.utils.config_manager +config = src.utils.config_manager.config + +# Регистрация параметра +config.register(name="app_name", val="komAI", desc="Наименование проекта", cat="app") + +# Получение значения +name = config.get("app_name", cat="app") + +# Изменение значения +config.set("app_name", "NewName", cat="app") + +# Сохранение в файл +config.save() + +# Сброс и перезагрузка +config.reset() +``` + +## Константы + +```python +from src.utils.config_manager import DEFAULT_CONFIG_FILE, DEFAULT_CONFIG_ENV, DEFAULT_CATEGORY +``` + +| Константа | Значение | Описание | +|----------|----------|----------| +| `DEFAULT_CONFIG_FILE` | `"config/global.yaml"` | Путь к файлу конфигурации | +| `DEFAULT_CONFIG_ENV` | `"KOMAI_CONFIG_FILE"` | Переменная окружения для переопределения пути | +| `DEFAULT_CATEGORY` | `"global"` | Категория по умолчанию | + +## Регистрация параметров + +```python +config.register( + name="param_name", # Имя параметра (обязательно) + val="value", # Текущее значение (обязательно) + default="def_value", # Значение по умолчанию + desc="Description", # Описание параметра + cat="category", # Категория (по умолчанию "global") + env="ENV_VAR", # Переменная окружения + validator=int # Функция валидации +) +``` + +### Категории + +Рекомендуемые категории: +- `app` - параметры приложения +- `logging` - параметры логирования +- `global` - общие параметры (по умолчанию) + +## Получение значений + +```python +# Получить значение параметра +value = config.get("param_name") # категория по умолчанию +value = config.get("param_name", cat="app") # категория app + +# Получить описание параметра +desc = config.get_description("param_name", cat="app") + +# Получить объект ConfigParameter +param = config.get_parameter("param_name", cat="app") +``` + +## Изменение значений + +```python +# Изменить значение параметра +config.set("param_name", "new_value", cat="app") +``` + +## Сохранение и загрузка + +```python +# Сохранить конфигурацию в файл +config.save() # использовать путь по умолчанию +config.save("/path/to/config.yaml") # сохранить в указанный файл + +# Загрузить конфигурацию из файла +config.load() # использовать путь по умолчанию +config.load("/path/to/config.yaml") # загрузить из указанного файла + +# Сбросить конфигурацию +config.reset() # очищает все параметры и перезагружает +``` + +## Примеры + +### Базовое использование + +```python +import src.utils.config_manager +config = src.utils.config_manager.config + +# Регистрация параметра приложения +config.register( + name="name", + val="komAI", + default="MyApp", + desc="Наименование проекта", + cat="app" +) + +# Получение значения +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 +# Установить путь через переменную окружения +# export KOMAI_CONFIG_FILE=/path/to/config.yaml + +config.load() # автоматически использует KOMAI_CONFIG_FILE +``` + +### Работа с ConfigParameter + +```python +param = config.get_parameter("name", cat="app") + +# Конвертация значений +param.as_int() # в целое число +param.as_bool() # в boolean +param.as_str() # в строку + +# Доступ к свойствам +param.name # имя параметра +param.val # текущее значение +param.default # значение по умолчанию +param.desc # описание +param.cat # категория +param.env # переменная окружения +``` diff --git a/log/README.md b/log/README.md new file mode 100644 index 0000000..6026a3e --- /dev/null +++ b/log/README.md @@ -0,0 +1,5 @@ +# Каталог для хранения логов. + +Параметр конфига: `global.log_path` + +`(gitignored)` diff --git a/modules/README.md b/modules/README.md new file mode 100644 index 0000000..468c34e --- /dev/null +++ b/modules/README.md @@ -0,0 +1,3 @@ +# Каталог модулей проекта + +Параметр конфига: `global.modules` diff --git a/src/README.md b/src/README.md new file mode 100644 index 0000000..1da9003 --- /dev/null +++ b/src/README.md @@ -0,0 +1,7 @@ +# src - основные исходники + +## Централизованный API + +## Утилиты + +- `config_manager/` - управление конфигурацией diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/utils/README.md b/src/utils/README.md new file mode 100644 index 0000000..7515815 --- /dev/null +++ b/src/utils/README.md @@ -0,0 +1,3 @@ +# src/utils - утилиты + +- `config_manager/` - ConfigManager diff --git a/src/utils/__init__.py b/src/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/utils/config_manager/AGENTS.md b/src/utils/config_manager/AGENTS.md new file mode 100644 index 0000000..246061d --- /dev/null +++ b/src/utils/config_manager/AGENTS.md @@ -0,0 +1,45 @@ +# ConfigManager + +## Usage + +```python +import src.utils.config_manager +config = src.utils.config_manager.config +``` + +## Key APIs + +- `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.get_parameter(name, cat)` - get ConfigParameter object +- `config.get_description(name, cat)` - get parameter description +- `config.load(path?)` - load from file or env +- `config.save(path?)` - save to file +- `config.reset()` - clear all and reload + +## Validator + +Signature: `validator(old_val, new_val)` - must raise on invalid values. + +Called on every value change (via `set()`, direct assignment, or `load()`). + +On failure: logs error via `logger.error()` and raises exception. Value unchanged. + +## Constants + +- `DEFAULT_CONFIG_FILE = "config/global.yaml"` +- `DEFAULT_CONFIG_ENV = "KOMAI_CONFIG_FILE"` +- `DEFAULT_CATEGORY = "global"` + +## ConfigParameter Properties + +- `name`, `val`, `default`, `desc`, `cat`, `env`, `validator` +- `as_int(default)`, `as_bool()`, `as_str()` + +## Implementation Notes + +- Single global instance created at module import (`config` object in `__init__.py`) +- All `__init__.py` files re-export only `config` +- Validator called via property setter on `_val` attribute +- `reset()` re-creates params dict and reloads from file diff --git a/src/utils/config_manager/README.md b/src/utils/config_manager/README.md new file mode 100644 index 0000000..2d27c27 --- /dev/null +++ b/src/utils/config_manager/README.md @@ -0,0 +1,92 @@ +# ConfigManager + +Управление конфигурацией с регистрацией параметров и описаний. + +## Описание + +### Класс ConfigParameter + +Базовый класс для описания параметра + + name: Имя параметра + val: Значение + def: Значение по умолчанию + desc: Описание параметра (не обязательно) + cat: Категория параметра (не обязательно, по умолчанию 'global') + env: Имя переменной окружения (не обязательно) + validator: Функция проверки допустимости параметра (если не используется, то значение -- строка. не обязательно) + +Параметры хранятся к конфиге так: + + ```config + #$description + #$category.$name: $value + $category.$name: $value + ``` + например: + ```config + #Наименование проекта + #global.name: "komAI" + global.name: "SuperAPP" + ``` + +Пример использования параметра: + + ```python + param1 = ConfigParameter(cat="global", name="name", val="komAI", desc="Наименование проекта", def="SuppaPuppa") + + #name:name value:Наименование проекта + print(f"name:{param1.name} value:{param1.desc}") + + #name.as_int:0 + print(f"{param1.name}.as_int:{param1.as_int()}") + + ##Наименование проекта\n#global.name: "SuppaPuppa"\nglobal.name: "komAI" + print(param1) + ``` + +### Класс ConfigManager + +- Базовый класс для работы с параметрами +- Параметры хранятся текстовом конфиге (например в `config/global.yaml`) +- Файл можно переопределить из командной строки (например `--config ~/myApp/config/app.yaml`) +- Файл можно переопределить через переменную окружения `$projectname_CONFIG=~/superApp/config.yaml` +- Приоритет определения параметров (от высшего к низшему): + коммандная строка --> переменные окружения --> файл ~/.ENV +- Для ConfigManager значение файла конфигурации по умолчанию задано в тексте конcтантой. Например: + ```python + config_file_default = "config/global.yaml" + config_env_default = "KOMAI_CONFIG_FILE" + ``` +- Регистрация и инициализация класса должна быть глобальная + +Пример использования: +```python +from src.utils.config_manager.config_manager import get_config + +config = get_config() +config.register(cat="global", name="app.name", val="komAI", desc="Наименование проекта") +#если не задана cat, использовать значение по умолчанию +#данный вызов вернёт значение параметра 'global.app.name' +value = config.get("app.name") +#данный вызов вернёт значение описания параметра 'global.app.name' +desc = config.get_description("app.name", cat="global") +``` + +## Глобальный доступ + +`get_config()` - возвращает singleton ConfigManager + +## Регистрация параметров модулями + +Каждый модуль регистрирует свои параметры при инициализации: +```python +config.register(name="level", val="INFO", desc="Уровень логирования", cat="logging") +``` + +## Категории + +Возможные категории параметров +- `app` - приложение +- `logging` - логирование +- `global` - общие diff --git a/src/utils/config_manager/__init__.py b/src/utils/config_manager/__init__.py new file mode 100644 index 0000000..538e481 --- /dev/null +++ b/src/utils/config_manager/__init__.py @@ -0,0 +1,3 @@ +from .config_manager import config + +__all__ = ["config"] diff --git a/src/utils/config_manager/config_manager.py b/src/utils/config_manager/config_manager.py new file mode 100644 index 0000000..1db5cc7 --- /dev/null +++ b/src/utils/config_manager/config_manager.py @@ -0,0 +1,208 @@ +import logging +import os +import yaml +from pathlib import Path +from typing import Callable, Optional + +DEFAULT_CONFIG_FILE = "config/global.yaml" +DEFAULT_CONFIG_ENV = "KOMAI_CONFIG_FILE" +DEFAULT_CATEGORY = "global" + +logger = logging.getLogger(__name__) + + +class ConfigParameter: + def __init__( + self, + name: str, + val, + default=None, + desc: str = None, + cat: str = DEFAULT_CATEGORY, + env: str = None, + validator: Callable = None, + ): + self.name = name + self.default = default if default is not None else val + self.desc = desc + self.cat = cat + self.env = env + self.validator = validator + self._val = val + + @property + def val(self): + return self._val + + @val.setter + def val(self, new_val): + self._set_val(self._val, new_val) + + def _set_val(self, old_val, new_val): + if self.validator: + try: + self.validator(old_val, new_val) + self._val = new_val + except Exception as e: + logger.error( + f"ConfigParameter '{self.cat}.{self.name}': validation failed for value '{new_val}': {e}" + ) + raise + else: + self._val = new_val + + def as_int(self, default: int = 0) -> int: + try: + return int(self.val) + except (ValueError, TypeError): + return default + + def as_str(self) -> str: + return str(self.val) + + def as_bool(self) -> bool: + if isinstance(self.val, bool): + return self.val + if isinstance(self.val, str): + return self.val.lower() in ("true", "yes", "1", "on") + return bool(self.val) + + def __str__(self) -> str: + comment_line = f"#{self.desc}" if self.desc else None + default_line = f"#{self.cat}.{self.name}: {self._format_value(self.default)}" + value_line = f"{self.cat}.{self.name}: {self._format_value(self.val)}" + lines = [l for l in [comment_line, default_line, value_line] if l is not None] + return "\n".join(lines) + + def _format_value(self, val) -> str: + if val is None: + return "null" + if isinstance(val, bool): + return "true" if val else "false" + if isinstance(val, str): + if not val or val.lower() in ("true", "false", "null"): + return f'"{val}"' + return f'"{val}"' + return str(val).lower() if isinstance(val, bool) else str(val) + + +class ConfigManager: + def __init__( + self, + config_file: str = DEFAULT_CONFIG_FILE, + config_env: str = DEFAULT_CONFIG_ENV, + ): + self._config_file = config_file + self._config_env = config_env + self._params: dict[str, ConfigParameter] = {} + self._config: dict = {} + self._config_path: Optional[Path] = None + + def register( + self, + name: str, + val=None, + default=None, + desc: str = None, + cat: str = DEFAULT_CATEGORY, + env: str = None, + validator: Callable = None, + ) -> ConfigParameter: + param = ConfigParameter( + name=name, + val=val, + default=default, + desc=desc, + cat=cat, + env=env, + validator=validator, + ) + key = f"{cat}.{name}" + self._params[key] = param + return param + + def get(self, name: str, cat: str = DEFAULT_CATEGORY) -> Optional[str]: + 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]: + 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]: + key = f"{cat}.{name}" + return self._params.get(key) + + def set(self, name: str, value, cat: str = DEFAULT_CATEGORY) -> None: + key = f"{cat}.{name}" + param = self._params.get(key) + if param is not None: + param.val = value + + def load(self, path: str = None) -> dict: + if path: + self._config_path = Path(path) + else: + env_path = os.environ.get(self._config_env) + if env_path: + self._config_path = Path(env_path) + else: + self._config_path = Path(self._config_file) + + if self._config_path.exists(): + with open(self._config_path, "r", encoding="utf-8") as f: + self._config = yaml.safe_load(f) or {} + else: + self._config = {} + + self._apply_config_to_params() + return self._config + + def reset(self) -> None: + self._params = {} + self._config = {} + self._config_path = None + self.load() + + def save(self, path: str = None) -> None: + if path: + self._config_path = Path(path) + elif self._config_path is None: + self._config_path = Path(self._config_file) + + self._config = {} + for key, param in self._params.items(): + self._config[key] = param.val + + self._config_path.parent.mkdir(parents=True, exist_ok=True) + with open(self._config_path, "w", encoding="utf-8") as f: + yaml.safe_dump( + self._config, f, default_flow_style=False, allow_unicode=True + ) + + def _apply_config_to_params(self) -> None: + for key, param in self._params.items(): + if key in self._config: + param.val = self._config[key] + elif param.env: + env_val = os.environ.get(param.env) + if env_val is not None: + param.val = env_val + + def __str__(self) -> str: + lines = [] + for param in self._params.values(): + lines.append(str(param)) + return "\n\n".join(lines) + + +config = ConfigManager() +config.load() diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000..100ed88 --- /dev/null +++ b/tests/README.md @@ -0,0 +1 @@ +# Тестовые юниты для тестирования различных аспектов проекта, которы можно выполнить автономно из консоли. diff --git a/tests/test_config_manager.py b/tests/test_config_manager.py new file mode 100644 index 0000000..2d5810e --- /dev/null +++ b/tests/test_config_manager.py @@ -0,0 +1,131 @@ +import os +import tempfile +from pathlib import Path + +from src.utils.config_manager import config + + +def test_config_manager_register_and_get(): + config.reset() + config.register(name="app_name", val="komAI", desc="Application name", cat="app") + assert config.get("app_name", cat="app") == "komAI" + assert config.get_description("app_name", cat="app") == "Application name" + + +def test_config_manager_register_default_category(): + config.reset() + config.register(name="param1", val="value1") + assert config.get("param1") == "value1" + + +def test_config_manager_singleton(): + from src.utils.config_manager import config as config2 + + assert config is config2 + + +def test_config_manager_load_save(): + config.reset() + with tempfile.TemporaryDirectory() as tmpdir: + config_path = Path(tmpdir) / "config.yaml" + config.register(name="app_name", val="TestApp", cat="app") + config.register(name="debug", val="true", cat="app") + + config.save(str(config_path)) + assert config_path.exists() + + config.reset() + config.register(name="app_name", val="Other", cat="app") + config.register(name="debug", val="false", cat="app") + config.load(str(config_path)) + + assert config.get("app_name", cat="app") == "TestApp" + assert config.get("debug", cat="app") == "true" + + +def test_config_manager_env_override(): + config.reset() + env_var = "TEST_APP_CONFIG" + os.environ[env_var] = "/nonexistent/path.yaml" + + from src.utils.config_manager.config_manager import ConfigManager + + test_config = ConfigManager(config_env=env_var) + test_config.register(name="param1", val="original") + test_config.load() + + assert test_config._config_path == Path("/nonexistent/path.yaml") + del os.environ[env_var] + + +def test_config_parameter_get_parameter(): + config.reset() + config.register(name="param1", val="value1", cat="test") + param = config.get_parameter("param1", cat="test") + assert param is not None + assert param.val == "value1" + assert param.name == "param1" + assert param.default == "value1" + + +def test_config_set_value(): + config.reset() + config.register(name="param1", val="original", cat="test") + assert config.get("param1", cat="test") == "original" + + config.set("param1", "modified", cat="test") + assert config.get("param1", cat="test") == "modified" + + +def test_config_validator(): + config.reset() + + 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", cat="test", validator=validate_level) + assert config.get("level", cat="test") == "INFO" + + config.set("level", "DEBUG", cat="test") + assert config.get("level", cat="test") == "DEBUG" + + try: + config.set("level", "INVALID", cat="test") + assert False, "Should have raised ValueError" + except ValueError: + assert config.get("level", cat="test") == "DEBUG" + + +def run_tests(): + tests = [ + test_config_manager_register_and_get, + test_config_manager_register_default_category, + test_config_manager_singleton, + test_config_manager_load_save, + test_config_manager_env_override, + test_config_parameter_get_parameter, + test_config_set_value, + test_config_validator, + ] + + passed = 0 + failed = 0 + + for test in tests: + try: + test() + print(f"PASS: {test.__name__}") + passed += 1 + except Exception as e: + print(f"FAIL: {test.__name__} - {e}") + failed += 1 + + print(f"\n{passed}/{passed + failed} tests passed") + return failed == 0 + + +if __name__ == "__main__": + success = run_tests() + exit(0 if success else 1)