All checks were successful
Deploy Trade-In / changes (push) Successful in 15s
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 3m50s
Deploy Trade-In / build-backend (push) Successful in 1m49s
Deploy Trade-In / deploy (push) Successful in 4m41s
Deploy Trade-In / deploy-status (push) Successful in 1s
Deploy Trade-In / perimeter-smoke (push) Successful in 1m44s
ДомКлик: DomClickGeoProfile вместо зашитых _EKB_ADDRESS_GUID/_EKB_AREA_ID; GUID Москвы и области проверены живьём, aids вне ЕКБ не нужен, гард по bbox профиля. Страница прогрева 77/50 — апексный domclick.ru: субдомены msk./moskovskaya-oblast. отдают 301. Циан/Авито/Яндекс: CityLocation.cian_host, три новых скоупа (moskva, moskovskaya_oblast, moskva_i_mo), avito_slug_is_region, city=NULL у мультигородских скоупов. Неизвестный слаг теперь падает с ValueError вместо молчаливого отката на Екатеринбург. Якоря: Москва — сетка 25 точек под radius_m=8000; область — 22 города-спутника (10 добраны из Nominatim) плюс 22 кластера лот-массы. Замер на проде: города радиусом 10 км дают 73.8% лот-массы области, вместе с кластерами — 95.2%. Ценовые коридоры: планировщик plan_price_corridors со статистикой усечения плюс BisectionStats в живом движке. Провайдеры их пока не передают — отдельный заход. Расписаний scrape_schedules для 77/50 в этом PR нет: они пойдут после первого ручного прогона, подтверждающего живость профилей.
94 lines
4.8 KiB
Python
94 lines
4.8 KiB
Python
"""Гард гео-скоупа свипов: `region_code` 77/50 не должен молча собирать Екатеринбург.
|
||
|
||
Контекст: планировщик читает регион из строки расписания (#3515,
|
||
`scheduler._resolve_region_code`), но гео-скоуп свипа задаётся НЕ им — у avito/cian/
|
||
yandex это anchors города (`CITY_ANCHORS`/`CITY_LOCATIONS`, сегодня только
|
||
Свердловская обл.), а у domclick — профиль региона в BFF-URL (DOMCLICK_GEO_PROFILES:
|
||
66/77/50; код без профиля — падение). Заведи мы
|
||
строку `*_city_sweep_moscow` с `{"region_code": 77}` — прогон собрал бы ЕКБ и
|
||
проштамповал его московским регионом, не показав ни одной аномалии в счётчиках.
|
||
Гард превращает эту тихую порчу в падение до первого запроса.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import inspect
|
||
from collections.abc import Callable
|
||
from typing import Any
|
||
|
||
import pytest
|
||
from scraper_kit.orchestration.pipeline import (
|
||
_require_region_scope,
|
||
run_avito_city_sweep,
|
||
run_cian_city_sweep,
|
||
run_domclick_city_sweep,
|
||
run_yandex_city_sweep,
|
||
)
|
||
from scraper_kit.providers.domclick.serp import geo_profile_for_region
|
||
|
||
_EKB_ANCHOR = [(56.8400, 60.6050, "Центр")]
|
||
|
||
|
||
def test_default_region_without_anchors_passes() -> None:
|
||
"""ЕКБ-прогоны (anchors=None, регион по умолчанию 66) не трогаем."""
|
||
_require_region_scope("avito_city_sweep", 66, None)
|
||
|
||
|
||
def test_default_region_with_city_anchors_passes() -> None:
|
||
"""Свердловские oblast-строки (#179/#262) продолжают работать как прежде."""
|
||
_require_region_scope("cian_city_sweep", 66, [(57.910, 59.980, "Н.Тагил центр")])
|
||
|
||
|
||
@pytest.mark.parametrize("region_code", [50, 77])
|
||
def test_foreign_region_without_anchors_raises(region_code: int) -> None:
|
||
"""Москва/МО без своих anchors — тот самый случай, что молча давал ЕКБ."""
|
||
with pytest.raises(ValueError, match=f"region_code={region_code}"):
|
||
_require_region_scope("avito_city_sweep", region_code, None)
|
||
|
||
|
||
@pytest.mark.parametrize("region_code", [50, 77])
|
||
def test_foreign_region_with_explicit_anchors_passes(region_code: int) -> None:
|
||
"""Явные anchors — явный гео-скоуп: запрещаем не регион, а молчаливый ЕКБ-fallback."""
|
||
_require_region_scope("avito_city_sweep", region_code, _EKB_ANCHOR)
|
||
|
||
|
||
def _required_kwargs(fn: Callable[..., Any], **overrides: Any) -> dict[str, Any]:
|
||
"""None во все обязательные keyword-параметры: гард обязан сработать раньше их."""
|
||
kwargs: dict[str, Any] = {
|
||
name: None
|
||
for name, prm in inspect.signature(fn).parameters.items()
|
||
if prm.kind is inspect.Parameter.KEYWORD_ONLY and prm.default is inspect.Parameter.empty
|
||
}
|
||
kwargs.update(overrides)
|
||
return kwargs
|
||
|
||
|
||
@pytest.mark.parametrize(
|
||
"sweep",
|
||
[run_avito_city_sweep, run_cian_city_sweep, run_yandex_city_sweep],
|
||
)
|
||
async def test_anchor_sweeps_reject_foreign_region(sweep: Callable[..., Any]) -> None:
|
||
"""Гард стоит до первого обращения к БД/сети — поэтому db=None здесь безопасен."""
|
||
with pytest.raises(ValueError, match="region_code=77"):
|
||
await sweep(None, **_required_kwargs(sweep, run_id=1, region_code=77, anchors=None))
|
||
|
||
|
||
@pytest.mark.parametrize("region_code", [1, 99])
|
||
async def test_domclick_sweep_rejects_region_without_profile(region_code: int) -> None:
|
||
"""У домклика anchors нет вовсе: гео-скоуп задаёт профиль региона
|
||
(DOMCLICK_GEO_PROFILES: address GUID, aids, страница прогрева, bbox). 66/77/50
|
||
теперь поддержаны, а код без профиля по-прежнему падает до первого запроса:
|
||
тихий fallback на ЕКБ — ровно та порча, от которой стережёт этот файл."""
|
||
with pytest.raises(ValueError, match=f"region_code={region_code}"):
|
||
await run_domclick_city_sweep(
|
||
None, # type: ignore[arg-type]
|
||
**_required_kwargs(run_domclick_city_sweep, run_id=1, region_code=region_code),
|
||
)
|
||
|
||
|
||
@pytest.mark.parametrize("region_code", [66, 77, 50])
|
||
def test_domclick_supported_regions_have_profiles(region_code: int) -> None:
|
||
"""Москва и область больше НЕ режутся: у них свой GUID и свой bbox."""
|
||
profile = geo_profile_for_region(region_code)
|
||
assert profile.region_code == region_code
|
||
assert profile.address_guid
|