"""SearchParams + SearchResponse — /api/v1/search (Phase 3.2).""" from __future__ import annotations from typing import Literal from pydantic import BaseModel, Field, model_validator SortKey = Literal[ "price_asc", "price_desc", "area_desc", "area_asc", "date_desc", "dist_asc" ] class SearchParams(BaseModel): """Фильтры поиска по listings_search_mv (master plan sec 9.1).""" # --- Geo (radius search) --- lat: float | None = Field(default=None, ge=-90.0, le=90.0) lon: float | None = Field(default=None, ge=-180.0, le=180.0) radius_m: int = Field(default=2000, ge=100, le=50000) # --- Property filters --- rooms: int | None = Field(default=None, ge=0, le=10) rooms_in: list[int] | None = None area_m2_min: float | None = Field(default=None, ge=0) area_m2_max: float | None = Field(default=None, ge=0) price_rub_min: int | None = Field(default=None, ge=0) price_rub_max: int | None = Field(default=None, ge=0) price_per_m2_max: int | None = Field(default=None, ge=0) floor_min: int | None = Field(default=None, ge=1) floor_max: int | None = Field(default=None, ge=1) # --- House filters --- year_built_min: int | None = Field(default=None, ge=1800, le=2100) year_built_max: int | None = Field(default=None, ge=1800, le=2100) house_class: list[str] | None = None floors_total_min: int | None = Field(default=None, ge=1) floors_total_max: int | None = Field(default=None, ge=1) # --- Quality / cross-source --- has_kadastr: bool = False sources: list[Literal["avito", "cian", "yandex_realty", "n1"]] | None = None multi_source_only: bool = False require_avito: bool = False require_cian: bool = False require_yandex: bool = False # --- Text search --- address_query: str | None = Field(default=None, max_length=200) description_query: str | None = Field(default=None, max_length=200) # --- Sort + pagination --- sort: SortKey = "date_desc" page: int = Field(default=1, ge=1, le=1000) page_size: int = Field(default=50, ge=1, le=200) @model_validator(mode="after") def _validate_geo_pair(self) -> SearchParams: if (self.lat is None) != (self.lon is None): raise ValueError("lat and lon must be provided together") if self.sort == "dist_asc" and self.lat is None: raise ValueError("sort=dist_asc requires lat+lon") if self.price_rub_min and self.price_rub_max and self.price_rub_min > self.price_rub_max: raise ValueError("price_rub_min > price_rub_max") if self.area_m2_min and self.area_m2_max and self.area_m2_min > self.area_m2_max: raise ValueError("area_m2_min > area_m2_max") return self @property def offset(self) -> int: return (self.page - 1) * self.page_size