All checks were successful
Deploy Trade-In / changes (push) Successful in 12s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / test (push) Successful in 1m39s
Deploy Trade-In / build-backend (push) Successful in 8m5s
Deploy Trade-In / deploy (push) Successful in 52s
104 lines
4.1 KiB
Python
104 lines
4.1 KiB
Python
"""Buildings — страница «доля квартир дома в продаже» (мигр. 143).
|
||
|
||
Дома вторичного рынка, где active_secondary / flat_count_effective >= threshold%.
|
||
Данные берём из готового view v_building_sale_share (логику доли не пересчитываем).
|
||
|
||
Endpoints:
|
||
- GET /api/v1/buildings/sale-share — список домов
|
||
- GET /api/v1/buildings/{house_id}/listings — активные вторичные объявления дома
|
||
- GET /api/v1/buildings/sale-share/summary — сводка для слайдера % + coverage
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import logging
|
||
from typing import Annotated
|
||
|
||
from fastapi import APIRouter, Depends, Path, Query
|
||
from sqlalchemy import text
|
||
from sqlalchemy.orm import Session
|
||
|
||
from app.core.db import get_db
|
||
from app.schemas.buildings import (
|
||
BuildingListing,
|
||
BuildingSaleShare,
|
||
HistogramBucket,
|
||
SaleShareSummary,
|
||
)
|
||
from app.services.buildings_query import (
|
||
HISTOGRAM_BUCKETS,
|
||
build_listings_query,
|
||
build_sale_share_query,
|
||
build_summary_histogram_query,
|
||
build_summary_scalars_query,
|
||
)
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
router = APIRouter()
|
||
|
||
|
||
@router.get("/sale-share", response_model=list[BuildingSaleShare])
|
||
async def list_sale_share(
|
||
db: Annotated[Session, Depends(get_db)],
|
||
min_pct: Annotated[float, Query(ge=0, le=1000)] = 5.0,
|
||
max_pct: Annotated[float | None, Query(ge=0, le=1000)] = None,
|
||
city: Annotated[str | None, Query(max_length=120)] = None,
|
||
price_min: Annotated[int | None, Query(ge=0)] = None,
|
||
price_max: Annotated[int | None, Query(ge=0)] = None,
|
||
year_min: Annotated[int | None, Query(ge=1800, le=2100)] = None,
|
||
year_max: Annotated[int | None, Query(ge=1800, le=2100)] = None,
|
||
house_type: Annotated[str | None, Query(max_length=40)] = None,
|
||
sort: Annotated[
|
||
str,
|
||
Query(pattern="^(share_desc|active_desc|exposure_desc|price_asc|price_desc)$"),
|
||
] = "share_desc",
|
||
limit: Annotated[int, Query(ge=1, le=1000)] = 200,
|
||
) -> list[BuildingSaleShare]:
|
||
"""Дома вторички с долей квартир в продаже >= min_pct (+ фильтры)."""
|
||
sql, args = build_sale_share_query(
|
||
min_pct=min_pct,
|
||
max_pct=max_pct,
|
||
city=city,
|
||
price_min=price_min,
|
||
price_max=price_max,
|
||
year_min=year_min,
|
||
year_max=year_max,
|
||
house_type=house_type,
|
||
sort=sort,
|
||
limit=limit,
|
||
)
|
||
rows = db.execute(text(sql), args).mappings().all()
|
||
return [BuildingSaleShare.model_validate(r) for r in rows]
|
||
|
||
|
||
@router.get("/{house_id}/listings", response_model=list[BuildingListing])
|
||
async def list_building_listings(
|
||
db: Annotated[Session, Depends(get_db)],
|
||
house_id: Annotated[int, Path(ge=1)],
|
||
) -> list[BuildingListing]:
|
||
"""Активные вторичные объявления в одном доме (для drawer), ORDER BY price."""
|
||
sql, args = build_listings_query(house_id)
|
||
rows = db.execute(text(sql), args).mappings().all()
|
||
return [BuildingListing.model_validate(r) for r in rows]
|
||
|
||
|
||
@router.get("/sale-share/summary", response_model=SaleShareSummary)
|
||
async def sale_share_summary(
|
||
db: Annotated[Session, Depends(get_db)],
|
||
) -> SaleShareSummary:
|
||
"""Сводка распределения sale_share_pct: coverage, max, p95, гистограмма."""
|
||
scalars = db.execute(text(build_summary_scalars_query())).mappings().one()
|
||
|
||
hist_rows = db.execute(text(build_summary_histogram_query())).mappings().all()
|
||
counts = {str(r["bucket"]): int(r["count"]) for r in hist_rows}
|
||
histogram = [HistogramBucket(bucket=b, count=counts.get(b, 0)) for b in HISTOGRAM_BUCKETS]
|
||
|
||
return SaleShareSummary(
|
||
total_secondary_buildings=int(scalars["total_secondary_buildings"]),
|
||
buildings_with_denominator=int(scalars["buildings_with_denominator"]),
|
||
coverage_pct=float(scalars["coverage_pct"]),
|
||
max_pct=(float(scalars["max_pct"]) if scalars["max_pct"] is not None else None),
|
||
p95_pct=(float(scalars["p95_pct"]) if scalars["p95_pct"] is not None else None),
|
||
histogram=histogram,
|
||
)
|