"""Golden-parity: `scraper_kit.providers.yandex.*` парсинг ≡ ещё живым `app.services.scrapers.yandex_*`. Strangler-инвариант (#2133): новая scraper_kit-копия yandex-провайдера должна давать БАЙТ-ИДЕНТИЧНЫЙ результат парсинга старому боевому коду на одинаковом входе. Развязка (settings→ScraperConfig, get_scraper_delay→delay_provider, house_type_normalizer→providers.yandex.shared, base/browser_fetcher/helpers→scraper_kit.*) НЕ меняет распарсенные данные. `app.services.scrapers.yandex_realty` удалён (#2397 Part E2, 0 runtime-импортёров). SERP `_entity_to_lot`/`_parse_gate_json`/`_build_url` parity-тесты (сравнение с `YandexRealtyScraper`) удалены вместе с ним (легаси-эталон исчез). Оставшееся сравнивает kit ≡ ЕЩЁ ЖИВЫЕ легаси-модули: - `normalize_house_type` — `app.services.scrapers.house_type_normalizer` (живой); - newbuilding `parse` (ЖК-лендинг → YandexNewbuildingInfo) — `app.services.scrapers.yandex_newbuilding` (живой); - strangler-guard: kit-провайдер не импортирует `app.*`. Легаси `app.services.scrapers.yandex_detail` / `yandex_valuation` удалены (#2277 финальный шаг scraper_kit-миграции, 0 runtime importers) — detail/valuation parity-проверки убраны отсюда ранее. Kit-side coverage для detail/valuation `.parse()` сохранена без изменений в `tests/test_yandex_detail.py` / `tests/test_yandex_detail_structural.py` / `tests/test_yandex_valuation.py` / `tests/test_yandex_valuation_save.py` (переведены на kit-эквиваленты тем же PR). Идентичность результата = kit-парсер верно скопирован (развязка не задела логику). """ from __future__ import annotations import inspect import os # Старый app.services.scrapers.house_type_normalizer/yandex_newbuilding импортирует # app.core.config.settings=Settings(), которому нужен DATABASE_URL. Офлайн-парсинг # БД не трогает — фиктивный DSN достаточен. os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://test:test@localhost/test_db") # НОВЫЙ (scraper_kit) и СТАРЫЙ (app.services.scrapers) yandex-парсеры — оба в одном # тесте; ruff сортирует scraper_kit (first-party) выше app-импортов. from scraper_kit.providers.yandex.newbuilding import ( YandexNewbuildingScraper as NewNewbuildingScraper, ) from scraper_kit.providers.yandex.shared import normalize_house_type as new_normalize_house_type from app.services.scrapers.house_type_normalizer import ( normalize_house_type as old_normalize_house_type, ) from app.services.scrapers.yandex_newbuilding import ( YandexNewbuildingScraper as OldNewbuildingScraper, ) # ── normalize_house_type parity ────────────────────────────────────────────── def test_normalize_house_type_parity() -> None: """SCREAMING / camelCase / канон / мусор → идентичный результат.""" raws = [ None, "", "MONOLIT", "BRICK", "PANEL", "MONOLIT_BRICK", "monolithBrick", "gasSilicateBlock", "monolith_brick", "monolith", "stalin", "OTHER", " MONOLIT ", ] for raw in raws: assert old_normalize_house_type(raw) == new_normalize_house_type(raw) # ── newbuilding parse parity (синтетика) ───────────────────────────────────── _NEWBUILDING_HTML = """

ЖК «Татлин»

Екатеринбург, ул. Черепанова 4.3 из 5 1505 оценок Смотреть все 353 отзыва 56.855312 60.576668

О комплексе

Дом комфорт-класса, монолитный. Введён в эксплуатацию в июнь 2023. 35-этажные башни на участке 1,5 га. Три корпуса.
""" def test_newbuilding_parse_parity() -> None: """ЖК-лендинг → YandexNewbuildingInfo идентичен.""" old_scraper = OldNewbuildingScraper() new_scraper = NewNewbuildingScraper() old_res = old_scraper.parse( _NEWBUILDING_HTML, jk_slug="tatlin", jk_id="1592987", source_url="https://realty.yandex.ru/ekaterinburg/kupit/novostrojka/tatlin-1592987/", ) new_res = new_scraper.parse( _NEWBUILDING_HTML, jk_slug="tatlin", jk_id="1592987", source_url="https://realty.yandex.ru/ekaterinburg/kupit/novostrojka/tatlin-1592987/", ) assert old_res.model_dump() == new_res.model_dump() # ── strangler-guard: kit-провайдер не импортирует app.* ────────────────────── def test_kit_yandex_has_no_app_imports() -> None: """Ни один модуль scraper_kit.providers.yandex.* не импортирует app.* (развязка).""" from scraper_kit.providers.yandex import detail, newbuilding, serp, shared, valuation for mod in (serp, detail, valuation, newbuilding, shared): source = inspect.getsource(mod) for line in source.splitlines(): stripped = line.strip() assert not stripped.startswith("from app"), f"{mod.__name__}: {line!r}" assert not stripped.startswith("import app"), f"{mod.__name__}: {line!r}"