Some checks failed
CI / changes (push) Successful in 6s
CI / frontend-tests (push) Has been skipped
CI / changes (pull_request) Successful in 6s
CI / frontend-tests (pull_request) Has been skipped
CI / backend-tests (push) Has been cancelled
CI / backend-tests (pull_request) Successful in 6m23s
Promote district to a first-class `location` entity (ТЗ §8.2), ADDITIVE — no district->FK refactor. New `location` table keyed by district_name (joinable by string), carrying 4 normalized [0,1] indices (NULL when no data, never 0): - infra: _district_poi_score / _city_avg_poi_score (per-district POI aggregate) - competition: market_metrics.overstock_index (available/stuck competing supply — orthogonal to demand; NOT sell-through, which is market-heat correlated w/ demand) - demand: market_metrics.unit_velocity (saturating /50) - future_supply: future_supply_pressure.index (passthrough, already 0..1) - data/sql/146_location.sql: idempotent table + UNIQUE(district_name) + range CHECK + centroid GIST - services/site_finder/locations.py: compute_location_indices (reuses forecast per-district fns) + refresh_locations (SAVEPOINT per-row, CAST, ON CONFLICT) - workers/tasks/location_refresh.py + beat (Mon 07:00 MSK, after supply-layers) - api/v1/locations.py: read-only GET list + GET by name (analyst+admin via rbac, frontend pilot-gated) - tests: 34 (normalization 0..1/null, competition⊥demand orthogonality, idempotent upsert, read API list/by-name/404) Part of #948 (Part A insight shipped #1164).
61 lines
2.8 KiB
Python
61 lines
2.8 KiB
Python
"""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()
|