3 pages (market, PRINZIP drilldown, developers leaderboard) on top of existing v_developer_full_metrics + domrf_realization views. ECharts on the frontend, FastAPI router /api/v1/analytics on the backend.
117 lines
4 KiB
Python
117 lines
4 KiB
Python
"""Analytics endpoints for the Sverdlovsk dashboard."""
|
||
|
||
from __future__ import annotations
|
||
|
||
import re
|
||
from typing import Annotated, Any, Literal
|
||
|
||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||
from sqlalchemy.orm import Session
|
||
|
||
from app.core.db import get_db
|
||
from app.services import analytics_queries as q
|
||
|
||
router = APIRouter()
|
||
|
||
DEV_ID_RE = re.compile(r"^\d+_\d+$")
|
||
|
||
|
||
def _validate_dev(dev_id: str) -> str:
|
||
if not DEV_ID_RE.match(dev_id):
|
||
raise HTTPException(status_code=400, detail="developer_id must match \\d+_\\d+")
|
||
return dev_id
|
||
|
||
|
||
# ---- Sverdlovsk region ------------------------------------------------------
|
||
|
||
|
||
@router.get("/sverdl/market-pulse")
|
||
def sverdl_market_pulse(db: Annotated[Session, Depends(get_db)]) -> list[dict[str, Any]]:
|
||
"""Monthly series Jan 2025 → present: total square (тыс м²), sold%, avg price."""
|
||
return q.market_pulse(db, region_code=66)
|
||
|
||
|
||
@router.get("/sverdl/quartirography")
|
||
def sverdl_quartirography(
|
||
db: Annotated[Session, Depends(get_db)],
|
||
source: Literal["portfolio", "deals"] = "portfolio",
|
||
) -> list[dict[str, Any]]:
|
||
"""Что строится (portfolio) vs что покупают (deals) по сегментам."""
|
||
return q.quartirography(db, source=source, region_id=66)
|
||
|
||
|
||
@router.get("/sverdl/pipeline")
|
||
def sverdl_pipeline(db: Annotated[Session, Depends(get_db)]) -> list[dict[str, Any]]:
|
||
"""По году ввода: total / sold% / unsold% / unopened%."""
|
||
return q.pipeline_by_year(db, region_code=66)
|
||
|
||
|
||
@router.get("/sverdl/districts")
|
||
def sverdl_districts(db: Annotated[Session, Depends(get_db)]) -> list[dict[str, Any]]:
|
||
"""ЕКБ-районы: ЖК-count, flat-count, area, цены."""
|
||
return q.districts(db)
|
||
|
||
|
||
@router.get("/sverdl/yandex-listings")
|
||
def sverdl_yandex_listings(db: Annotated[Session, Depends(get_db)]) -> dict[str, Any]:
|
||
"""Снимок активных новостроек Яндекс.Недвижимости."""
|
||
return q.yandex_listings(db)
|
||
|
||
|
||
# ---- Developers -------------------------------------------------------------
|
||
|
||
|
||
@router.get("/developers/top")
|
||
def developers_top(
|
||
db: Annotated[Session, Depends(get_db)],
|
||
limit: Annotated[int, Query(ge=1, le=50)] = 15,
|
||
) -> list[dict[str, Any]]:
|
||
"""Топ-девелоперы Свердл по объёму с Δ sold% за всю доступную историю."""
|
||
return q.top_developers(db, region_code=66, limit=limit)
|
||
|
||
|
||
@router.get("/developers/history")
|
||
def developers_history(
|
||
db: Annotated[Session, Depends(get_db)],
|
||
ids: Annotated[str, Query(min_length=1)],
|
||
) -> list[dict[str, Any]]:
|
||
"""Comma-separated developer_ids → time series sold_perc."""
|
||
dev_ids = [_validate_dev(i.strip()) for i in ids.split(",") if i.strip()]
|
||
if not dev_ids:
|
||
raise HTTPException(status_code=400, detail="ids must contain at least one developer_id")
|
||
return q.developer_history(db, developer_ids=dev_ids, region_code=66)
|
||
|
||
|
||
@router.get("/developers/{developer_id}")
|
||
def developer_detail(
|
||
db: Annotated[Session, Depends(get_db)],
|
||
developer_id: str,
|
||
) -> dict[str, Any]:
|
||
dev = _validate_dev(developer_id)
|
||
result = q.developer_detail(db, developer_id=dev)
|
||
if result is None:
|
||
raise HTTPException(status_code=404, detail="developer not found")
|
||
return result
|
||
|
||
|
||
@router.get("/developers/{developer_id}/portfolio")
|
||
def developer_portfolio(
|
||
db: Annotated[Session, Depends(get_db)],
|
||
developer_id: str,
|
||
) -> list[dict[str, Any]]:
|
||
dev = _validate_dev(developer_id)
|
||
return q.developer_portfolio(db, developer_id=dev)
|
||
|
||
|
||
# ---- PRINZIP-specific -------------------------------------------------------
|
||
|
||
|
||
@router.get("/prinzip/insights")
|
||
def prinzip_insights() -> dict[str, Any]:
|
||
"""Static recommendations + benchmarks (from PRINZIP_Strategy_Apr27)."""
|
||
return q.prinzip_insights()
|
||
|
||
|
||
@router.get("/prinzip/districts")
|
||
def prinzip_districts(db: Annotated[Session, Depends(get_db)]) -> list[dict[str, Any]]:
|
||
return q.prinzip_district_distribution(db, developer_id="6208_0")
|