Initial commit: add config_manager module with ConfigParameter and ConfigManager
This commit is contained in:
9
.env.example
Normal file
9
.env.example
Normal 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
49
.gitignore
vendored
Normal 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
32
AGENTS.md
Normal 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
21
CHECKLIST.md
Normal 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
28
README.md
Normal 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
2
app/README.md
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# Приложения проекта
|
||||||
|
|
||||||
3
config/README.md
Normal file
3
config/README.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Каталог хранения конфигурационных файлов проекта.
|
||||||
|
|
||||||
|
Глобальный конфигурационный файл: `global.yaml`
|
||||||
11
config/global.yaml
Normal file
11
config/global.yaml
Normal 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"
|
||||||
172
doc/src.utils.config_manager.md
Normal file
172
doc/src.utils.config_manager.md
Normal 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
5
log/README.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# Каталог для хранения логов.
|
||||||
|
|
||||||
|
Параметр конфига: `global.log_path`
|
||||||
|
|
||||||
|
`(gitignored)`
|
||||||
3
modules/README.md
Normal file
3
modules/README.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Каталог модулей проекта
|
||||||
|
|
||||||
|
Параметр конфига: `global.modules`
|
||||||
7
src/README.md
Normal file
7
src/README.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# src - основные исходники
|
||||||
|
|
||||||
|
## Централизованный API
|
||||||
|
|
||||||
|
## Утилиты
|
||||||
|
|
||||||
|
- `config_manager/` - управление конфигурацией
|
||||||
0
src/__init__.py
Normal file
0
src/__init__.py
Normal file
3
src/utils/README.md
Normal file
3
src/utils/README.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# src/utils - утилиты
|
||||||
|
|
||||||
|
- `config_manager/` - ConfigManager
|
||||||
0
src/utils/__init__.py
Normal file
0
src/utils/__init__.py
Normal file
45
src/utils/config_manager/AGENTS.md
Normal file
45
src/utils/config_manager/AGENTS.md
Normal 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
|
||||||
92
src/utils/config_manager/README.md
Normal file
92
src/utils/config_manager/README.md
Normal 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` - общие
|
||||||
3
src/utils/config_manager/__init__.py
Normal file
3
src/utils/config_manager/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from .config_manager import config
|
||||||
|
|
||||||
|
__all__ = ["config"]
|
||||||
208
src/utils/config_manager/config_manager.py
Normal file
208
src/utils/config_manager/config_manager.py
Normal 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
1
tests/README.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# Тестовые юниты для тестирования различных аспектов проекта, которы можно выполнить автономно из консоли.
|
||||||
131
tests/test_config_manager.py
Normal file
131
tests/test_config_manager.py
Normal 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)
|
||||||
Reference in New Issue
Block a user