"""Регулярные refresh-задачи для аналитического слоя. Сейчас здесь: - refresh_ekb_districts_median: пересчёт median/mean ₽/м² 8 районов ЕКБ из ДДУ-сделок rosreestr_deals (окно 24 мес). Запускается через Celery beat (ежемесячно). См. workers/tasks/refresh_analytics.py. """ from __future__ import annotations import logging from typing import Any from sqlalchemy import text from sqlalchemy.orm import Session logger = logging.getLogger(__name__) def refresh_ekb_districts_median( db: Session, *, window_months: int = 24, min_deals: int = 50, ) -> list[dict[str, Any]]: """Пересчитать ekb_districts.median_price_per_m2 + mean_price_per_m2 из rosreestr ДДУ-сделок последних `window_months` месяцев. Использует SQL-функцию `refresh_ekb_districts_median(int, int)` из миграции 67_refresh_ekb_districts_median.sql. Возвращает список обновлённых районов с фактически использованными метриками. Не обновляет района с deals < min_deals (статистически шумно). Не трогает 'не определён' (служебная категория). """ rows = ( db.execute( text( """ SELECT district_name, deals_used, median_pm, mean_pm FROM refresh_ekb_districts_median(:wm, :md) """ ), {"wm": window_months, "md": min_deals}, ) .mappings() .all() ) db.commit() out = [ { "district_name": r["district_name"], "deals_used": int(r["deals_used"]), "median_pm": float(r["median_pm"]) if r["median_pm"] is not None else None, "mean_pm": float(r["mean_pm"]) if r["mean_pm"] is not None else None, } for r in rows ] logger.info( "refresh_ekb_districts_median: window=%dmo min_deals=%d updated=%d districts", window_months, min_deals, len(out), ) return out