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

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