Initial commit: add config_manager module with ConfigParameter and ConfigManager

This commit is contained in:
2026-04-15 18:44:47 +03:00
commit b8f34faff7
21 changed files with 825 additions and 0 deletions

9
.env.example Normal file
View File

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

49
.gitignore vendored Normal file
View File

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

32
AGENTS.md Normal file
View File

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

21
CHECKLIST.md Normal file
View File

@@ -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
- [ ] Написать интеграционные тесты

28
README.md Normal file
View File

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

2
app/README.md Normal file
View File

@@ -0,0 +1,2 @@
# Приложения проекта

3
config/README.md Normal file
View File

@@ -0,0 +1,3 @@
# Каталог хранения конфигурационных файлов проекта.
Глобальный конфигурационный файл: `global.yaml`

11
config/global.yaml Normal file
View File

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

View File

@@ -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 # переменная окружения
```

5
log/README.md Normal file
View File

@@ -0,0 +1,5 @@
# Каталог для хранения логов.
Параметр конфига: `global.log_path`
`(gitignored)`

3
modules/README.md Normal file
View File

@@ -0,0 +1,3 @@
# Каталог модулей проекта
Параметр конфига: `global.modules`

7
src/README.md Normal file
View File

@@ -0,0 +1,7 @@
# src - основные исходники
## Централизованный API
## Утилиты
- `config_manager/` - управление конфигурацией

0
src/__init__.py Normal file
View File

3
src/utils/README.md Normal file
View File

@@ -0,0 +1,3 @@
# src/utils - утилиты
- `config_manager/` - ConfigManager

0
src/utils/__init__.py Normal file
View File

View File

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

View File

@@ -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` - общие

View File

@@ -0,0 +1,3 @@
from .config_manager import config
__all__ = ["config"]

View File

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

1
tests/README.md Normal file
View File

@@ -0,0 +1 @@
# Тестовые юниты для тестирования различных аспектов проекта, которы можно выполнить автономно из консоли.

View File

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