Multi-Source Integration Phase 3.2. /api/v1/search with cross-source filters + POST body params + Redis 5min hot cache. New files: app/schemas/search.py — SearchParams (~20 filters, Pydantic v2) app/schemas/search_response.py — SearchResponse + SearchResultItem app/services/search_query.py — build_search_query (psycopg v3, CAST(:x AS type)) app/services/cache.py — SearchCache async wrapper (singleton lru_cache) app/api/v1/search.py — POST /search endpoint tests/test_search_api.py — unit + integration tests Edited: app/main.py — include search.router prefix=/api/v1 app/core/config.py — settings.redis_url default pyproject.toml — add redis>=5.0.0 Queries listings_search_mv (matview from PR #469). Cache failures swallowed (degrade to slow-but-correct). Sort whitelisted dict (no SQL injection). Refs Master Plan sec 9.1 + 7.2.
53 lines
1.3 KiB
Python
53 lines
1.3 KiB
Python
"""SearchResponse + SearchResultItem — /api/v1/search return shape."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import datetime
|
|
|
|
from pydantic import BaseModel, ConfigDict
|
|
|
|
|
|
class SearchResultItem(BaseModel):
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
listing_id: int
|
|
source: str
|
|
source_url: str | None = None
|
|
address: str | None = None
|
|
lat: float | None = None
|
|
lng: float | None = None
|
|
|
|
rooms: int | None = None
|
|
total_area: float | None = None
|
|
floor: int | None = None
|
|
total_floors: int | None = None
|
|
|
|
price_rub: int | None = None
|
|
price_per_m2: int | None = None
|
|
kadastr_num: str | None = None
|
|
scraped_at: datetime | None = None
|
|
|
|
house_id: int | None = None
|
|
year_built: int | None = None
|
|
house_class: str | None = None
|
|
developer_name: str | None = None
|
|
house_rating: float | None = None
|
|
house_ratings_count: int | None = None
|
|
|
|
source_count: int | None = None
|
|
sources: list[str] | None = None
|
|
has_avito: bool | None = None
|
|
has_cian: bool | None = None
|
|
has_yandex: bool | None = None
|
|
|
|
house_median_ppm2: float | None = None
|
|
district: str | None = None
|
|
|
|
|
|
class SearchResponse(BaseModel):
|
|
items: list[SearchResultItem]
|
|
total: int
|
|
page: int
|
|
page_size: int
|
|
elapsed_ms: float | None = None
|
|
cache_hit: bool = False
|