Files
config_manager/README.md

259 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Config Manager
## Описание
Пакет предназначен для запуска приложений с периодическим выполнением логики, перезагрузкой конфига и управлением по HTTP API.
**Контракт:** приложение наследует **ConfigManagerV2**, переопределяет **execute()** (периодическая работа). Управление (старт/стоп, health) — через каналы, которые создаются снаружи и передаются в конструктор в **control_channels** (в т.ч. HttpControlChannel для API).
## ConfigManager: устройство и взаимосвязи
**ConfigManager** (класс ConfigManagerV2) — точка входа приложения. Он наследует внутреннюю логику от **\_RuntimeController** (циклы воркера и обновления конфига, запуск/остановка каналов управления).
**Ядро (core):**
- **ConfigLoader** — читает конфиг из файла (YAML/JSON), считает хеш и отдаёт конфиг только при изменении; при ошибке парсинга возвращает последний валидный конфиг.
- **WorkerLoop** — в отдельном потоке циклически вызывает ваш метод `execute()` с паузой между вызовами; реагирует на событие остановки и колбэки успеха/ошибки.
- **LogManager** — применяет секцию `log` из конфига к логированию (dictConfig).
- **HealthAggregator** — собирает состояние: жизненный цикл (idle/starting/running/…), время последнего успешного `execute()` и таймаут здоровья; формирует единый ответ для health (ok/unhealthy).
- **ControlChannelBridge** — один мост для всех каналов: обработчики on_start/on_stop/on_status (сброс/установка halt, текст статуса).
**Каналы управления (control):**
- **ControlChannel** — абстрактный контракт: `start(on_start, on_stop, on_status)`, `stop()`.
- **HttpControlChannel** — HTTP API (`/health`, `/actions/start`, `/actions/stop`, `/actions/status`); использует **UvicornServerRunner**; для `/health` вызывает **HealthAggregator.collect()**, для действий — переданные обработчики из **ControlChannelBridge**.
- **TelegramControlChannel** — реализация через long polling Telegram; команды `/start`, `/stop`, `/status` вызывают переданные обработчики.
**Поток работы:** при `start()` менеджер поднимает каналы из **control_channels** (заданные снаружи), затем запускает два цикла: **WorkerLoop** и периодическое обновление конфига через **ConfigLoader**. Управление по API: `/health`, `/actions/start`, `/actions/stop` — если в control_channels передан **HttpControlChannel**. Остановка по halt завершает оба цикла; в конце останавливаются все каналы.
## Запуск приложения с ConfigManagerV2 и HttpControlChannel
1. **Наследуйте ConfigManagerV2** и реализуйте метод `execute()` (в нём — ваша периодическая работа). При необходимости переопределите `get_health_status()` для кастомного ответа `/health`.
2. **Создайте каналы снаружи и передайте в конструктор.** Для HTTP API создайте **HttpControlChannel**; для health нужен колбэк менеджера — передайте **control_channels** как фабрику (lambda, получающую менеджер):
```python
from config_manager.v2.control import HttpControlChannel
app = MyApp(
str(path_to_config),
control_channels=lambda m: [
HttpControlChannel(
host="0.0.0.0",
port=8000,
timeout=3,
health_provider=m.get_health_provider(),
)
],
)
```
Либо передайте готовый список каналов: `control_channels=[channel1, channel2]`.
3. **Запустите из async-контекста:** `await app.start()` или `asyncio.create_task(app.start())` для фона. Остановка: `await app.stop()` или запрос `/actions/stop` по HTTP.
**Минимальный пример с HTTP API:**
```python
import asyncio
import logging
from pathlib import Path
from config_manager import ConfigManager
from config_manager.v2.control import HttpControlChannel
class MyApp(ConfigManager):
def execute(self) -> None:
pass # ваша периодическая работа
async def main() -> None:
app = MyApp(
str(Path(__file__).parent / "config.yaml"),
control_channels=lambda m: [
HttpControlChannel(
host="0.0.0.0", port=8000, timeout=3,
health_provider=m.get_health_provider(),
)
],
)
asyncio.create_task(app.start())
await asyncio.sleep(3600)
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO)
asyncio.run(main())
```
Готовый пример: `tests/test_app.py`.
## Диаграмма классов
```mermaid
classDiagram
direction TB
class ConfigManagerV2 {
+str path
+Any config
+float update_interval
+float work_interval
-ConfigLoader _loader
-LifecycleState _state
+start() async
+stop() async
+execute()*
+get_health_status() HealthPayload
-_run() async
-_worker_loop() async
-_periodic_update_loop() async
}
class _RuntimeController {
<<внутренний>>
-_on_execute_success()
-_on_execute_error(exc)
-_worker_loop() async
-_periodic_update_loop() async
-_start_control_channels() async
-_stop_control_channels() async
-_run() async
}
class ConfigLoader {
+str path
+Any config
+Any last_valid_config
+load_if_changed() async
+parse_config(data) Any
-_read_file_sync() str
-read_file_async() async
}
class WorkerLoop {
-Callable execute
-Callable get_interval
-Event halt_event
+run() async
}
class LogManager {
+apply_config(config) None
}
class ControlChannel {
<<абстрактный>>
+start(on_start, on_stop, on_status) async*
+stop() async*
}
class TelegramControlChannel {
-str _token
-int _chat_id
+start(on_start, on_stop, on_status) async
+stop() async
-_poll_loop() async
}
class HttpControlChannel {
-UvicornServerRunner _runner
-Callable _health_provider
+start(on_start, on_stop, on_status) async
+stop() async
+int port
}
class HealthAggregator {
-Callable get_state
-Callable get_app_health
+collect() async HealthPayload
}
class ControlChannelBridge {
-Event _halt
-Callable _get_state
-Callable _get_status
+on_start() async str
+on_stop() async str
+on_status() async str
}
class UvicornServerRunner {
-Server _server
-Task _serve_task
+start(app) async
+stop() async
+int port
}
ConfigManagerV2 --|> _RuntimeController : наследует
ConfigManagerV2 --> ConfigLoader : использует
ConfigManagerV2 --> LogManager : использует
ConfigManagerV2 --> HealthAggregator : использует
ConfigManagerV2 --> ControlChannelBridge : использует
ConfigManagerV2 ..> ControlChannel : список каналов
_RuntimeController ..> WorkerLoop : создаёт в _worker_loop
TelegramControlChannel --|> ControlChannel : реализует
HttpControlChannel --|> ControlChannel : реализует
HttpControlChannel --> UvicornServerRunner : использует
HttpControlChannel ..> HealthAggregator : health_provider
ControlChannelBridge ..> ControlChannel : on_start, on_stop, on_status
```
## Логирование
Логирование настраивается из конфигурационного файла только если в нём есть секция **`log`** в формате [dictConfig](https://docs.python.org/3/library/logging.config.html#logging.config.dictConfig). Если секции `log` нет, менеджер пишет предупреждение в лог, а уровень Python по умолчанию (WARNING) сохраняется — сообщения INFO/DEBUG могут не отображаться.
**Как проверить, что конфигурация логирования применилась:**
- Убедитесь, что путь к файлу конфига верный и файл загружается при старте (в логах нет ошибки чтения конфига).
- Убедитесь, что в конфиге есть ключ `log` с `version: 1`, `handlers` и `loggers` (пример — `tests/config.yaml`).
- После старта в логе должно появиться сообщение уровня INFO: `"Logging configuration applied"` (из `config_manager.v2.core.log_manager`). Если его нет, либо секция `log` отсутствует (будет предупреждение), либо уровень root/пакета выше INFO.
## Установка
``pip install git+https://git.lesha.spb.ru/alex/config_manager.git``
## Контакты
- **e-mail**: lesha.spb@gmail.com
- **telegram**: https://t.me/lesha_spb
---
## Description (English)
**Config Manager** is a Python package for running applications with periodic execution logic, config reload, and control via HTTP API (and optionally Telegram).
**Contract:** Your application subclasses **ConfigManagerV2** and overrides **execute()** (periodic work). Control (start/stop, health) is done via channels that are created externally and passed into the constructor as **control_channels** (e.g. **HttpControlChannel** for REST API).
### Architecture and components
**ConfigManager** (class **ConfigManagerV2**) is the application entry point. It inherits internal logic from **_RuntimeController** (worker and config-update loops, starting/stopping control channels).
**Core:**
- **ConfigLoader** — reads config from file (YAML/JSON), computes hash and returns config only when changed; on parse error returns last valid config.
- **WorkerLoop** — in a separate thread, repeatedly calls your **execute()** with a pause; reacts to stop event and success/error callbacks.
- **LogManager** — applies the **log** section from config to logging (dictConfig).
- **HealthAggregator** — aggregates state: lifecycle (idle/starting/running/…), last successful **execute()** time and health timeout; produces a single health response (ok/unhealthy).
- **ControlChannelBridge** — single bridge for all channels: on_start/on_stop/on_status handlers (halt flag and status text).
**Control channels:**
- **ControlChannel** — abstract contract: **start(on_start, on_stop, on_status)**, **stop()**.
- **HttpControlChannel** — HTTP API (**/health**, **/actions/start**, **/actions/stop**, **/actions/status**); uses **UvicornServerRunner**; **/health** calls **HealthAggregator.collect()**, actions use handlers from **ControlChannelBridge**.
- **TelegramControlChannel** — implementation via Telegram long polling; commands **/start**, **/stop**, **/status** invoke the same handlers.
**Flow:** On **start()** the manager starts all channels from **control_channels**, then runs two loops: **WorkerLoop** and periodic config refresh via **ConfigLoader**. API control: **/health**, **/actions/start**, **/actions/stop** when **HttpControlChannel** is in **control_channels**. Halt stops both loops; then all channels are stopped.
### Running with ConfigManagerV2 and HttpControlChannel
1. Subclass **ConfigManagerV2** and implement **execute()** (your periodic logic). Optionally override **get_health_status()** for custom **/health** response.
2. Create channels externally and pass them to the constructor. For HTTP API create **HttpControlChannel**; for health the managers callback is needed — pass **control_channels** as a factory (lambda that receives the manager).
3. Start from an async context: **await app.start()** or **asyncio.create_task(app.start())** for background. Stop: **await app.stop()** or HTTP **/actions/stop**.
See the minimal example in the Russian section above; a full example is in **tests/test_app.py**.
### Logging
Logging is configured from the config file only if it contains a **log** section in [dictConfig](https://docs.python.org/3/library/logging.config.html#logging.config.dictConfig) format. If **log** is missing, the manager logs a warning and the default Python level (WARNING) remains.
### Installation
```bash
pip install git+https://git.lesha.spb.ru/alex/config_manager.git
```
### Contacts
- **e-mail**: lesha.spb@gmail.com
- **telegram**: https://t.me/lesha_spb