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 — чисто на всех изменённых файлах.
120 lines
5.5 KiB
Python
120 lines
5.5 KiB
Python
"""Issue #2336 (Group E3 of scraper_kit migration epic #2277/#2308).
|
|
|
|
`scraper_kit.providers.yandex.valuation.YandexValuationScraper` is already a
|
|
strangler-copy (#2133) of the (now deleted, #2277 final step)
|
|
`app.services.scrapers.yandex_valuation`, and already used via the kit path in
|
|
`admin.py::scrape_yandex_valuation` (#2305, Group A) with the established DI
|
|
convention: `config=RealScraperConfig()`, `delay_provider=get_scraper_delay`,
|
|
`proxy_provider=_kit_proxy_provider()`.
|
|
|
|
This file proves #2336's two confirmed risk patterns for any future call site that
|
|
constructs the kit scraper directly, so a regression is caught in CI rather than
|
|
silently in prod:
|
|
|
|
1. **Mandatory `config: ScraperConfig`** (same shape as the cian_valuation footgun,
|
|
#2335) — omitting it raises `TypeError`.
|
|
|
|
2. **Optional `delay_provider`** — if omitted, silently falls back to the hardcoded
|
|
class-default `request_delay_sec = 5.0`, discarding whatever an admin configured via
|
|
the DB-backed `scraper_settings` table (`get_scraper_delay`, cached 60s,
|
|
`max(per_source, global)`). No exception, no log — just quietly weaker anti-ban
|
|
throttling if an admin had raised it after ban trouble.
|
|
|
|
Legacy-vs-kit parity coverage (previously here, using
|
|
`app.services.scrapers.yandex_valuation`) was removed together with the legacy module
|
|
deletion — `.parse()` correctness itself is still covered kit-only by
|
|
`tests/test_yandex_valuation.py` / `tests/test_yandex_valuation_save.py`.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
|
|
# app.services.scraper_adapters imports app.core.config.settings=Settings(), which
|
|
# requires DATABASE_URL. Offline construction never touches a real DB — a fake DSN
|
|
# is enough.
|
|
os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://test:test@localhost/test_db")
|
|
|
|
from scraper_kit.providers.yandex.valuation import (
|
|
YandexValuationScraper as KitYandexValuationScraper,
|
|
)
|
|
|
|
from app.services import scraper_settings
|
|
from app.services.scraper_adapters import RealScraperConfig
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Footgun #1 — mandatory `config`
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_kit_valuation_requires_config() -> None:
|
|
"""Omitting `config=` raises TypeError — documents the mandatory-DI contract.
|
|
|
|
Guards the exact risk flagged for #2336: if a future call site (#2337, switching
|
|
estimator.py to the kit class) forgets `config=`, this must fail loud in CI, not
|
|
silently in prod inside estimator.py's blanket except-block.
|
|
"""
|
|
with pytest.raises(TypeError):
|
|
KitYandexValuationScraper() # type: ignore[call-arg]
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Footgun #2 — `delay_provider` silently falls back to hardcoded 5.0s
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_kit_valuation_without_delay_provider_uses_hardcoded_default() -> None:
|
|
"""No `delay_provider=` -> `request_delay_sec` stays at the class-default 5.0s.
|
|
|
|
This is the regression this test guards: a real call site that forgets to wire
|
|
`delay_provider=get_scraper_delay` silently reverts an admin-tuned anti-ban delay
|
|
(e.g. raised after a ban) to 5.0s -- no error, no log, just quietly weaker
|
|
throttling.
|
|
"""
|
|
scraper = KitYandexValuationScraper(config=RealScraperConfig())
|
|
assert scraper.request_delay_sec == 5.0
|
|
assert KitYandexValuationScraper.request_delay_sec == 5.0 # class default untouched
|
|
|
|
|
|
def test_kit_valuation_with_delay_provider_uses_configured_value() -> None:
|
|
"""`delay_provider=get_scraper_delay` -> instance picks up whatever it returns.
|
|
|
|
Controls the return value via patch so the assertion doesn't depend on a real DB
|
|
row / DB availability -- this proves the WIRING (delay_provider is actually called
|
|
and its return value actually lands in request_delay_sec), not
|
|
`scraper_settings`'s own DB-fallback behaviour (covered elsewhere, e.g.
|
|
`tests/test_yandex_scrapers_delay_wiring.py` for the legacy module).
|
|
"""
|
|
with patch("app.services.scraper_settings.get_scraper_delay", return_value=12.5) as mock_delay:
|
|
# Attribute lookup on the MODULE (not the bare name imported at file-top)
|
|
# so this resolves to the patched mock, not the original function object
|
|
# `from ... import get_scraper_delay` would have bound at import time.
|
|
scraper = KitYandexValuationScraper(
|
|
config=RealScraperConfig(), delay_provider=scraper_settings.get_scraper_delay
|
|
)
|
|
mock_delay.assert_called_once_with("yandex_valuation")
|
|
assert scraper.request_delay_sec == 12.5
|
|
assert scraper.request_delay_sec != 5.0 # explicitly NOT the hardcoded fallback
|
|
|
|
|
|
def test_kit_valuation_delay_provider_receives_scraper_name() -> None:
|
|
"""delay_provider is invoked with `self.name` ('yandex_valuation'), not a typo'd string.
|
|
|
|
`get_scraper_delay` aliases 'yandex_valuation' to the shared 'yandex' DB row today
|
|
(`_KEY_ALIASES` in scraper_settings.py), but a hardcoded/mismatched source string
|
|
passed at the call site would be a latent bug if that alias ever changes.
|
|
"""
|
|
calls: list[str] = []
|
|
|
|
def _spy_delay_provider(source: str) -> float:
|
|
calls.append(source)
|
|
return 9.0
|
|
|
|
scraper = KitYandexValuationScraper(
|
|
config=RealScraperConfig(), delay_provider=_spy_delay_provider
|
|
)
|
|
assert calls == ["yandex_valuation"]
|
|
assert scraper.request_delay_sec == 9.0
|