"""Read/update Objective sync config (single-row БД-конфигурация). Используется: - tasks.scrape_objective.sync_all_groups: читает groups/use_ddu/use_dkp/ period_months_back/inter_group_delay_s/rate_ms/retries при каждом запуске - workers.celery_app: читает cron_schedule при старте beat (один раз) - api.v1.admin_scrape: GET/PUT /admin/scrape/objective/config Если таблица objective_sync_config недоступна (БД не поднята / таблицы нет / ошибка) — все методы возвращают defaults на основе settings.objective_*. Это обеспечивает безопасный fallback для cold-start и unit-тестов. """ from __future__ import annotations import logging from dataclasses import dataclass from typing import Any from sqlalchemy import text from app.core.config import settings logger = logging.getLogger(__name__) @dataclass class ObjectiveSyncConfig: cron_schedule: str groups_csv: str use_ddu: bool use_dkp: bool period_months_back: int inter_group_delay_s: int rate_ms: int retries: int updated_at: str | None = None updated_by: str | None = None @property def groups(self) -> list[str]: return [g.strip() for g in self.groups_csv.split(",") if g.strip()] def to_dict(self) -> dict[str, Any]: return { "cron_schedule": self.cron_schedule, "groups_csv": self.groups_csv, "groups": self.groups, "use_ddu": self.use_ddu, "use_dkp": self.use_dkp, "period_months_back": self.period_months_back, "inter_group_delay_s": self.inter_group_delay_s, "rate_ms": self.rate_ms, "retries": self.retries, "updated_at": self.updated_at, "updated_by": self.updated_by, } def _defaults_from_settings() -> ObjectiveSyncConfig: """Fallback когда БД-таблицы нет / ошибка.""" return ObjectiveSyncConfig( cron_schedule=settings.objective_sync_cron, groups_csv=settings.objective_sync_groups, use_ddu=True, use_dkp=True, period_months_back=1, inter_group_delay_s=30, rate_ms=3000, retries=2, ) def get_config(db) -> ObjectiveSyncConfig: """Прочитать config (single row id=1). Fallback на settings при ошибке.""" try: r = ( db.execute( text( """ SELECT cron_schedule, groups_csv, use_ddu, use_dkp, period_months_back, inter_group_delay_s, rate_ms, retries, updated_at, updated_by FROM objective_sync_config WHERE id = 1 """ ) ) .mappings() .first() ) except Exception as e: logger.warning("get_config: fallback на settings (БД недоступна?): %s", e) return _defaults_from_settings() if not r: # Таблица есть но пустая — посеем default и вернём try: db.execute( text("INSERT INTO objective_sync_config (id) VALUES (1) ON CONFLICT DO NOTHING") ) db.commit() except Exception: pass return _defaults_from_settings() return ObjectiveSyncConfig( cron_schedule=r["cron_schedule"], groups_csv=r["groups_csv"], use_ddu=bool(r["use_ddu"]), use_dkp=bool(r["use_dkp"]), period_months_back=int(r["period_months_back"]), inter_group_delay_s=int(r["inter_group_delay_s"]), rate_ms=int(r["rate_ms"]), retries=int(r["retries"]), updated_at=r["updated_at"].isoformat() if r["updated_at"] else None, updated_by=r["updated_by"], ) def update_config( db, *, cron_schedule: str | None = None, groups_csv: str | None = None, use_ddu: bool | None = None, use_dkp: bool | None = None, period_months_back: int | None = None, inter_group_delay_s: int | None = None, rate_ms: int | None = None, retries: int | None = None, updated_by: str | None = None, ) -> ObjectiveSyncConfig: """PATCH-style update — обновляет только переданные поля.""" sets: list[str] = ["updated_at = NOW()"] params: dict[str, Any] = {} for field, value in [ ("cron_schedule", cron_schedule), ("groups_csv", groups_csv), ("use_ddu", use_ddu), ("use_dkp", use_dkp), ("period_months_back", period_months_back), ("inter_group_delay_s", inter_group_delay_s), ("rate_ms", rate_ms), ("retries", retries), ("updated_by", updated_by), ]: if value is not None: sets.append(f"{field} = :{field}") params[field] = value if len(sets) == 1: # Только updated_at — нечего обновлять return get_config(db) # Гарантия что строка существует db.execute(text("INSERT INTO objective_sync_config (id) VALUES (1) ON CONFLICT DO NOTHING")) db.execute( text(f"UPDATE objective_sync_config SET {', '.join(sets)} WHERE id = 1"), params, ) db.commit() return get_config(db) def get_cron_schedule_safe(db_factory) -> str: """Безопасное чтение cron при старте Celery beat — открывает свою сессию. db_factory: callable () -> Session (обычно SessionLocal) """ try: db = db_factory() try: cfg = get_config(db) return cfg.cron_schedule finally: db.close() except Exception as e: logger.warning("get_cron_schedule_safe: fallback на settings: %s", e) return settings.objective_sync_cron