All checks were successful
CI / changes (push) Successful in 6s
CI / frontend-tests (push) Has been skipped
CI / changes (pull_request) Successful in 6s
Deploy / changes (push) Successful in 6s
CI / frontend-tests (pull_request) Has been skipped
Deploy / build-backend (push) Successful in 1m29s
Deploy / build-frontend (push) Has been skipped
Deploy / build-worker (push) Successful in 2m42s
Deploy / deploy (push) Successful in 1m11s
CI / backend-tests (push) Successful in 6m27s
CI / backend-tests (pull_request) Successful in 6m22s
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).
68 lines
2.9 KiB
Python
68 lines
2.9 KiB
Python
"""Pydantic schemas для location (#948 Part B, ТЗ §8.2).
|
||
|
||
LocationOut — ответ API: район + 4 district-level индекса (0..1 или None).
|
||
LocationList — ответ list-эндпоинта (total + rows).
|
||
|
||
Read-only: индексы ВЫЧИСЛЯЮТСЯ воркером (tasks/location_refresh.py), не вводятся
|
||
пользователем → Create/Update-схем нет (CRUD отсутствует, только GET-API).
|
||
|
||
Все индексы Optional[float] ∈ [0,1]: None = нет данных (graceful-on-thin-data —
|
||
никогда не фабрикуем 0). centroid не отдаём бинарём — lon/lat достаточно для карты
|
||
(NULL если centroid не вычислен).
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from datetime import datetime
|
||
|
||
from pydantic import BaseModel, Field
|
||
|
||
|
||
class LocationOut(BaseModel):
|
||
"""Локация (район) с district-level индексами (#948 Part B, §8.2)."""
|
||
|
||
id: int
|
||
district_name: str = Field(..., description="Логический ключ района (8 админ-районов ЕКБ)")
|
||
region: str | None = Field(None, description="Регион (NULL = ЕКБ/Свердл.)")
|
||
lon: float | None = Field(None, description="Долгота центроида района (WGS-84), None если нет")
|
||
lat: float | None = Field(None, description="Широта центроида района (WGS-84), None если нет")
|
||
infra_index: float | None = Field(
|
||
None,
|
||
ge=0.0,
|
||
le=1.0,
|
||
description="Инфра-индекс 0..1 (POI района / POI города); None=нет данных",
|
||
)
|
||
competition_index: float | None = Field(
|
||
None,
|
||
ge=0.0,
|
||
le=1.0,
|
||
description=(
|
||
"Индекс конкуренции 0..1 (доступное/затоваренное конкурирующее предложение "
|
||
"= overstock; выше = больше конкур. давления; ортогонален demand); "
|
||
"None=нет данных"
|
||
),
|
||
)
|
||
demand_index: float | None = Field(
|
||
None,
|
||
ge=0.0,
|
||
le=1.0,
|
||
description="Индекс спроса 0..1 (saturated velocity); None=нет данных",
|
||
)
|
||
future_supply_index: float | None = Field(
|
||
None,
|
||
ge=0.0,
|
||
le=1.0,
|
||
description="Индекс будущего предложения 0..1; None=нет данных",
|
||
)
|
||
indices_computed_at: datetime | None = Field(
|
||
None, description="Когда индексы последний раз пересчитаны (None до первого refresh)"
|
||
)
|
||
created_at: datetime
|
||
updated_at: datetime
|
||
|
||
|
||
class LocationList(BaseModel):
|
||
"""Ответ list-эндпоинта: total + страница строк (зеркало InsightList)."""
|
||
|
||
total: int
|
||
rows: list[LocationOut]
|