Custom Config Providers API
Доступно с версии 3.2.0 · Модуль:
chutils.config
Custom Config Providers — это расширяемый API для интеграции внешних источников
конфигурации (БД, Redis, Consul, Vault, удалённые API и др.) в единый интерфейс
chutils.config. Он позволяет динамически переопределять настройки в рантайме без
изменения локальных файлов config.yml.
Быстрый старт
from chutils.config import register_provider, get_config_value
from chutils.config.custom_providers import DictConfigProvider
# 1. Создаём провайдер
provider = DictConfigProvider(
{
"database": {"host": "prod-db.internal", "port": "5432"},
"app": {"debug": "false"},
}
)
# 2. Регистрируем (priority=10 — высокий приоритет)
register_provider(provider, priority=10)
# 3. Используем стандартный API — провайдер опрашивается автоматически
host = get_config_value("database", "host")
# → "prod-db.internal"
Порядок приоритетов
При вызове get_config_value / aget_config_value источники опрашиваются
в следующем порядке (от высшего к низшему):
| # | Источник | Описание |
|---|---|---|
| 1 | Кастомные провайдеры | По числовому приоритету (priority). Меньше → выше. |
| 2 | Переменные окружения | CH_[SECTION]_[KEY] |
| 3 | config.local.yml |
Локальные переопределения (не коммитятся) |
| 4 | config.{CH_ENV}.yml |
Файл окружения (production, staging…) |
| 5 | config.yml |
Базовые настройки |
Важно: если провайдер вернул
None— поиск продолжается к следующему источнику. ВозвратNoneозначает «не знаю», а не «значение отсутствует».
Реализация собственного провайдера
Унаследуйтесь от BaseConfigProvider и реализуйте два метода:
from typing import Any
from chutils.config.custom_providers import BaseConfigProvider
class RedisConfigProvider(BaseConfigProvider):
"""Провайдер, читающий настройки из Redis Hash."""
def __init__(self, redis_client, hash_key: str = "app:config") -> None:
self._redis = redis_client
self._hash_key = hash_key
def get_value(self, section: str, key: str) -> Any | None:
# Ключ в Redis: "section:key"
field = f"{section.lower()}:{key.lower()}"
value = self._redis.hget(self._hash_key, field)
return value.decode() if value else None
async def aget_value(self, section: str, key: str) -> Any | None:
field = f"{section.lower()}:{key.lower()}"
value = await self._redis.hget(self._hash_key, field)
return value.decode() if value else None
Регистрация:
import redis.asyncio as redis
from chutils.config import register_provider
client = redis.Redis(host="localhost")
register_provider(RedisConfigProvider(client), priority=5)
Пример: SQLAlchemyConfigProvider
Пример полноценного провайдера для хранения конфигурации в реляционной БД
через SQLAlchemy. Значения берутся из таблицы config_entries.
Схема таблицы
CREATE TABLE config_entries
(
id SERIAL PRIMARY KEY,
section VARCHAR(128) NOT NULL,
key VARCHAR(128) NOT NULL,
value TEXT,
UNIQUE (section, key)
);
Реализация
from __future__ import annotations
import asyncio
from typing import Any
from sqlalchemy import text
from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, create_async_engine
from sqlalchemy.orm import sessionmaker
from chutils.config.custom_providers import BaseConfigProvider
class SQLAlchemyConfigProvider(BaseConfigProvider):
"""Провайдер конфигурации на базе SQLAlchemy (async).
Читает значения из таблицы ``config_entries``.
Поддерживает TTL-кэш для снижения нагрузки на БД.
Args:
dsn: DSN для подключения к БД (asyncpg / aiosqlite и др.).
cache_ttl: Время жизни кэша в секундах. 0 = без кэша.
"""
def __init__(self, dsn: str, cache_ttl: int = 60) -> None:
self._engine: AsyncEngine = create_async_engine(dsn, pool_pre_ping=True)
self._session_factory = sessionmaker(
self._engine, class_=AsyncSession, expire_on_commit=False
)
self._cache: dict[str, Any] = {}
self._cache_ttl = cache_ttl
self._cache_ts: dict[str, float] = {}
# ------------------------------------------------------------------
# Публичный интерфейс BaseConfigProvider
# ------------------------------------------------------------------
def get_value(self, section: str, key: str) -> Any | None:
"""Синхронная обёртка — запускает async-метод в executor."""
try:
loop = asyncio.get_running_loop()
except RuntimeError:
# Нет активного event loop — создаём временный
return asyncio.run(self.aget_value(section, key))
else:
# Есть event loop — нельзя вызвать asyncio.run(), используем
# синхронное чтение из кэша или возвращаем None
cache_key = f"{section.lower()}:{key.lower()}"
return self._cache.get(cache_key)
async def aget_value(self, section: str, key: str) -> Any | None:
"""Асинхронно читает значение из БД с кэшированием."""
import time
cache_key = f"{section.lower()}:{key.lower()}"
# Проверяем кэш
if self._cache_ttl > 0 and cache_key in self._cache:
age = time.monotonic() - self._cache_ts.get(cache_key, 0)
if age < self._cache_ttl:
return self._cache[cache_key]
# Запрос к БД
async with self._session_factory() as session:
row = await session.execute(
text(
"SELECT value FROM config_entries "
"WHERE LOWER(section) = :section AND LOWER(key) = :key "
"LIMIT 1"
),
{"section": section.lower(), "key": key.lower()},
)
result = row.scalar_one_or_none()
if result is not None:
self._cache[cache_key] = result
self._cache_ts[cache_key] = time.monotonic()
return result
async def close(self) -> None:
"""Закрывает соединение с БД."""
await self._engine.dispose()
Использование
from chutils.config import register_provider, get_config_value, aget_config_value
# При старте приложения
provider = SQLAlchemyConfigProvider(
dsn="postgresql+asyncpg://user:pass@db/myapp",
cache_ttl=30,
)
register_provider(provider, priority=10)
# Синхронный доступ (читает из кэша провайдера)
db_host = get_config_value("database", "host", fallback="localhost")
# Асинхронный доступ (опрашивает БД напрямую)
async def get_db_host() -> str:
return await aget_config_value("database", "host", fallback="localhost")
DictConfigProvider — для тестов
Встроенный провайдер на основе словаря. Идеален для мокирования настроек в unit-тестах:
import pytest
from chutils.config import register_provider, reset_providers, get_config_value
from chutils.config.custom_providers import DictConfigProvider
@pytest.fixture(autouse=True)
def mock_config():
provider = DictConfigProvider(
{
"database": {"host": "test-db", "port": "5432"},
"app": {"debug": "true", "log_level": "DEBUG"},
}
)
register_provider(provider, priority=0) # priority=0 → максимальный приоритет
yield
reset_providers() # обязательно сбрасываем после теста
def test_something():
assert get_config_value("database", "host") == "test-db"
Справочник API
register_provider(provider, priority=100)
Регистрирует провайдер в глобальном реестре.
| Параметр | Тип | Описание |
|---|---|---|
provider |
BaseConfigProvider |
Экземпляр провайдера |
priority |
int |
Приоритет (меньше → выше, по умолчанию: 100) |
reset_providers()
Очищает реестр. Вызывайте в teardown тестов.
get_config_value(section, key, fallback=None, ...)
Синхронное чтение значения. Автоматически опрашивает зарегистрированные провайдеры.
aget_config_value(section, key, fallback=None, ...)
Асинхронное чтение. Вызывает aget_value() провайдеров без блокировки event loop.
BaseConfigProvider
Абстрактный класс. Обязательные методы:
def get_value(self, section: str, key: str) -> Any | None: ...
async def aget_value(self, section: str, key: str) -> Any | None: ...
DictConfigProvider(data)
Готовый провайдер на словаре. Ключи секций и полей нечувствительны к регистру.
Советы и ограничения
- Возврат
None— сигнал «ключ не найден», поиск продолжается дальше. - Ошибки в провайдере логируются и не прерывают работу — следующий провайдер опрашивается в штатном режиме.
- Порядок при одинаковом
priority— FIFO (первый зарегистрированный побеждает). - Потокобезопасность — реестр защищён
threading.RLock. - Кэш конфигурации
chutilsне очищается автоматически при регистрации нового провайдера. Если провайдер должен перекрыть уже загруженный кэш — используйтеchutils.config.clear_cache()перед регистрацией.