Merge pull request 'Московский city_hint больше не уходит молча в регион 66' (#3470) from feat/msk-suggest-region-inference into main
Some checks failed
Deploy Trade-In / perimeter-smoke (push) Blocked by required conditions
Deploy Trade-In / deploy-status (push) Blocked by required conditions
Deploy Trade-In / changes (push) Successful in 16s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / test (push) Successful in 4m47s
Deploy Trade-In / build-backend (push) Successful in 1m14s
Deploy Trade-In / deploy (push) Has been cancelled
Some checks failed
Deploy Trade-In / perimeter-smoke (push) Blocked by required conditions
Deploy Trade-In / deploy-status (push) Blocked by required conditions
Deploy Trade-In / changes (push) Successful in 16s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / test (push) Successful in 4m47s
Deploy Trade-In / build-backend (push) Successful in 1m14s
Deploy Trade-In / deploy (push) Has been cancelled
This commit is contained in:
commit
00e0bddfb4
4 changed files with 104 additions and 8 deletions
|
|
@ -79,7 +79,7 @@ from pydantic import BaseModel, Field
|
|||
from sqlalchemy import text
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.v1.geocode import SuggestResponse, suggest_addresses
|
||||
from app.api.v1.geocode import SuggestResponse, effective_region_code, suggest_addresses
|
||||
from app.api.v1.trade_in import coverage_probe, estimate
|
||||
from app.core.config import settings
|
||||
from app.core.db import get_db
|
||||
|
|
@ -222,6 +222,9 @@ class PublicSuggestInput(BaseModel):
|
|||
q: str = Field(min_length=2, max_length=200)
|
||||
limit: int = Field(default=8, ge=1, le=10)
|
||||
city_hint: str | None = Field(default=None, max_length=100)
|
||||
# #3051: явный регион с фронта (если он его когда-нибудь пришлёт) —
|
||||
# приоритетнее вывода из city_hint, см. effective_region_code.
|
||||
region_code: int | None = Field(default=None)
|
||||
|
||||
|
||||
def _fold(text: str) -> str:
|
||||
|
|
@ -326,6 +329,7 @@ async def public_suggest(
|
|||
limit=payload.limit,
|
||||
db=db,
|
||||
city_hint=payload.city_hint,
|
||||
region_code=effective_region_code(payload.region_code, payload.city_hint),
|
||||
)
|
||||
finally:
|
||||
_suggest_slots.release()
|
||||
|
|
|
|||
|
|
@ -13,12 +13,30 @@ from sqlalchemy.orm import Session
|
|||
from app.core.db import get_db, run_db_thread
|
||||
from app.services.estimator import _lookup_house_facts
|
||||
from app.services.geocoder import GeocodeResult, geocode, reverse_geocode, suggest
|
||||
from app.services.regions import DEFAULT_REGION_CODE, region_by_city
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
def effective_region_code(region_code: int | None, city_hint: str | None) -> int:
|
||||
"""Регион для геокодера (#3051): явный `region_code` > вывод из `city_hint` > 66.
|
||||
|
||||
До этого хелпера `/suggest` всегда уходил в геокодер с регионом 66
|
||||
по умолчанию — московский `city_hint` («Москва») молча получал
|
||||
свердловский bbox-констрейнт и терял подсказки. Реестр `app.services.regions`
|
||||
уже знает, каким городам какой регион соответствует (REGIONS[77].cities
|
||||
содержит «москва») — используем его вместо повторного захардкоженного списка.
|
||||
"""
|
||||
if region_code is not None:
|
||||
return region_code
|
||||
region = region_by_city(city_hint)
|
||||
if region is not None:
|
||||
return region.code
|
||||
return DEFAULT_REGION_CODE
|
||||
|
||||
|
||||
@router.get("/lookup", response_model=GeocodeResult)
|
||||
async def lookup(
|
||||
address: Annotated[str, Query(min_length=3, max_length=500)],
|
||||
|
|
@ -81,16 +99,17 @@ async def suggest_addresses(
|
|||
),
|
||||
] = None,
|
||||
region_code: Annotated[
|
||||
int,
|
||||
int | None,
|
||||
Query(
|
||||
description=(
|
||||
"Регион покрытия (#3051). Дефолт 66 — Свердловская область, прежнее "
|
||||
"поведение для существующих клиентов. 77 — Москва: без него DaData "
|
||||
"и Nominatim получают свердловский hard-констрейнт и молча "
|
||||
"возвращают ПУСТО на московском адресе."
|
||||
"Регион покрытия (#3051). None (дефолт) — выводится из `city_hint` "
|
||||
"через реестр регионов, иначе 66 (Свердловская область, прежнее "
|
||||
"поведение). 77 — Москва: без него DaData и Nominatim получают "
|
||||
"свердловский hard-констрейнт и молча возвращают ПУСТО на "
|
||||
"московском адресе."
|
||||
),
|
||||
),
|
||||
] = 66,
|
||||
] = None,
|
||||
) -> SuggestResponse:
|
||||
"""Автокомплит адресов в регионе `region_code` (дефолт 66 — Свердловская область;
|
||||
ЕКБ — основной трафик, остаётся быстрым fast-path).
|
||||
|
|
@ -105,8 +124,11 @@ async def suggest_addresses(
|
|||
/api/v1/geocode/suggest?q=Ленина+1&city_hint=Нижний+Тагил
|
||||
/api/v1/geocode/suggest?q=Тверская+6®ion_code=77 # Москва
|
||||
"""
|
||||
resolved_region_code = effective_region_code(region_code, city_hint)
|
||||
try:
|
||||
items = await suggest(q, db=db, limit=limit, city_hint=city_hint, region_code=region_code)
|
||||
items = await suggest(
|
||||
q, db=db, limit=limit, city_hint=city_hint, region_code=resolved_region_code
|
||||
)
|
||||
except ValueError as exc:
|
||||
# Регион вне реестра покрытия — 422, а не 500: это ошибка ввода клиента.
|
||||
raise HTTPException(status_code=422, detail=str(exc)) from exc
|
||||
|
|
|
|||
|
|
@ -23,6 +23,8 @@ os.environ.setdefault("DATABASE_URL", "postgresql://test:test@localhost/test_db"
|
|||
|
||||
import pytest
|
||||
|
||||
from app.api.v1 import geocode as geocode_module
|
||||
from app.api.v1.geocode import SuggestResponse, effective_region_code
|
||||
from app.services.geocoder import (
|
||||
OBLAST66_VIEWBOX,
|
||||
SVERDLOVSK_OBLAST_REGION,
|
||||
|
|
@ -179,3 +181,52 @@ async def test_suggest_unknown_region_raises() -> None:
|
|||
"""Неизвестный регион — ValueError до похода к провайдерам."""
|
||||
with pytest.raises(ValueError, match="unknown region_code"):
|
||||
await suggest("Ленина 1", db=None, limit=5, region_code=99)
|
||||
|
||||
|
||||
# ── API-слой: effective_region_code и хендлер /suggest ──────────────────────
|
||||
#
|
||||
# Всё выше проверяет, что `suggest()` в geocoder.py УМЕЕТ регион. Ниже —
|
||||
# СЛЕДУЮЩИЙ слой той же истории #3051: сам HTTP-хендлер `/suggest` (и его
|
||||
# публичный прокси в mera.py) до сих пор звал `suggest()` с `region_code=66`
|
||||
# по умолчанию БЕЗУСЛОВНО, даже когда `city_hint='Москва'` уже прямо говорил,
|
||||
# какой регион нужен. Приоритет: явный `region_code` > вывод из `city_hint`
|
||||
# (реестр `app.services.regions`) > 66. Инварианты ниже — на хелпере
|
||||
# `effective_region_code`, который эту логику и вносит.
|
||||
|
||||
|
||||
def test_effective_region_no_hint_no_explicit_defaults_to_66() -> None:
|
||||
assert effective_region_code(None, None) == 66
|
||||
|
||||
|
||||
def test_effective_region_sverdlovsk_city_hint_stays_66() -> None:
|
||||
assert effective_region_code(None, "Серов") == 66
|
||||
|
||||
|
||||
def test_effective_region_moscow_city_hint_infers_77() -> None:
|
||||
assert effective_region_code(None, "Москва") == 77
|
||||
|
||||
|
||||
def test_effective_region_explicit_wins_over_moscow_hint() -> None:
|
||||
assert effective_region_code(66, "Москва") == 66
|
||||
|
||||
|
||||
async def test_suggest_handler_resolves_region_from_moscow_hint() -> None:
|
||||
"""`suggest_addresses` без явного `region_code` реально передаёт вниз 77,
|
||||
а не только хелпер в изоляции."""
|
||||
captured: dict[str, object] = {}
|
||||
|
||||
async def _fake_suggest(q, db=None, limit=8, city_hint=None, region_code=66):
|
||||
captured["region_code"] = region_code
|
||||
return []
|
||||
|
||||
with patch.object(geocode_module, "suggest", _fake_suggest):
|
||||
result = await geocode_module.suggest_addresses(
|
||||
q="Тверская 6",
|
||||
limit=8,
|
||||
db=MagicMock(),
|
||||
city_hint="Москва",
|
||||
region_code=None,
|
||||
)
|
||||
|
||||
assert isinstance(result, SuggestResponse)
|
||||
assert captured["region_code"] == 77
|
||||
|
|
|
|||
|
|
@ -640,6 +640,25 @@ def test_suggest_passes_city_prefixed_query_downstream(client: TestClient) -> No
|
|||
assert captured["q"] == "Серов, Ленина 1"
|
||||
# Сам хинт продолжаем передавать: от него зависит гейт кадастрового тира.
|
||||
assert captured["city_hint"] == "Серов"
|
||||
# Серов — регион 66, вывод из city_hint не меняет прежнее поведение.
|
||||
assert captured["region_code"] == 66
|
||||
|
||||
|
||||
def test_suggest_infers_region_from_moscow_city_hint(client: TestClient) -> None:
|
||||
"""#3051: `city_hint='Москва'` без явного region_code должен уйти в 77,
|
||||
а не молча получить свердловский region_code=66 по умолчанию.
|
||||
"""
|
||||
captured: dict[str, object] = {}
|
||||
|
||||
async def _fake(**kwargs):
|
||||
captured.update(kwargs)
|
||||
return SuggestResponse(items=[])
|
||||
|
||||
with patch.object(public_mera, "suggest_addresses", _fake):
|
||||
resp = client.post(f"{PREFIX}/suggest", json={"q": "Тверская 6", "city_hint": "Москва"})
|
||||
|
||||
assert resp.status_code == 200, resp.text
|
||||
assert captured["region_code"] == 77
|
||||
|
||||
|
||||
# ── 9. Невалидный вход: 422 и всё тот же бюджет ──────────────────────────────
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue