gendesign/tradein-mvp/backend/app/api/v1/search.py
Light1YT 86e9ea2937 fix(week-review): автофиксы код-ревью — 169 issue (label «week ревью 1»)
Многоагентный аудит + имплементация: один воркер на файл, точечные правки.
Верификация: py_compile (47/47 .py) + tsc --noEmit (0 ошибок). Unit-тесты
не прогонялись (окружение не поднято: rollup native dep / нет pytest-venv).

Полностью исправлено (169): #1336, #1337, #1339, #1340, #1341, #1342, #1343, #1345, #1346, #1348, #1349, #1350, #1351, #1354, #1356, #1358, #1359, #1360, #1362, #1364, #1365, #1366, #1367, #1368, #1369, #1370, #1371, #1372, #1373, #1374, #1375, #1376, #1377, #1378, #1379, #1380, #1381, #1382, #1384, #1385, #1386, #1387, #1388, #1389, #1390, #1391, #1392, #1394, #1395, #1396, #1397, #1399, #1400, #1401, #1402, #1403, #1404, #1408, #1409, #1410, #1411, #1412, #1413, #1414, #1415, #1416, #1417, #1418, #1420, #1423, #1425, #1426, #1427, #1428, #1429, #1430, #1431, #1432, #1433, #1434, #1435, #1437, #1438, #1439, #1440, #1441, #1442, #1443, #1444, #1445, #1446, #1447, #1448, #1449, #1450, #1451, #1452, #1453, #1454, #1455, #1456, #1457, #1458, #1459, #1460, #1461, #1462, #1463, #1464, #1465, #1466, #1467, #1468, #1469, #1471, #1472, #1473, #1474, #1476, #1478, #1479, #1481, #1482, #1483, #1484, #1485, #1487, #1488, #1489, #1490, #1491, #1492, #1493, #1494, #1495, #1496, #1497, #1499, #1500, #1501, #1502, #1504, #1505, #1506, #1507, #1510, #1514, #1515, #1516, #1517, #1518, #1519, #1521, #1522, #1523, #1524, #1525, #1526, #1527, #1528, #1529, #1531, #1532, #1533, #1534, #1535, #1536, #1537, #1538

Частично (9, in-file часть, остаток cross-file): #1361, #1419, #1422, #1424, #1470, #1475, #1477, #1480, #1498
Требуют cross-file (3, не тронуты): #1338, #1363, #1421
Пропущено (1): #1539

Не входило в партию: 22 needs-Leha issue (нужны решения владельца).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 20:21:11 +05:00

78 lines
2.6 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.

"""Search endpoint — POST /api/v1/search (Phase 3.2, master plan sec 9.1)."""
from __future__ import annotations
import logging
import time
from typing import Annotated
from fastapi import APIRouter, Depends
from sqlalchemy import text
from sqlalchemy.orm import Session
from app.core.db import get_db
from app.schemas.search import SearchParams
from app.schemas.search_response import SearchResponse, SearchResultItem
from app.services.cache import get_search_cache
from app.services.search_query import build_count_query, build_search_query
logger = logging.getLogger(__name__)
router = APIRouter()
def _map_row(row: object) -> dict[str, object]:
"""Маппит строку matview в поля SearchResultItem.
SQL отдаёт колонку `cadastral_number`, а в схеме поле зовётся
`kadastr_num` — без явного маппинга значение молча теряется (#1514).
"""
data = dict(row)
if "cadastral_number" in data:
data["kadastr_num"] = data.pop("cadastral_number")
return data
@router.post("/search", response_model=SearchResponse)
async def search(
params: SearchParams,
db: Annotated[Session, Depends(get_db)],
) -> SearchResponse:
"""Поиск listings по фильтрам (cross-source matview + Redis 5min cache)."""
started = time.monotonic()
cache = get_search_cache()
cache_key = cache.search_key(params.model_dump(mode="json"))
cached = await cache.get(cache_key)
if cached is not None:
logger.info(
"search cache HIT key=%s page=%d size=%d",
cache_key[:24], params.page, params.page_size,
)
resp = SearchResponse.model_validate(cached)
resp.cache_hit = True
return resp
sql, args = build_search_query(params)
rows = db.execute(text(sql), args).mappings().all()
count_sql, count_args = build_count_query(params)
total = int(db.execute(text(count_sql), count_args).scalar() or 0)
items = [SearchResultItem.model_validate(_map_row(r)) for r in rows]
elapsed_ms = (time.monotonic() - started) * 1000.0
response = SearchResponse(
items=items,
total=total,
page=params.page,
page_size=params.page_size,
elapsed_ms=round(elapsed_ms, 1),
cache_hit=False,
)
await cache.set(cache_key, response.model_dump(mode="json"), ttl=cache.TTL_SEARCH)
logger.info(
"search MISS page=%d size=%d total=%d items=%d elapsed=%.1fms",
params.page, params.page_size, total, len(items), elapsed_ms,
)
return response