Москва: пред-геокод Авито, Яндекс третьей площадкой, продукт отвечает по региону 77 #3440

Merged
bot-backend merged 9 commits from feat/msk-collector-cian into main 2026-09-11 22:30:12 +00:00
2 changed files with 51 additions and 1 deletions
Showing only changes of commit de5f4a32cd - Show all commits

View file

@ -224,6 +224,7 @@ CREATE TABLE IF NOT EXISTS msk_raw.avito_geocode (
resolved_at timestamptz NOT NULL DEFAULT NOW()
)
"""
_GEO_CACHE_EXISTS = "SELECT to_regclass('msk_raw.avito_geocode')"
_GEO_CACHE_SELECT = """
SELECT cache_key, lat, lon, region_code, full_address
FROM msk_raw.avito_geocode
@ -350,7 +351,17 @@ def _resolve_points(
return {}
points: dict[str, GeoPoint | None] = {}
cached = db.execute(text(_GEO_CACHE_SELECT), {"keys": list(wanted)}).mappings().all()
# Кэш создаётся только боевым прогоном (`geocode and not dry_run`), поэтому на
# первом `--dry-run` таблицы ещё нет и SELECT по ней роняет весь замер — то
# есть ломается ровно та репетиция, ради которой сухой прогон и существует.
# Проверяем наличие отношения, а не ловим исключение: в Postgres упавший
# оператор кладёт транзакцию целиком, и except потребовал бы rollback.
cache_exists = db.execute(text(_GEO_CACHE_EXISTS)).scalar() is not None
cached = (
db.execute(text(_GEO_CACHE_SELECT), {"keys": list(wanted)}).mappings().all()
if cache_exists
else ()
)
for row in cached:
points[row["cache_key"]] = (
None

View file

@ -105,6 +105,10 @@ class _FakeResult:
def all(self) -> list[dict[str, Any]]:
return self._rows
def scalar(self) -> Any:
"""Проба существования кэш-таблицы: None = таблицы ещё нет."""
return self._rows[0] if self._rows else None
class _FakeSession:
"""Отдаёт строки постранично по тому же keyset-условию, что и настоящая вью."""
@ -361,3 +365,38 @@ def test_geocode_is_refused_for_cian(captured: list[dict[str, Any]]) -> None:
"""У Циана город виден по префиксу округа даром — квоту на него не тратим."""
with pytest.raises(SystemExit):
mod.import_msk_raw(_FakeSession(_rows()), source="cian", batch_size=10, geocode=True)
def test_dry_run_survives_missing_geocode_cache(monkeypatch: pytest.MonkeyPatch) -> None:
"""Первый `--dry-run` идёт по проду, где кэш-таблицы ещё нет.
Таблицу создаёт только боевой прогон, поэтому SELECT по ней в сухом ронял
ровно ту репетицию, ради которой сухой прогон и существует (замер на проде
11.09: UndefinedTable msk_raw.avito_geocode). Резолвер обязан молча считать
кэш пустым и идти к DaData за остатком.
"""
class _NoCacheSession:
def __init__(self) -> None:
self.cache_reads = 0
def execute(self, sql: Any, params: dict[str, Any] | None = None) -> _FakeResult:
if "to_regclass" in str(sql):
return _FakeResult([]) # таблицы нет
self.cache_reads += 1
raise AssertionError("к отсутствующей кэш-таблице ходить нельзя")
point = mod.GeoPoint(55.6412, 37.6089, 77, "г Москва, ш Варшавское, д 62 к 1")
async def _fake_geocode(misses: list[Any]) -> dict[str, Any]:
return {misses[0][0]: point}
monkeypatch.setattr(mod, "_geocode_many", _fake_geocode)
db = _NoCacheSession()
points = mod._resolve_points(
db, [{"id": 1, "payload": _avito()}], mod.GeocodeBudget(10), dry_run=True
)
assert db.cache_reads == 0
assert list(points.values()) == [point]