"""Шаблоны сообщений из БД с TTL-кешем (Блок 4.5). Бот читает тексты из ``bot.message_templates``: правка в админке применяется без перезапуска — в течение ``TEMPLATE_CACHE_TTL`` секунд. Если БД недоступна (или таблицы ещё нет), используются тексты по умолчанию из ``common.texts.TEMPLATES``. """ from __future__ import annotations import asyncio import logging import time from collections.abc import Awaitable, Callable from common.config import get_settings from common.texts import TEMPLATES logger = logging.getLogger(__name__) #: Загрузчик «ключ -> текст» (по умолчанию — из Postgres). Loader = Callable[[], Awaitable[dict[str, str]]] async def _load_from_db() -> dict[str, str]: # Импорт внутри функции: не тянем слой БД на этапе импорта текстов. from common.repositories.templates_repo import load_texts return await load_texts() class TemplateStore: """Кеш шаблонов с временем жизни; при ошибке БД — тексты по умолчанию.""" def __init__(self, loader: Loader | None = None, ttl: float | None = None) -> None: self._loader = loader or _load_from_db self._ttl = ttl self._texts: dict[str, str] = {} self._loaded_at: float | None = None self._lock = asyncio.Lock() @property def ttl(self) -> float: """TTL кеша в секундах (из настроек, если не задан явно).""" if self._ttl is None: self._ttl = get_settings().template_cache_ttl return max(self._ttl, 0.0) async def get(self, key: str) -> str: """Вернуть текст шаблона (из БД или из кода, если его там нет).""" await self._ensure_fresh() text = self._texts.get(key) if text is None: return TEMPLATES.get(key, "") return text def clear(self) -> None: """Сбросить кеш: следующий ``get`` перечитает БД.""" self._loaded_at = None async def _ensure_fresh(self) -> None: if self._loaded_at is not None and time.monotonic() - self._loaded_at < self.ttl: return async with self._lock: # Пока ждали лок, кеш мог обновить другой вызов. if self._loaded_at is not None and time.monotonic() - self._loaded_at < self.ttl: return try: texts = await self._loader() except Exception: logger.warning( "Не удалось прочитать шаблоны из БД — использую тексты по умолчанию", exc_info=True, ) # Не долбим БД каждым сообщением: следующая попытка через TTL. self._loaded_at = time.monotonic() return self._texts = texts self._loaded_at = time.monotonic() logger.debug("Шаблоны обновлены из БД: %s", len(texts)) _store: TemplateStore | None = None def get_template_store() -> TemplateStore: """Вернуть общий кеш шаблонов процесса.""" global _store if _store is None: _store = TemplateStore() return _store async def get_text(key: str) -> str: """Короткий доступ к шаблону: ``await get_text("info.help")``.""" return await get_template_store().get(key)