Add getall/getrawall for nested params, ROOT_KEY constant, tests and docs

This commit is contained in:
2026-04-15 20:04:04 +03:00
parent b8f34faff7
commit 3beccbf35e
6 changed files with 235 additions and 49 deletions

View File

@@ -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
```

View File

@@ -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

1
requirements.txt Normal file
View File

@@ -0,0 +1 @@
PyYAML>=6.0

View File

@@ -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

View File

@@ -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)

View File

@@ -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