All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI / changes (pull_request) Successful in 8s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 1m36s
Удалены (0 runtime importers в app/, scripts/, packages/; проверено grep'ом, включая intra-legacy импорты в оставшихся 21 legacy-модуле scrapers/): - backend/app/services/scrapers/avito_imv.py - backend/app/services/scrapers/domclick_detail.py - backend/app/services/scrapers/ekb_geoportal_client.py - backend/app/services/scrapers/yandex_detail.py - backend/app/services/scrapers/yandex_valuation.py Тесты — удалены (pure legacy-vs-kit parity, kit-only coverage уже есть в другом месте): - tests/scrapers/test_ekb_geoportal_client_kit_parity.py - tests/scrapers/test_domclick_detail_kit_parity.py Тесты — конвертированы в kit-only (импорт легаси заменён на scraper_kit.*, покрытие сохранено без потерь): - tests/scrapers/test_avito_imv_kit_parity.py (parity-тесты убраны, config-footgun regression на kit-стороне оставлен) - tests/scrapers/test_avito_imv_browser_transport.py - tests/scrapers/test_yandex_valuation_kit_migration.py (parity убрана, footgun- регрессия mandatory config/delay_provider оставлена) - tests/scrapers/test_domclick_detail.py (+ exceptions переведены на scraper_kit.domclick_exceptions, т.к. kit parse_detail_html поднимает их) - tests/test_avito_imv_parse.py - tests/test_ekb_geoportal_ingest.py (client-часть; ingest/geocoder-часть не трогали — не зависят от удалённого модуля) - tests/test_yandex_detail.py - tests/test_yandex_detail_structural.py - tests/test_yandex_valuation.py (YandexValuationScraper() → config=_KIT_CONFIG, kit конструктор требует обязательный config) - tests/test_yandex_valuation_save.py - tests/test_extval_house_id_write_path.py (только yandex_valuation-часть; cian_valuation остался нетронутым — живой легаси-модуль) - tests/test_yandex_history_area_filter.py Тесты — частично отредактированы (убраны только части про удалённые модули, живые легаси-модули/их parity не тронуты): - tests/scrapers/test_admin_domclick_ingest_kit_parity.py (base.py/ScrapedLot parity остался) - tests/scrapers/test_admin_yandex_kit_parity.py (yandex_realty parity остался) - tests/scrapers/test_admin_avito_kit_parity.py (avito.py/avito_houses.py parity остался; _parse_price parity убран) - tests/scrapers/test_avito_unix_date_tz_consistency.py (IMV-ветка переведена на kit avito.imv, легаси avito/avito_houses/avito_shared/avito_detail не тронуты) - tests/tasks/test_yandex_detail_backfill.py (subject — kit-задача, уже вызывает scraper_kit.providers.yandex.detail; save_detail_enrichment coverage-тесты внизу файла переведены на kit) - tests/test_scraper_kit_yandex_golden_parity.py (detail/valuation parity убраны; serp/newbuilding/house_type_normalizer/build_url для yandex_realty/ yandex_newbuilding не тронуты) - tests/test_scraper_proxy.py (YandexValuation proxy-тесты переведены на kit config= DI; test_avito_imv_own_session_receives_proxies удалён — дублирует test_avito_imv_kit_parity.py footgun-тесты) - tests/test_yandex_scrapers_delay_wiring.py (yandex_detail/yandex_valuation → kit delay_provider=; yandex_realty/yandex_newbuilding не тронуты) - tests/test_scraper_kit_group_c_backfill_kit_parity.py (docstring уточнение, без функциональных изменений) Полный backend-suite зелёный (3253 passed, 6 skipped) кроме известного pre-existing flake test_search_api.py::test_search_cache_hit (#2208, не регрессия этого PR). ruff check + ruff format — чисто на всех изменённых файлах.
99 lines
4.4 KiB
Python
99 lines
4.4 KiB
Python
"""Parity-proof: admin.py Yandex debug-роуты (#2305, Group A) — legacy vs scraper_kit.
|
||
|
||
`app/api/v1/admin.py::scrape_yandex_detail` / `scrape_yandex_valuation` теперь
|
||
зовут `scraper_kit.providers.yandex.{detail,valuation}` вместо
|
||
`app.services.scrapers.yandex_{detail,valuation}` (тот же `/scrape` root-роут —
|
||
`scraper_kit.providers.yandex.serp.YandexRealtyScraper`). Каждая функция ниже —
|
||
чистое, offline-тестируемое parsing-ядро (см. `tests/support/README.md`:
|
||
"Live network/DB в parity-тестах избегайте").
|
||
|
||
- `_entity_to_lot` — `/scrape` root-роут → `YandexRealtyScraper.fetch_around`
|
||
конвертирует gate-API entity → ScrapedLot этой функцией (также доказывает
|
||
ScrapedLot-контракт, общий с `save_listings`/`scripts/ingest_domclick_jsonl.py`).
|
||
|
||
Легаси `app.services.scrapers.yandex_detail` / `yandex_valuation` удалены (#2277
|
||
финальный шаг scraper_kit-миграции, 0 runtime importers) — `_parse_card_fields` /
|
||
`_parse_house_meta` parity-проверки убраны отсюда вместе с ними; kit-side coverage
|
||
для обеих функций сохранена standalone в `tests/test_yandex_detail_structural.py` /
|
||
`tests/test_yandex_valuation.py`. `yandex_realty` остаётся легаси (не мигрирован) —
|
||
его `_entity_to_lot` parity-проверка не тронута.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import os
|
||
|
||
os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://test:test@localhost/test_db")
|
||
|
||
from scraper_kit.providers.yandex.serp import _entity_to_lot as kit_entity_to_lot
|
||
|
||
from app.services.scrapers.yandex_realty import _entity_to_lot as legacy_entity_to_lot
|
||
from tests.support.parity import assert_parity
|
||
|
||
# gate-API entity (realty.yandex.ru), без creationDate/author — исключает
|
||
# time-dependent поля (days_on_market) из фикстуры (deterministic offline).
|
||
_YANDEX_ENTITY = {
|
||
"offerId": "12345",
|
||
"url": "//realty.yandex.ru/offer/12345",
|
||
"price": {"value": 8500000, "valuePerPart": 157000},
|
||
"area": {"value": 54.3},
|
||
"livingSpace": {"value": 32.1},
|
||
"kitchenSpace": {"value": 9.5},
|
||
"roomsTotal": 2,
|
||
"floorsOffered": [5],
|
||
"floorsTotal": 9,
|
||
"ceilingHeight": 2.7,
|
||
"building": {"builtYear": 2015, "buildingType": "MONOLIT"},
|
||
"location": {
|
||
"point": {"latitude": 56.83, "longitude": 60.6},
|
||
"geocoderAddress": "Екатеринбург, ул. Тестовая, 1",
|
||
},
|
||
"mainImages": ["//avatars.mds.yandex.net/img1.jpg"],
|
||
}
|
||
|
||
|
||
def test_yandex_entity_to_lot_are_class_distinct() -> None:
|
||
"""Sanity: legacy/kit ScrapedLot — разные классы (pydantic), обычный `==` даёт False."""
|
||
legacy_result = legacy_entity_to_lot(_YANDEX_ENTITY)
|
||
kit_result = kit_entity_to_lot(_YANDEX_ENTITY)
|
||
|
||
assert legacy_result is not None
|
||
assert kit_result is not None
|
||
assert type(legacy_result) is not type(kit_result)
|
||
assert legacy_result.model_dump() == kit_result.model_dump()
|
||
|
||
|
||
def test_parity_yandex_serp_entity_to_lot() -> None:
|
||
"""`/scrape` root-роут (admin.py) → YandexRealtyScraper.fetch_around → _entity_to_lot.
|
||
|
||
Также доказывает ScrapedLot-контракт, общий с `save_listings`
|
||
(используется и `/scrape`, и `scripts/ingest_domclick_jsonl.py`).
|
||
"""
|
||
assert_parity(
|
||
legacy_fn=legacy_entity_to_lot,
|
||
kit_fn=kit_entity_to_lot,
|
||
fixtures=[(_YANDEX_ENTITY,)],
|
||
)
|
||
|
||
|
||
def test_parity_harness_catches_yandex_entity_divergence() -> None:
|
||
"""Harness реально ЛОВИТ расхождение — мутируем price_rub в kit-результате."""
|
||
import pytest
|
||
|
||
from tests.support.parity import ParityMismatchError
|
||
|
||
legacy_result = legacy_entity_to_lot(_YANDEX_ENTITY)
|
||
kit_result = kit_entity_to_lot(_YANDEX_ENTITY)
|
||
assert legacy_result is not None
|
||
assert kit_result is not None
|
||
|
||
# ScrapedLot — pydantic BaseModel (НЕ dataclass) → model_copy, не dataclasses.replace.
|
||
mutated_kit_result = kit_result.model_copy(update={"price_rub": kit_result.price_rub + 1})
|
||
|
||
with pytest.raises(ParityMismatchError) as exc_info:
|
||
assert_parity(
|
||
legacy_fn=lambda: legacy_result,
|
||
kit_fn=lambda: mutated_kit_result,
|
||
fixtures=[()],
|
||
)
|
||
assert "price_rub" in str(exc_info.value)
|