Рецепты и полезные советы
В этом разделе собраны практические примеры решения типичных задач с помощью chutils.
1. Логирование
Асинхронное логирование (Performance)
Для высоконагруженных приложений запись логов в файл или консоль может стать «бутылочным горлышком». Включите асинхронный режим, чтобы вынести запись в фоновый поток.
from chutils import setup_logger
# Включение асинхронного режима
logger = setup_logger(use_async=True)
# Теперь основной поток не будет ждать завершения записи на диск
logger.info("Это сообщение будет обработано в фоновом потоке")
Расширенное маскирование PII
chutils может автоматически скрывать чувствительные данные (email, карты) не только по конкретным значениям, но и по
паттернам.
from chutils import setup_logger
# Настройка автоматического маскирования email и телефонов
logger = setup_logger(
use_predefined_patterns=["email", "phone"],
custom_patterns=[r"ID-\d{4}"] # Свои регулярные выражения
)
# Выведет: "Contact user [MASKED] at [MASKED]"
logger.info("Contact user ID-1234 at test@example.com")
Несколько логгеров для разных модулей
Если ваше приложение состоит из нескольких крупных компонентов, удобно разделять их логи.
from chutils import setup_logger
# Логгер для сетевого модуля (только ошибки в файл network.log)
net_logger = setup_logger("network", log_level="ERROR", log_file_name="network.log")
# Логгер для ядра (детальная отладка в консоль и core.log)
core_logger = setup_logger("core", log_level="DEVDEBUG", log_file_name="core.log")
net_logger.error("Ошибка соединения!")
core_logger.devdebug("Состояние объекта: %s", obj_data)
Настройка через конфигурационный файл
Вы можете централизованно управлять всеми логгерами в config.yml:
# config.yml
Logging:
log_level: INFO
rotation_type: time
AuditLogger:
log_level: DEBUG
log_file_name: "audit.log"
В коде:
# Использует секцию AuditLogger
audit_logger = setup_logger("audit", config_section_name="AuditLogger")
Предотвращение конфликтов конфигурации логов (pydantic-settings)
[!WARNING] Если ваше приложение одновременно использует
chutils(для логирования и чтенияconfig.yml) иpydantic-settings( для загрузки настроек, таких какLOG_LEVELиз.env/ системного окружения), у вас работают две независимые системы конфигурации.
setup_logger_from_config()читает уровень логирования из секцииLogging.log_levelвconfig.yml, тогда какpydantic-settingsсчитываетLOG_LEVELиз.env. Они никак не связаны по умолчанию. Изменение уровня логов в.envне повлияет на логгерchutils.
Для решения этого конфликта рекомендуется выбрать один из двух подходов:
Вариант А. Единый источник правды через chutils (Рекомендуется)
Полностью доверьтесь механизму конфигурации chutils (config.yml и config.local.yml). Удалите log_level из вашей
Pydantic-модели и .env файла.
Управляйте уровнем логирования в config.yml, а для локальной разработки переопределяйте его в config.local.yml (или
используйте переменные окружения CH_LOGGING_LOG_LEVEL).
В этом случае вы просто вызываете логгер без аргументов:
from chutils import setup_logger_from_config
# Настройки уровня берутся из config.yml (секция Logging)
logger = setup_logger_from_config()
Вариант Б. Явный мост (Экспорт из Pydantic)
Если вы хотите продолжать хранить LOG_LEVEL в .env и управлять им через Pydantic-модель, явно передавайте это
значение при настройке логгера:
from pydantic_settings import BaseSettings
from chutils import setup_logger
class AppSettings(BaseSettings):
log_level: str = "INFO"
class Config:
env_file = ".env"
# Инициализируем настройки приложения (читает .env и системное окружение)
settings = AppSettings()
# Явно связываем настройки Pydantic и логгер chutils
logger = setup_logger(log_level=settings.log_level)
Контекстное логирование в FastAPI / asyncio
Если вы хотите автоматически добавлять ID запроса во все логи без передачи его через аргументы функций.
from chutils import setup_logger, bind_context
import asyncio
logger = setup_logger()
async def deep_nested_function():
# Нам не нужно передавать request_id сюда, он подхватится сам!
logger.info("Лог из глубины приложения")
async def handle_request(request_id: str):
bind_context(request_id=request_id)
logger.info("Начало обработки")
await deep_nested_function()
# В асинхронном цикле контексты изолированы
asyncio.gather(
handle_request("REQ-1"),
handle_request("REQ-2")
)
2. Кэширование (Smart Caching)
Используйте декоратор @cache_with_ttl для автоматического сохранения результатов тяжелых функций. Он поддерживает как
обычные функции, так и асинхронные корутины.
Как это работает
Кэширование строго привязано к аргументам функции. При каждом вызове декоратор генерирует уникальный ключ на основе:
- Полного имени функции (включая модуль).
- Всех позиционных аргументов (
args). - Всех именованных аргументов (
kwargs).
Это значит, что вызовы с разными данными будут кэшироваться независимо:
@cache_with_ttl(ttl=60)
def calculate_price(item_id: int, discount: float = 0.0):
# Вычисления...
return final_price
# Разные аргументы = разные записи в кэше
calculate_price(1) # Вычислится и сохранится для key_1
calculate_price(2) # Вычислится и сохранится для key_2
calculate_price(1, 0.1) # Вычислится и сохранится для key_3
calculate_price(1) # Вернется из кэша (мгновенно)
Примеры использования
import asyncio
from chutils.cache import cache_with_ttl
# Кэшируем результат на 60 секунд
@cache_with_ttl(ttl=60)
def get_heavy_data(user_id: int):
print(f"Вычисляем данные для {user_id}...")
return {"id": user_id, "data": "..."}
# Асинхронный кэш с префиксом ключа
@cache_with_ttl(ttl=300, key_prefix="api_response")
async def fetch_remote_api(url: str):
await asyncio.sleep(1) # Имитация задержки
return {"url": url, "status": "ok"}
Инвалидация и тегирование кэша
Если данные во внешнем источнике изменились, вы можете принудительно сбросить закэшированные значения:
1. Точечная инвалидация по аргументам
Удаляет запись только для конкретного набора аргументов, с которыми вызывалась функция:
@cache_with_ttl(ttl=300)
def get_user_profile(user_id: int):
return {"id": user_id, "name": "Alice"}
# Сбросит кэш только для вызова get_user_profile(42)
get_user_profile.invalidate(42)
# Для асинхронных функций:
# await get_user_profile.ainvalidate(42)
2. Очистка всего кэша функции
Сбрасывает все сохраненные записи кэша, сгенерированные данной функцией:
get_user_profile.invalidate_all()
# Для асинхронных функций:
# await get_user_profile.ainvalidate_all()
3. Инвалидация по тегам (Cache Tagging)
Вы можете группировать кэш-записи с помощью тегов. Теги могут быть как статическими (список строк), так и динамическими (callable, принимающий те же аргументы, что и функция):
# Использование динамического тега
@cache_with_ttl(ttl=300, tags=lambda user_id: [f"user_{user_id}", "all_users"])
def get_user_permissions(user_id: int):
return ["read", "write"]
# Сбросит кэш разрешений только для пользователя 42
get_user_permissions.invalidate_tag("user_42")
# Сбросит кэш разрешений для всех пользователей
get_user_permissions.invalidate_tag("all_users")
# Для асинхронных функций:
# await get_user_permissions.ainvalidate_tag("all_users")
3. Работа с конфигурацией
Использование относительных путей
chutils умеет автоматически делать пути абсолютными относительно корня проекта.
# config.yml
Paths:
upload_dir: "data/uploads"
В коде:
from chutils import get_config_path
# Если корень /home/user/project, вернет /home/user/project/data/uploads
upload_path = get_config_path("Paths", "upload_dir")
Локальные переопределения
Создайте config.local.yml (и добавьте его в .gitignore), чтобы переопределить настройки для разработки:
# config.local.yml
Database:
host: "localhost" # На сервере будет production.db.com
Специфичные для окружения конфигурации
Вы можете создавать отдельные файлы настроек для разных сред развертывания (например, staging, production).
Библиотека автоматически подхватит нужный файл на основе переменной окружения CH_ENV.
- Создайте файл
config.production.yml. - Установите
CH_ENV=productionв вашей среде.
Приоритет загрузки (от высшего к низшему):
- Переменные окружения (
CH_SECTION_KEY). - Локальный файл (
config.local.yml). - Файл окружения (
config.{CH_ENV}.yml). - Основной файл (
config.yml).
4. Управление секретами
Использование в Docker / CI-CD
В изолированных средах системное хранилище (Keyring) часто недоступно. Чтобы избежать лишних предупреждений и ошибок:
- Установите переменную окружения
CH_DISABLE_KEYRING_WARNING=true. - Используйте
.envфайл для хранения секретов.
# .env
DB_PASSWORD="my-safe-password"
SecretManager автоматически подхватит это значение.
5. Hot-Reload конфигурации
Автоматическое обновление состояния приложения
Если ваше приложение должно менять свое поведение (например, уровень логирования или лимиты) без перезагрузки.
from chutils import (
setup_logger,
start_config_watcher,
on_config_change,
get_config_value
)
logger = setup_logger()
def update_app_state():
# Читаем новые значения
new_limit = get_config_value("App", "rate_limit", 100)
logger.info(f"Лимит обновлен: {new_limit}")
# Здесь можно обновить объект приложения или глобальное состояние
# app.rate_limiter.set_limit(new_limit)
# 1. Подписываемся на изменения
on_config_change(update_app_state)
# 2. Запускаем мониторинг
start_config_watcher()
Использование с Pydantic моделями
При каждом изменении файла кэш get_config() сбрасывается, поэтому вы всегда будете получать свежую провалидированную
модель.
def on_reload():
# При вызове заново будет создана новая модель с актуальными данными
cfg = get_config(model=AppConfig)
print(f"Новое имя приложения: {cfg.name}")
6. Декораторы
Отладка производительности
Используйте декоратор @log_function_details для быстрого анализа работы функций без изменения их кода.
from chutils import log_function_details, setup_logger
# Важно: установите уровень DEVDEBUG, чтобы увидеть вывод декоратора
setup_logger(log_level="DEVDEBUG")
@log_function_details
def process_heavy_task(data):
# Какая-то логика...
return True
process_heavy_task([1, 2, 3])
В логах появится время выполнения с точностью до миллисекунд и переданные аргументы.
7. Валидация через Pydantic
Строгая типизация всей конфигурации
Вы можете описать ожидаемую структуру вашего config.yml в виде Pydantic моделей для автоматической валидации при
загрузке.
from pydantic import BaseModel, Field
from chutils import get_config
class DbConfig(BaseModel):
host: str
port: int
class AppConfig(BaseModel):
name: str
version: str
db: DbConfig = Field(alias="Database")
# Валидация и автодополнение
cfg = get_config(model=AppConfig)
print(f"Подключение к {cfg.db.host}:{cfg.db.port}")
Валидация отдельной секции
Если вам нужна только часть настроек:
from chutils import get_config_section
db_cfg = get_config_section("Database", model=DbConfig)
Групповая валидация обязательных ключей
Если вам нужно проверить наличие нескольких обязательных переменных конфигурации (например, при запуске сервиса) без
использования тяжелых Pydantic моделей, используйте validate_required_keys. В случае отсутствия одного или нескольких
ключей функция сгенерирует агрегированную ошибку ConfigValidationGroupError.
from chutils.config import validate_required_keys
from chutils.exceptions import ConfigValidationGroupError
try:
validate_required_keys(
section="Secrets",
keys=["telegram_bot_token", "database_url", "api_key"]
)
except ConfigValidationGroupError as e:
# Выведет структурированный список всех недостающих настроек за один проход
print(f"Ошибка инициализации сервиса: {e}")
Интеграция с IDE (VSCode, PyCharm) через JSON Schema
Чтобы получить автодополнение и проверку типов прямо в YAML файле, вы можете сгенерировать JSON Schema для вашей Pydantic модели и подключить её.
-
Генерация схемы:
bash chutils config generate-schema --model my_app.models:Settings -o .config.schema.json -
Подключение в VSCode: Добавьте "магический комментарий" в начало вашего
config.yml: ```yaml # yaml-language-server: $schema=./.config.schema.json
Database: host: localhost port: 5432 ``` Требуется расширение "YAML" от Red Hat.
- Подключение в PyCharm:
- Перейдите в
Settings->Languages & Frameworks->Schemas and DTDs->JSON Schema Mappings. - Добавьте новую схему, укажите путь к
.config.schema.jsonи выберите ваш файлconfig.yml.
- Перейдите в
8. Утилита командной строки (CLI)
Управление секретами без кода
Используйте команду chutils, чтобы быстро настроить секреты в окружении разработки или на сервере.
# Сохранить API ключ
chutils secrets set STRIPE_KEY "sk_test_..."
# Удалить секрет
chutils secrets delete STRIPE_KEY
# Указать конкретный сервис (по умолчанию - имя текущей папки)
chutils secrets set AWS_SECRET "..." --service my-production-app
9. Работа с файловой системой
Безопасная запись данных
Используйте atomic_write, чтобы гарантировать, что файл не будет поврежден при сбое питания или внезапной остановке
процесса. Данные сначала записываются во временный файл, который затем атомарно заменяет целевой.
from chutils.fs import atomic_write
config_data = {"version": 1, "settings": {"theme": "dark"}}
# Автоматически сериализует в JSON, так как расширение .json
atomic_write("settings.json", config_data)
# Автоматически сериализует в YAML, так как расширение .yaml
atomic_write("settings.yaml", config_data)
# Запись обычного текста
atomic_write("hello.txt", "Hello world")
Временные файлы с авто-удалением
Контекстный менеджер get_temp_file создает временный файл и гарантированно удаляет его при выходе из блока with.
from chutils.fs import get_temp_file
with get_temp_file(suffix=".tmp") as temp_path:
# Делаем что-то с временным файлом
temp_path.write_text("temporary content")
print(f"Путь к файлу: {temp_path}")
# Здесь файл уже удален
10. Graceful Shutdown (Управление жизненным циклом)
Механизм корректного завершения работы приложения позволяет выполнить необходимые действия по очистке ресурсов (закрытие
соединений с БД, логов, сокетов) при получении сигналов от ОС (например, Ctrl+C).
Регистрация функций очистки
Используйте декоратор @register_cleanup для регистрации как синхронных, так и асинхронных функций.
import asyncio
from chutils import register_cleanup, setup_graceful_shutdown
@register_cleanup
async def close_db():
print("Closing database connections...")
await asyncio.sleep(0.1) # Имитация работы
print("DB connections closed.")
@register_cleanup
def cleanup_temp_files():
print("Deleting temporary files...")
# В начале работы приложения активируйте перехват сигналов
setup_graceful_shutdown()
# Пример работы асинхронного цикла
async def main():
print("App is running... Press Ctrl+C to stop.")
while True:
await asyncio.sleep(1)
if __name__ == "__main__":
try:
asyncio.run(main())
except KeyboardInterrupt:
pass
Настройка таймаута
По умолчанию на выполнение всех функций очистки дается 10 секунд. Вы можете изменить это значение в config.yml:
# config.yml
shutdown:
timeout: 5 # Таймаут в секундах
Особенности работы
- LIFO (Last-In-First-Out): Функции выполняются в обратном порядке их регистрации. Это удобно, если ресурсы зависят друг от друга (например, сначала закрыть логгер, потом БД).
- Log and Continue: Если одна из функций выбросит исключение,
chutilsзалогирует ошибку и продолжит выполнение остальных функций. - Кроссплатформенность: На Windows перехватываются
SIGINTиSIGTERM, на Linux/Unix дополнительноSIGHUP.
11. Работа со временем (Painless Datetime)
Модуль chutils.time обеспечивает "UTC-first" подход, гарантируя, что вы всегда работаете с осведомленными (timezone
aware) объектами времени.
Получение текущего времени в UTC
from chutils import utc_now
# Возвращает datetime с tzinfo=timezone.utc
now = utc_now()
print(f"Текущее время: {now}")
Умный парсинг дат
Функция parse_datetime поддерживает ISO строки, UNIX-таймстампы (в секундах и миллисекундах) и автоматически приводит
их к UTC.
from chutils import parse_datetime
# ISO 8601
dt1 = parse_datetime("2023-10-27T12:00:00")
# UNIX Timestamp (секунды)
dt2 = parse_datetime(1698400000)
# UNIX Timestamp (миллисекунды)
dt3 = parse_datetime(1698400000000)
# Если установлена библиотека chutils[date], поддерживается любой формат:
# dt4 = parse_datetime("27 Oct 2023 12:00")
Человекочитаемая разница во времени
Превращает разницу между датами в понятные строки на русском или английском языке.
from datetime import timedelta
from chutils import utc_now, humanize_timedelta
now = utc_now()
past_date = now - timedelta(minutes=5)
future_date = now + timedelta(days=1)
print(humanize_timedelta(past_date)) # "5 минут назад"
print(humanize_timedelta(future_date)) # "завтра"
print(humanize_timedelta(past_date, locale='en')) # "5 minutes ago"
12. Быстрое создание CLI (CLI Booster)
Декоратор @cli_command позволяет превратить любую функцию в полноценный CLI-инструмент за одну секунду. Он
автоматически создает парсер аргументов на основе сигнатуры функции.
Простой скрипт
# my_tool.py
from pathlib import Path
from chutils import cli_command
@cli_command
def copy_files(source: Path, dest: Path, verbose: bool = False):
"""
Утилита для копирования файлов.
Args:
source (Path): Путь к исходному файлу.
dest (Path): Путь назначения.
verbose (bool): Выводить подробную информацию.
"""
if verbose:
print(f"Копируем из {source} в {dest}")
# Логика...
if __name__ == "__main__":
copy_files()
Теперь вы можете запустить его из терминала:
python my_tool.py /tmp/src /tmp/dst --verbose
python my_tool.py --help
Асинхронные команды и списки
CLI Booster отлично справляется с асинхронностью и списками аргументов.
import asyncio
from chutils import cli_command
@cli_command
async def process_batch(ids: list[int], retry: int = 3):
"""Обработка списка ID с повторами."""
for item_id in ids:
print(f"Processing {item_id} (retries: {retry})")
await asyncio.sleep(0.1)
if __name__ == "__main__":
process_batch()
13. Отладка и диагностика конфигурации (Config Diagnostics)
Если вы не понимаете, почему значение ключа в приложении отличается от того, что написано в config.yml, используйте
интерактивный отладчик.
Использование через CLI
Команда config debug покажет всю историю изменений для каждого ключа: откуда он был загружен изначально и чем перекрыт
позже.
# Показать дерево конфигурации (по умолчанию)
chutils config debug
# Вывод в виде таблицы
chutils config debug --format table
# Показать секретные значения (по умолчанию они маскируются)
chutils config debug --show-secrets
# Экспорт в JSON для анализа
chutils config debug --format json > config_trace.json
Использование через API
Вы можете включить трассировку и получить данные программно:
from chutils.config.manager import _cm
from chutils.config import get_config
from chutils.config.diagnostics import format_trace
# 1. Включаем сбор метаданных
_cm.tracing_enabled = True
# 2. Сбрасываем кэш и загружаем конфиг
_cm.clear_cache()
get_config()
# 3. Получаем и форматируем отчет
trace = _cm.get_trace()
print(format_trace(trace, format_type='tree'))
14. Распределенное трассирование (OpenTelemetry)
chutils предоставляет легковесную интеграцию с OpenTelemetry для отслеживания пути выполнения запросов и связи логов с
трассами.
Установка
Функционал трассировки является опциональным:
pip install chutils[otel]
Быстрый старт
- Настройте сбор трасс в начале вашего приложения:
from chutils import setup_tracing
# Настройка вывода трасс в консоль (для локальной разработки)
setup_tracing(service_name="my_service", exporter_type="console")
- Используйте декоратор
@traceдля функций, которые хотите отслеживать:
from chutils import trace, setup_logger
logger = setup_logger()
@trace(capture_kwargs=True)
def process_order(order_id: int):
logger.info("Начинаем обработку заказа")
# ... логика ...
return True
Особенности
- Связь с логами: В текстовых логах автоматически появятся
[trace_id=... span_id=...]. В JSON логах эти поля будут вынесены на верхний уровень. - Async: Декоратор
@traceполностью поддерживает асинхронные функции. - OTLP: Для промышленного использования (Jaeger, Grafana Tempo) используйте
exporter_type="otlp". - Zero Overhead: Если пакеты
opentelemetryне установлены, декоратор@traceне создает никаких накладных расходов.
15. Дистанционная конфигурация (Remote Config)
chutils позволяет загружать настройки из удаленных HTTP/HTTPS источников. Это полезно для централизованного управления
конфигурациями в микросервисной архитектуре.
Быстрый старт
Просто укажите URL при получении конфигурации:
from chutils import get_config
# Загрузка и объединение с локальными файлами
config = get_config(remote_url="https://api.example.com/config.json")
Периодический опрос (Polling)
Вы можете настроить автоматическое фоновое обновление конфигурации:
# Опрос каждые 60 секунд
config = get_config(
remote_url="https://api.example.com/config.json",
polling_interval=60
)
Динамический интервал
Вы можете управлять интервалом опроса прямо из удаленного конфига. Если в загруженных данных есть секция
RemoteConfig (или polling) с ключом interval, chutils автоматически переключится на этот интервал.
{
"RemoteConfig": {
"interval": 300
},
"Database": {
"host": "remote-db"
}
}
Авторизация и безопасность
Для доступа к защищенным эндпоинтам используйте remote_auth:
config = get_config(
remote_url="https://secure-config.local/app.yml",
remote_auth=("admin", "secret-token")
)
Отказоустойчивость (Fallback)
Если удаленный сервер временно недоступен, chutils автоматически вернет последнюю успешно загруженную версию из
памяти (кэша), чтобы приложение продолжало работать.
16. Инструменты разработчика и AI-контекст
Если вы хотите быстро создать документацию по API вашего проекта или подготовить глубокий индекс для AI-агента.
Генерация карты API
Создает Markdown-файл со списком всех публичных функций, классов и их описаний.
chutils dev generate-context -o api_map.md
Генерация семантического индекса для AI
Генерирует JSON-дерево проекта (через AST), которое включает связи между модулями, веса зависимостей и метаданные символов. Это "золотой стандарт" контекста для современных LLM.
chutils dev generate-context --tree -o project_index.json
17. Внутренняя шина событий (In-Memory Event Bus)
Шина событий (chutils.events) позволяет развязать компоненты вашего приложения (loose coupling) через паттерн Pub/Sub.
Подписка и публикация
Используйте декоратор @subscribe для регистрации обработчиков и publish (или publish_async) для отправки событий.
from chutils.events import subscribe, publish, publish_async
import asyncio
# 1. Синхронный обработчик
@subscribe("user_created")
def send_email(user_id: int, username: str):
print(f"Отправка письма для {username}...")
# 2. Асинхронный обработчик
@subscribe("user_created")
async def setup_profile(user_id: int, username: str):
await asyncio.sleep(0.1)
print(f"Профиль {username} настроен.")
# Публикация событий:
# Синхронная (запускает асинхронных подписчиков в фоновом режиме):
publish("user_created", user_id=1, username="Дмитрий")
# Асинхронная (ждет завершения всех подписчиков):
# await publish_async("user_created", user_id=2, username="Анна")
Стратегии обработки ошибок
При возникновении исключений в обработчиках вы можете выбрать одну из трех стратегий:
IGNORE(по умолчанию) — логирует ошибки черезchutils.loggerи продолжает работу остальных обработчиков.FAIL_FAST— немедленно прерывает выполнение и пробрасывает ошибку.COLLECT— выполняет все задачи, собирает ошибки и выбрасывает объединенное исключениеEventBusExceptionGroup.
from chutils.events import EventBus, ErrorStrategy
from chutils.exceptions import EventBusExceptionGroup
bus = EventBus(error_strategy=ErrorStrategy.COLLECT)
@bus.subscribe("data_sync")
def step_1():
raise ValueError("Сбой шага 1")
@bus.subscribe("data_sync")
def step_2():
raise IOError("Сбой шага 2")
try:
bus.publish("data_sync")
except EventBusExceptionGroup as e:
# Классический перехват (совместим со всеми версиями Python >= 3.10)
print(f"Возникли ошибки: {e.exceptions}")
# На Python >= 3.11 вы также можете использовать стандартный синтаксис except*:
# try:
# bus.publish("data_sync")
# except* ValueError as eg:
# print(f"Обработка ValueError из группы: {eg.exceptions}")
# except* IOError as eg:
# print(f"Обработка IOError из группы: {eg.exceptions}")
Передача Pydantic-моделей
Если установлен пакет pydantic (chutils[pydantic]), шина поддерживает прямую передачу экземпляров Pydantic-моделей в
качестве payload:
from pydantic import BaseModel
from chutils.events import subscribe, publish
class UserEvent(BaseModel):
user_id: int
role: str
@subscribe("user_updated")
def handle_user_update(event: UserEvent):
print(f"Роль пользователя {event.user_id} обновлена на {event.role}")
# Публикация Pydantic-модели
publish("user_updated", UserEvent(user_id=10, role="admin"))
18. Планировщик фоновых задач (Lightweight Task Scheduler)
Модуль chutils.tasks предоставляет планировщик для выполнения периодических фоновых задач. Он поддерживает как
синхронные, так и асинхронные обработчики, отслеживает перекрытия (overlapping), предоставляет стратегии обработки
ошибок и автоматически интегрируется с Graceful Shutdown (chutils.lifecycle).
Простой пример
Используйте декоратор @periodic_task для регистрации задач и функцию start_scheduler() для их запуска в Event Loop.
import asyncio
import time
from chutils import periodic_task, start_scheduler, setup_graceful_shutdown
from chutils.tasks import ErrorStrategy
# 1. Асинхронная задача запускается сразу при старте планировщика
@periodic_task(interval_seconds=5, run_immediately=True, name="async_logger")
async def log_status():
print("Статус приложения в норме... (асинхронно)")
# 2. Синхронная задача выполняется в пуле потоков каждые 10 секунд
@periodic_task(interval_seconds=10, run_immediately=False, name="sync_cleaner")
def cleanup_temp_files():
print("Очистка временных файлов... (синхронно)")
time.sleep(1.0)
async def main():
# Настраиваем graceful shutdown для остановки планировщика при Ctrl+C
setup_graceful_shutdown()
# Запускаем планировщик фоновых задач
start_scheduler()
# Имитируем работу приложения
await asyncio.sleep(30.0)
if __name__ == "__main__":
asyncio.run(main())
Контроль перекрытия задач (Overlapping)
- По умолчанию (
overlap=False): Если предыдущий запуск задачи еще не завершился к моменту наступления следующего интервала, запуск пропускается (откладывается до следующего тика). - Параллельный запуск (
overlap=True): Задача запускается строго по интервалу, независимо от того, завершились ли предыдущие запуски.
# Эта задача будет запускаться каждые 2 секунды параллельно, не дожидаясь окончания
@periodic_task(interval_seconds=2, overlap=True)
async def parallel_task():
await asyncio.sleep(5.0)
Стратегии обработки ошибок (ErrorStrategy)
В случае сбоя задачи вы можете выбрать одну из трех стратегий:
IGNORE(по умолчанию) — залогировать ошибку вchutils.loggerи продолжить выполнение по расписанию.STOP_TASK— исключить сбойную задачу из расписания планировщика.STOP_SCHEDULER— остановить весь планировщик.
@periodic_task(interval_seconds=5, error_strategy=ErrorStrategy.STOP_TASK)
def fragile_task():
raise RuntimeError("Неустранимая ошибка в задаче")
Динамические интервалы (Dynamic Intervals)
Интервал запуска interval_seconds может быть динамическим:
- Callable-функция: Передайте функцию, возвращающую
int. Планировщик будет вычислять интервал заново перед каждым циклом ожидания. - Конфигурация
chutils.config: Передайте строку видаsection.key(или простоkeyдля поиска в секцииdefault). Значение будет считываться из конфигурации на лету перед каждым циклом ожидания.
Важные особенности:
- Горячая перезагрузка (Hot-Reload) "на лету": Планировщик повторно вычисляет интервал (через вызов функции или
чтение конфигурации) перед каждым циклом ожидания (
asyncio.sleep). Это позволяет менять расписание фоновых задач в реальном времени без необходимости перезапуска самого планировщика или всего приложения. - Отказоустойчивость: Если при считывании интервала возникла ошибка (например, передан неверный тип данных вроде
строки вместо числа, ключ отсутствует в конфигурации, или функция выбросила исключение), планировщик логирует
предупреждение (
logger.warning/logger.error), автоматически переключается на безопасный резервный интервал в 1 секунду и продолжает работу. Вся служба планировщика при этом не падает.
# А. Динамический интервал через функцию
current_interval = 5
def get_interval():
return current_interval # Может меняться динамически во время работы
@periodic_task(interval_seconds=get_interval)
async def dynamic_job():
print("Выполнение с динамическим интервалом...")
# Б. Динамический интервал из chutils.config (ключ 'scheduler.cleanup_delay')
@periodic_task(interval_seconds="scheduler.cleanup_delay")
async def config_bound_job():
print("Интервал берется и динамически обновляется из конфигурации...")
19. Ограничение частоты вызовов (Rate Limiting / Throttling)
Декоратор @rate_limit из модуля chutils.decorators позволяет ограничить частоту выполнения синхронных и асинхронных
функций, обеспечивая защиту от перегрузки и соблюдение лимитов внешних API.
Базовое использование (Fail Fast)
По умолчанию декоратор работает в режиме wait=False (Fail Fast) с использованием алгоритма Token Bucket. При
превышении лимита мгновенно выбрасывается исключение RateLimitExceededError.
from chutils import rate_limit, RateLimitExceededError
# Разрешено максимум 3 вызова за 5 секунд
@rate_limit(max_calls=3, period=5.0)
def call_api():
print("API вызван успешно")
for _ in range(3):
call_api()
try:
call_api() # Четвертый вызов упадет
except RateLimitExceededError:
print("Превышен лимит запросов!")
Сглаживание и ожидание (Wait)
Если передан флаг wait=True, выполнение функции блокируется на время, необходимое для восстановления лимита (для
синхронных функций используется time.sleep, для асинхронных — asyncio.sleep).
import asyncio
from chutils import rate_limit
# Разрешено максимум 2 вызова в секунду. Лишние вызовы будут ожидать очереди
@rate_limit(max_calls=2, period=1.0, wait=True)
async def process_item(item_id: int):
print(f"Обработка элемента {item_id}")
async def main():
# Запустим 5 задач параллельно. Они будут выполняться пачками по 2 штуки в секунду
tasks = [process_item(i) for i in range(5)]
await asyncio.gather(*tasks)
Раздельные лимиты по ключам (key_func)
По умолчанию лимит действует глобально на всю функцию. Вы можете настроить индивидуальные лимиты (например, на каждого
пользователя или IP) с помощью функции key_func.
from chutils import rate_limit
# Лимит: 1 вызов в секунду на каждого конкретного пользователя
@rate_limit(
max_calls=1,
period=1.0,
key_func=lambda user_id, *args, **kwargs: f"user_{user_id}"
)
def send_notification(user_id: int, message: str):
print(f"Уведомление отправлено пользователю {user_id}")
send_notification(1, "Привет!") # OK
send_notification(2, "Привет!") # OK для другого пользователя
# send_notification(1, "Еще раз привет") # Упадет, превышен лимит для user_1
Алгоритмы лимитирования (strategy)
Поддерживаются две стратегии:
token_bucket(по умолчанию) — алгоритм маркерной корзины. Разрешает кратковременные всплески нагрузки (доmax_callsодновременно), после чего скорость ограничивается.leaky_bucket— алгоритм дырявого ведра. Обеспечивает строгое сглаживание нагрузки с равномерной задержкой между вызовами.
# Строгий Leaky Bucket: вызовы будут распределены равномерно
@rate_limit(max_calls=60, period=60.0, strategy="leaky_bucket", wait=True)
def send_email():
pass
20. Внедрение зависимостей (Dependency Injection)
Встроенный IoC/DI контейнер (chutils.di) позволяет связать независимые компоненты приложения без ручной передачи
аргументов через всю цепочку вызовов (prop drilling) и без привязки к жестким глобальным зависимостям.
Декларативная регистрация (@provide)
Используйте декоратор @provide для регистрации классов или функций-фабрик. По умолчанию используется время жизни
singleton (объект создается один раз при первом обращении и кэшируется).
from chutils import provide
# 1. Регистрация класса (singleton)
@provide()
class DatabaseConnection:
def __init__(self) -> None:
self.connected = True
print("Подключение к БД создано")
# 2. Регистрация функции-фабрики (transient - новый объект при каждом запросе)
@provide(scope="transient")
def get_current_time() -> float:
import time
return time.time()
Автоматическое внедрение (@inject)
Декоратор @inject автоматически подставляет зарегистрированные зависимости из контейнера в аргументы функции. Для
этого аргумент должен иметь тип зарегистрированной зависимости и значение по умолчанию Inject().
from chutils import inject, Inject
# Автоматически внедряем DatabaseConnection
@inject()
def get_user_profile(user_id: int, db: DatabaseConnection = Inject()):
if db.connected:
return f"User profile {user_id}"
Рекурсивный резолв зависимостей
Если одна зависимость требует другую зависимость в своем конструкторе __init__, контейнер автоматически разрешит весь
граф при обращении (Auto-wiring).
from chutils import provide, inject, Inject
@provide()
class CacheService:
pass
@provide()
class UserService:
# Контейнер автоматически создаст CacheService и передаст его сюда
def __init__(self, cache: CacheService) -> None:
self.cache = cache
@inject()
def process(user_service: UserService = Inject()):
# user_service уже имеет внутри инициализированный cache
pass
Тестирование и переопределение зависимостей
Для написания unit-тестов вы можете легко переопределить любую зависимость в контейнере (mocking) напрямую через метод
register() глобального контейнера:
from chutils import container
class MockDatabaseConnection:
def __init__(self) -> None:
self.connected = True
print("Mock подключение к БД создано!")
def test_my_service():
# Переопределяем реальное подключение на мок
container.register(DatabaseConnection, provider=MockDatabaseConnection)
# Теперь все вызовы @inject внедрят MockDatabaseConnection вместо реального
profile = get_user_profile(42)
assert "User profile 42" in profile
21. Сбор метрик (Unified Metrics)
Модуль chutils.metrics предоставляет унифицированный API для сбора многомерных метрик (Counters, Gauges, Histograms) с
полной поддержкой меток (labels).
Быстрый старт (Counters & Gauges)
По умолчанию метрики сохраняются во встроенное in-memory хранилище. При установке библиотеки prometheus-client они
автоматически транслируются в формат Prometheus.
from chutils.metrics import increment, set_gauge, observe
# 1. Увеличить счетчик (Counter)
increment("http_requests_total", 1.0, {"method": "GET", "endpoint": "/users"})
# 2. Установить значение датчика (Gauge)
set_gauge("cpu_usage_percent", 45.2, {"node": "worker-1"})
# 3. Записать значение в гистограмму (Histogram)
observe("request_size_bytes", 1024.0, {"client": "ios"})
Автоматические метрики Circuit Breaker (Предохранителя)
Декоратор @circuit_breaker автоматически экспортирует метрику состояния цепи типа Gauge под именем
circuit_breaker_state с меткой name (имя декорируемой функции).
Метрика состояния кодируется числовыми значениями:
0.0—CLOSED(цепь замкнута, всё работает нормально).1.0—OPEN(цепь разомкнута из-за сбоев, вызовы заблокированы).2.0—HALF_OPEN(период восстановления, пробные вызовы).
Это позволяет визуализировать аварийные режимы и работоспособность интеграций в реальном времени (например, на дашборде Grafana).
Автоматический замер времени (@timer)
Используйте @timer в качестве декоратора (для синхронных и асинхронных функций) или в качестве контекстного менеджера.
from chutils.metrics import timer
import asyncio
# A. Декоратор (асинхронный)
@timer("http_request_duration_seconds", labels={"handler": "get_users"})
async def handle_request():
await asyncio.sleep(0.05)
# B. Контекстный менеджер
def run_db_query():
with timer("db_query_duration_seconds", labels={"query": "select_orders"}):
# тяжелая операция
pass
Экспорт метрик в FastAPI / Flask
Чтобы отдавать собранные метрики в систему мониторинга Prometheus, просто добавьте эндпоинт /metrics, вызывающий
generate_latest():
from fastapi import FastAPI, Response
from chutils.metrics import generate_latest
app = FastAPI()
@app.get("/metrics")
def metrics_endpoint():
# generate_latest возвращает валидный Prometheus-текст
return Response(content=generate_latest(), media_type="text/plain")
Безопасность при отсутствии prometheus-client (Graceful Fallback)
Модуль chutils.metrics спроектирован так, что отсутствие внешней библиотеки prometheus-client не вызывает ошибок. В
этом случае все метрики собираются во внутренний in-memory пул и форматируются функцией generate_latest() в полностью
Prometheus-совместимый текстовый формат.
Таким образом, ваше приложение гарантированно продолжит работу без внешних зависимостей.
22. Универсальная валидация данных (chutils.validation)
Модуль chutils.validation предоставляет удобный инструмент для валидации структурированных данных (JSON, dict) с
использованием Pydantic, а также автоматическую валидацию аргументов функций с генерацией детальных, структурированных
ошибок ChutilsValidationError.
Валидация словарей и JSON-строк (validate_data)
Функция validate_data принимает Pydantic модель и данные (словарь или JSON-строку), возвращая валидный объект модели.
В случае ошибки выбрасывается ChutilsValidationError.
from pydantic import BaseModel, Field
from chutils import validate_data
from chutils.exceptions import ChutilsValidationError
class User(BaseModel):
name: str
age: int = Field(gt=0)
try:
# 1. Валидация словаря
user1 = validate_data(User, {"name": "Alice", "age": 30})
# 2. Валидация JSON-строки
user2 = validate_data(User, '{"name": "Bob", "age": 25}')
except ChutilsValidationError as e:
print(f"Ошибка: {e}")
Автоматическая валидация аргументов функций (@validate_call)
Декоратор @validate_call автоматически проверяет типы и значения аргументов при вызове функции. Поддерживает как
синхронные, так и асинхронные функции.
from chutils import validate_call
from chutils.exceptions import ChutilsValidationError
@validate_call
def send_message(user_id: int, message: str) -> None:
print(f"Отправка пользователю {user_id}: {message}")
try:
# Вызовет ошибку валидации, так как передан неверный тип
send_message("not_an_int", "Привет")
except ChutilsValidationError as e:
print(f"Неверные аргументы вызова:\n{e}")
Красивый вывод ошибок через Rich
Исключение ChutilsValidationError бесшовно интегрировано с библиотекой rich. Если rich установлен, при печати
исключения в консоль автоматически выводится красивая структурированная таблица ошибок с указанием пути к полю, причины
ошибки и полученного невалидного значения.
from chutils import get_console
from chutils.exceptions import ChutilsValidationError
console = get_console()
try:
validate_data(User, {"name": "Alice", "age": -5})
except ChutilsValidationError as e:
# Автоматически отрендерит красивую таблицу ошибок в консоль
console.print(e)
23. Мониторинг работоспособности и Diagnostics API (chutils.diagnostics)
Модуль chutils.diagnostics предназначен для проверки жизнедеятельности (health check) компонентов системы с контролем
таймаутов, встроенными проверками и готовыми хелперами для веб-фреймворков.
Использование DiagnosticsManager
Вы можете зарегистрировать кастомные функции проверок (как синхронные, так и асинхронные) с указанием таймаутов выполнения.
import asyncio
from chutils.diagnostics import DiagnosticsManager
manager = DiagnosticsManager()
# Регистрация асинхронной проверки с таймаутом 2 секунды
@manager.register(name="database", critical=True, timeout=2.0)
async def check_db() -> str:
await asyncio.sleep(0.1) # Симуляция пинга БД
return "Соединение с БД успешно установлено."
# Запуск проверок асинхронно
async def main():
report = await manager.run_checks()
print(f"Статус системы: {report.status}") # HEALTHY, DEGRADED, UNHEALTHY
for name, result in report.results.items():
print(f"[{name}] {result.status} - {result.message}")
asyncio.run(main())
Встроенные проверки
Модуль предоставляет встроенные проверки из коробки:
check_keyring— проверяет доступность системного хранилища секретов (keyring) путём пробной записи и удаления тестового секрета.check_config— проверяет валидность и физическое наличие конфигурационного файла на диске.
Встроенные проверки зарегистрированы в DiagnosticsManager по умолчанию.
Интеграция с FastAPI и Flask
Модуль содержит готовые хелперы для быстрой отдачи статуса здоровья системы по HTTP-эндпоинтам.
FastAPI:
from fastapi import FastAPI
from chutils.diagnostics import DiagnosticsManager
from chutils.diagnostics.web import get_fastapi_health_handler
app = FastAPI()
manager = DiagnosticsManager()
# Регистрируем роут /health
app.add_api_route("/health", get_fastapi_health_handler(manager), methods=["GET"])
При возникновении критических сбоев (UNHEALTHY) обработчик автоматически возвращает HTTP-код
503 Service Unavailable, а при успешной или частично деградировавшей работе (HEALTHY, DEGRADED) — 200 OK.
Проверка через CLI
Вы можете запустить диагностику системы напрямую из терминала:
# Красивая таблица результатов в rich
chutils dev diagnostics
# Вывод в формате структурированного JSON для систем мониторинга
chutils dev diagnostics --json
24. Декларативный манифест переменных окружения (chutils.env)
Модуль chutils.env предоставляет механизм для декларативного описания, загрузки и валидации переменных окружения с
использованием Pydantic моделей, поддержкой автоматического маскирования секретов и интеграцией с SecretManager.
Создание манифеста
Опишите ожидаемые переменные окружения как поля класса, наследующегося от BaseEnvManifest:
from pydantic import Field
from chutils.env import BaseEnvManifest
class AppEnv(BaseEnvManifest):
DATABASE_URL: str = Field(description="URL подключения к базе данных")
API_KEY: str = Field(json_schema_extra={"secret": True}, description="Секретный API-ключ")
PORT: int = Field(default=8080, description="Порт веб-сервера")
Загрузка и валидация в коде
Метод load() автоматически считывает значения из os.environ и выполняет приведение типов:
import os
from chutils.exceptions import EnvValidationError
# Устанавливаем переменные для теста
os.environ["DATABASE_URL"] = "postgresql://localhost:5432/db"
os.environ["API_KEY"] = "super-secret-token"
try:
env = AppEnv.load()
print(f"Server will run on port {env.PORT}")
except EnvValidationError as e:
# Все секретные поля (API_KEY) будут автоматически замаскированы (показано как ***)
print(f"Ошибка валидации окружения: {e}")
Если переменная отсутствует в os.environ, но помечена как secret=True, метод load() автоматически попытается
получить её значение через SecretManager (если он доступен).
Валидация окружения через CLI
Вы можете автоматически валидировать переменные окружения при развертывании приложения (например, в Docker/K8s
init-контейнерах) с помощью CLI-команды chutils env validate.
- Явный запуск с указанием манифеста:
chutils env validate --manifest myapp.env:AppEnv
- Запуск через конфигурацию проекта (
pyproject.toml):
Добавьте секцию в ваш pyproject.toml:
[tool.chutils.env]
manifest = "myapp.env:AppEnv"
После этого вы можете запускать проверку без параметров:
chutils env validate
При успешной валидации команда вернет код 0, при сбое — выведет подробную таблицу ошибок и вернет код 1.