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
Group A of the scraper_kit migration epic (#2277). Switches admin.py's manual "run parser" debug endpoints and scripts/ingest_domclick_jsonl.py from direct app.services.scrapers.* imports to their scraper_kit.providers.* equivalents, using the DI adapters (RealScraperConfig/RealMatcherAdapter/RealProxyProvider, app.services.scraper_settings.get_scraper_delay) already established by app.scheduler_main._run_kit_scheduler. Migrated: /scrape (AvitoScraper/CianScraper/YandexRealtyScraper + save_listings), scrape_avito_house, scrape_avito_detail, scrape_avito_imv, scrape_yandex_detail, scrape_yandex_valuation, scrape_cian_detail, cian_auto_login's BrowserFetcher. scripts/ingest_domclick_jsonl.py: ScrapedLot/save_listings + (now that #2307/ Group D ported a kit equivalent while this was in flight) DomClickDetailEnrichment/ save_detail_enrichment. Deliberately NOT migrated (documented in admin.py): scrape_yandex_newbuilding / scrape_cian_newbuilding — their kit equivalents (providers/{yandex,cian}/newbuilding.py) construct an internal BrowserFetcher(...) without the mandatory `endpoint` kwarg, so any call crashes/silently-fails regardless of caller-side DI. Bug lives in scraper_kit provider code, out of scope here (only consuming, not touching provider logic) — flagged as follow-up. Parity proven via tests/support/parity.py (assert_parity) against the exact functions each debug route now calls, on offline fixtures — no live network/DB. Refs #2305
129 lines
5.3 KiB
Python
129 lines
5.3 KiB
Python
"""Parity-proof: admin.py Avito debug-роуты (#2305, Group A) — legacy vs scraper_kit.
|
||
|
||
`app/api/v1/admin.py::scrape_avito_house` / `scrape_avito_imv` теперь зовут
|
||
`scraper_kit.providers.avito.{houses,imv}` вместо `app.services.scrapers.avito_*`
|
||
(тот же `/scrape` root-роут — `scraper_kit.providers.avito.serp.AvitoScraper`).
|
||
Каждая пара функций ниже — чистое, offline-тестируемое parsing-ядро, которое
|
||
реально используется соответствующим debug-эндпоинтом (не сам HTTP-fetch —
|
||
см. `tests/support/README.md`: "Live network/DB в parity-тестах избегайте").
|
||
|
||
- `_parse_house_page` — `scrape_avito_house` → `fetch_house_catalog` парсит виджет
|
||
`housePage` этой функцией.
|
||
- `_parse_price` — `scrape_avito_imv` → `evaluate_via_imv` парсит top-level `price`
|
||
блок ответа `realty-imv/get-data` этой функцией.
|
||
- `_extract_rooms_from_title` / `_extract_area_from_title` — использует
|
||
`AvitoScraper` (SERP-парсинг `/scrape` root-роута) для title-fallback полей.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import dataclasses
|
||
import os
|
||
|
||
# Соседние модули app.services.scrapers.* импортируют app.core.config при импорте
|
||
# пакета — фиктивный DSN мостит офлайн-запуск (mirror test_avito_detail_kit_parity.py).
|
||
os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://test:test@localhost/test_db")
|
||
|
||
from scraper_kit.providers.avito.houses import _parse_house_page as kit_parse_house_page
|
||
from scraper_kit.providers.avito.imv import _parse_price as kit_parse_price
|
||
from scraper_kit.providers.avito.serp import (
|
||
_extract_area_from_title as kit_extract_area_from_title,
|
||
)
|
||
from scraper_kit.providers.avito.serp import (
|
||
_extract_rooms_from_title as kit_extract_rooms_from_title,
|
||
)
|
||
|
||
from app.services.scrapers.avito import _extract_area_from_title as legacy_extract_area_from_title
|
||
from app.services.scrapers.avito import (
|
||
_extract_rooms_from_title as legacy_extract_rooms_from_title,
|
||
)
|
||
from app.services.scrapers.avito_houses import _parse_house_page as legacy_parse_house_page
|
||
from app.services.scrapers.avito_imv import _parse_price as legacy_parse_price
|
||
from tests.support.parity import assert_parity
|
||
|
||
_HOUSE_WIDGET = {
|
||
"props": {
|
||
"developmentData": {
|
||
"avitoId": 3171365,
|
||
"id": "abc123hash",
|
||
"title": "ЖК Тестовый",
|
||
"address": "ул. Тестовая, 1",
|
||
"fullAddress": "Екатеринбург, ул. Тестовая, 1",
|
||
"coords": {"lat": 56.8, "lng": 60.6},
|
||
"aboutDevelopment": {
|
||
"expandParams": {
|
||
"items": [
|
||
{
|
||
"params": [
|
||
{"type": "Год постройки", "value": "2020"},
|
||
{"type": "Этажей", "value": "16"},
|
||
{"type": "Пассажирский лифт", "value": "2"},
|
||
]
|
||
}
|
||
]
|
||
}
|
||
},
|
||
"ratingBadge": {"info": {"score": 4.5, "scoreString": "4.5"}},
|
||
"ratingSummaryStat": {"reviewCount": 10, "ratingStat": []},
|
||
"mapPreview": {"objects": "школа рядом", "distance": "5 мин", "pins": []},
|
||
"developer": {"name": "ООО Застройщик", "key": "dev1"},
|
||
}
|
||
}
|
||
}
|
||
|
||
_IMV_GETDATA = {
|
||
"price": {
|
||
"recommendedPrice": 8500000,
|
||
"lowerPrice": 7800000,
|
||
"higherPrice": 9200000,
|
||
"count": 42,
|
||
"addItemUrl": "/add",
|
||
}
|
||
}
|
||
|
||
|
||
def test_avito_house_page_parse_are_class_distinct() -> None:
|
||
"""Sanity: legacy/kit HouseInfo — разные классы, обычный `==` даёт False."""
|
||
legacy_result = legacy_parse_house_page(_HOUSE_WIDGET)
|
||
kit_result = kit_parse_house_page(_HOUSE_WIDGET)
|
||
|
||
assert type(legacy_result) is not type(kit_result)
|
||
assert legacy_result != kit_result
|
||
assert dataclasses.asdict(legacy_result) == dataclasses.asdict(kit_result)
|
||
|
||
|
||
def test_parity_avito_house_page() -> None:
|
||
"""`scrape_avito_house` (admin.py) → fetch_house_catalog → _parse_house_page."""
|
||
assert_parity(
|
||
legacy_fn=legacy_parse_house_page,
|
||
kit_fn=kit_parse_house_page,
|
||
fixtures=[(_HOUSE_WIDGET,)],
|
||
)
|
||
|
||
|
||
def test_parity_avito_imv_price() -> None:
|
||
"""`scrape_avito_imv` (admin.py) → evaluate_via_imv → _parse_price."""
|
||
assert_parity(
|
||
legacy_fn=legacy_parse_price,
|
||
kit_fn=kit_parse_price,
|
||
fixtures=[(_IMV_GETDATA,)],
|
||
)
|
||
|
||
|
||
def test_parity_avito_serp_title_extract() -> None:
|
||
"""`/scrape` root-роут (admin.py) → AvitoScraper SERP title-fallback fields."""
|
||
titles = [
|
||
"2-к. квартира, 54.3 м², 3/9 эт.",
|
||
"Студия, 25 м², 1/5 эт.",
|
||
"3-к. кв., 75,5 м², 12/16 эт.",
|
||
]
|
||
assert_parity(
|
||
legacy_fn=legacy_extract_rooms_from_title,
|
||
kit_fn=kit_extract_rooms_from_title,
|
||
fixtures=titles,
|
||
)
|
||
assert_parity(
|
||
legacy_fn=legacy_extract_area_from_title,
|
||
kit_fn=kit_extract_area_from_title,
|
||
fixtures=titles,
|
||
)
|