Initial commit: add config_manager module with ConfigParameter and ConfigManager
This commit is contained in:
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()
|
||||
Reference in New Issue
Block a user