"""Celery task: пересчёт district-level индексов локаций → `location` (#948 Part B, §8.2). Прогоняет refresh_locations (app/services/site_finder/locations.py): итерирует 8 админ-районов ЕКБ, для каждого считает индексы инфра/конкуренция/спрос/будущее- предложение (reuse per-district forecast-функций, #1129-кэшированы) и апсертит в `location`. ON CONFLICT (district_name) делает прогон идемпотентным. Расписание — еженедельно (district-индексы меняются медленно: рынок поглощает сток неделями, будущее предложение — слой supply-layers, который сам обновляется еженедельно по понедельникам). Регистрируется в beat_schedule.py ПОСЛЕ supply-layers (Mon 06:00) — чтобы future_supply-индекс считался по СВЕЖЕМУ складу предложения. Детерминированно, без LLM. SAVEPOINT per-row внутри refresh_locations (backend.md): сбойный район не валит остальные. Mirror conventions (cbr_macro_sync / supply_layers_refresh): SessionLocal() + try/finally close, logger (не print), задача техническая — не управляется через job_settings, добавляется в beat_schedule hardcoded-блок. """ from __future__ import annotations import logging from typing import Any from app.core.db import SessionLocal from app.services.site_finder.locations import refresh_locations from app.workers.celery_app import celery_app logger = logging.getLogger(__name__) @celery_app.task( bind=True, name="tasks.location_refresh.location_refresh", max_retries=2, ) def location_refresh(self: Any, region: str | None = None) -> dict[str, Any]: """Пересчитать + upsert-нуть district-level индексы по всем районам в `location`. Идемпотентно (ON CONFLICT по district_name). Graceful: сбойный район откатывается изолированно (SAVEPOINT), не роняя прогон. Args: region: метка региона в колонку location.region (None = ЕКБ/Свердл.). Returns: Счётчики refresh_locations: {"districts": N, "upserted": M, "failed": K}. """ db = SessionLocal() try: result = refresh_locations(db, region=region) logger.info( "location_refresh: districts=%s upserted=%s failed=%s", result["districts"], result["upserted"], result["failed"], ) return result finally: db.close()