gendesign/backend/app/api/v1/analytics.py
lekss361 8d3a0874ef add interactive analytics dashboard for Sverdlovsk market and PRINZIP
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.
2026-04-27 16:55:30 +03:00

117 lines
4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""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")