gendesign/tradein-mvp/backend/app/api/v1/buildings.py
lekss361 f3e10a04ad
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
feat(tradein): API домов «доли квартир в продаже» — /buildings/sale-share (+ listings, summary) (#2055)
2026-06-28 14:14:18 +00:00

104 lines
4.1 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.

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