gendesign/tradein-mvp/backend/app/schemas/buildings.py
bot-backend b105e0a500
All checks were successful
CI / changes (pull_request) Successful in 8s
CI / frontend-tests (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
fix(tradein/sale-share): дедуп карточки листингов + чистка голых алиасов
#4 Карточка дома показывала cross-площадочные дубли (Искровцев 33: 2 квартиры
= 4 строки avito/domklik/cian). build_listings_query теперь дедуплит по сигнатуре
(rooms, round(area_m2), floor) — как v_building_sale_share мигр.148 — одна строка
на квартиру, представитель = самый дешёвый, +колонка sources[] (площадки).
Гео-фильтр ≤300м (мигр.150) сохранён. +BuildingListing.sources в схеме.

P4a Мигр.151: DELETE 6338/21720 голых алиасов house_address_aliases (без номера
дома) — сток, в который сваливались безномерные листинги (Tier-2b). Матчер уже
захардненен (P1 #2082), голые не регенерируют. Алиасы — регенерируемый кэш,
house_id_fk/house_sources не тронуты.

ruff clean, pytest test_buildings_api 26 green.
2026-06-29 00:22:20 +03:00

94 lines
3.7 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.

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