"""Pydantic-схемы страницы «доля квартир дома в продаже» (мигр. 143). Источник данных — view v_building_sale_share + базовая таблица listings. Список домов вторичного рынка, где active_secondary / flat_count_effective >= threshold%. """ from __future__ import annotations from datetime import date from pydantic import BaseModel, ConfigDict class BuildingSaleShare(BaseModel): """Один дом из v_building_sale_share для списка /buildings/sale-share.""" model_config = ConfigDict(from_attributes=True) house_id: int address: str | None = None lat: float | None = None lon: float | None = None sale_share_pct: float | None = None # 2-й процент: distinct vtorichka за последние 45д (активные ИЛИ недавно # закрытые) / знаменатель. Дом проходит фильтр по ЛЮБОМУ из двух процентов. sale_share_pct_45d: float | None = None # implausible-матч (числитель > знаменателя по любому из %) — скорее всего # коллизия адреса при ГАР-матче; не прячем, а маркируем для UI. over_100: bool = False active_secondary: int | None = None # distinct vtorichka-листинги (активные ИЛИ закрытые) за окно 45д. listings_45d: int | None = None flat_count_effective: int | None = None gar_match_method: str | None = None median_price_rub: int | None = None median_price_per_m2: int | None = None avg_days_on_market: float | None = None year_built: int | None = None house_type: str | None = None total_floors: int | None = None series_name: str | None = None is_emergency: bool | None = None class BuildingListing(BaseModel): """Активное вторичное объявление в доме (click-through drawer).""" model_config = ConfigDict(from_attributes=True) listing_id: int source: str source_url: str | None = None price_rub: int | None = None price_per_m2: int | None = None rooms: int | None = None area_m2: float | None = None floor: int | None = None total_floors: int | None = None days_on_market: int | None = None listing_date: date | None = None address: str | None = None # Площадки, где видна эта физическая квартира (дедуп кросс-площадочных дублей # по сигнатуре (rooms, round(area_m2), floor) — мигр.148). Представитель строки — # самый дешёвый листинг; sources перечисляет все источники той же квартиры. sources: list[str] | None = None class HistogramBucket(BaseModel): """Корзина гистограммы распределения sale_share_pct.""" bucket: str count: int class SaleShareSummary(BaseModel): """Сводка для слайдера % + баннера покрытия (/buildings/sale-share/summary). buildings_with_denominator/coverage считают дома с непустым ЛЮБЫМ из двух %. max/p95 и histogram — отдельно для каждого процента (now + 45д). """ total_secondary_buildings: int buildings_with_denominator: int coverage_pct: float max_pct: float | None = None p95_pct: float | None = None max_pct_45d: float | None = None p95_pct_45d: float | None = None histogram: list[HistogramBucket] histogram_45d: list[HistogramBucket]