From 7800f6014a054fd48189be6e775852db547ef6ac Mon Sep 17 00:00:00 2001 From: bot-backend Date: Sat, 4 Jul 2026 09:23:36 +0300 Subject: [PATCH] refactor(tradein/scraper-kit): yandex providers on shared _base.py (#2363) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit F4c-yandex (epic #2277 Group F, parent #2352): migrate the SERP scraper's BrowserFetcher construction onto build_browser_fetcher() from the Foundation module (#2358, providers/_base.py) instead of local ad-hoc construction. - yandex/serp.py::__aenter__ now calls build_browser_fetcher(config, source=, proxy_provider=, fetch_timeout_s=) instead of constructing BrowserFetcher(...) directly. Preserves the existing fetch_timeout_s=30 (see caveat below), endpoint/proxy_provider/use_pool resolution byte-for-byte. The retry/tarpit logic in fetch_around() (rotate_ip + sleep on status==0/JSON-error) is untouched, per the Foundation module's documented exclusion. Left unchanged (deliberate, not an oversight): - yandex/serp.py::_rotate_ip's curl_cffi AsyncSession(timeout=30) — this hits the mobile-proxy provider's own changeip API, not Yandex; it intentionally carries no impersonate/headers/proxy (same as the mirrored avito/serp.py rotate-ip session). Not listed among the _base.py docstring's documented extraction sites -- consistent exclusion, not a new one. - yandex/detail.py -- no BrowserFetcher/curl_cffi construction exists in this file (uses BaseScraper's httpx-based _http_get). Nothing to migrate. - yandex/newbuilding.py -- both BrowserFetcher(...) call sites take an OPTIONAL `config: ScraperConfig | None`, a genuinely different contract from build_browser_fetcher's mandatory `config`. Migrating would require making config mandatory (breaking admin.py:1511, yandex_newbuilding_sweep.py:353, and several tests that call without config -- out of scope for a call-site swap) and breaks test_scraper_kit_newbuilding_endpoint.py's "without config, endpoint=None" regression case (its SimpleNamespace mock lacks use_proxy_pool_browser, and the mock config path can never satisfy a mandatory-config helper). Left untouched; the class-bug #2322/#2330 this file already fixed (config now threaded, just not via the shared helper) remains fixed. Also updates test_kit_serp_proxy_pool.py's 3 yandex-specific tests: they monkeypatched the module-local `yandex_serp.BrowserFetcher` name, which the migrated call path no longer references directly (it now goes through scraper_kit.providers._base.build_browser_fetcher -> _base's own BrowserFetcher import). Patch target moved to scraper_kit.providers._base.BrowserFetcher; every assertion is unchanged -- this adapts the mock to the new (intentional) call path, it does not weaken what the test verifies. Cian's tests in the same file are untouched (cian/serp.py is not migrated yet, separate F4b issue). --- .../backend/tests/test_kit_serp_proxy_pool.py | 26 ++++++++++++------- .../src/scraper_kit/providers/yandex/serp.py | 12 ++++++--- 2 files changed, 24 insertions(+), 14 deletions(-) diff --git a/tradein-mvp/backend/tests/test_kit_serp_proxy_pool.py b/tradein-mvp/backend/tests/test_kit_serp_proxy_pool.py index 74847e65..8352d77d 100644 --- a/tradein-mvp/backend/tests/test_kit_serp_proxy_pool.py +++ b/tradein-mvp/backend/tests/test_kit_serp_proxy_pool.py @@ -11,13 +11,19 @@ proxy_provider + use_pool=config.use_proxy_pool_browser в BrowserFetcher. - proxy_provider задан + флаг on → BrowserFetcher получает провайдер + use_pool=True. BrowserFetcher монки-патчится на recording-заглушку, так что реального HTTP к -tradein-browser не происходит. Yandex SERP всё ещё конструирует BrowserFetcher -напрямую в своём модуле — патчим `yandex_serp.BrowserFetcher`. Cian SERP -(#2362, F4b) переведён на общую фабрику `scraper_kit.providers._base. -build_browser_fetcher()` — конструктор теперь вызывается ИЗ _base.py, поэтому -патчим `BrowserFetcher` там, а не в `cian_serp` (там больше нет своей ссылки на -класс). Проверяемый инвариант (какие kwargs долетают до BrowserFetcher) -не меняется — меняется только точка патча, отражающая реальное место вызова. +tradein-browser не происходит. + +После F4-миграции (#2362 F4b cian + #2363 F4c yandex) ОБА скрапера +(`yandex/serp.py::__aenter__` и `cian/serp.py::__aenter__`) больше не конструируют +`BrowserFetcher(...)` напрямую — оба вызывают shared `build_browser_fetcher()` из +`scraper_kit.providers._base`, который ВНУТРИ себя ссылается на СВОЙ собственный +импорт `BrowserFetcher`. Патч локального имени `yandex_serp.BrowserFetcher` / +`cian_serp.BrowserFetcher` больше не перехватывает вызов (реальный +`BrowserFetcher.__init__` конструировался бы, а `calls` оставался пуст) — поэтому +для обоих провайдеров патчим `scraper_kit.providers._base.BrowserFetcher` +(реальную зависимость `build_browser_fetcher`), сохраняя все assertions как есть. +Проверяемый инвариант (какие kwargs долетают до BrowserFetcher) не меняется — +меняется только точка патча, отражающая реальное место вызова. """ from __future__ import annotations @@ -91,7 +97,7 @@ def _config(*, use_pool: bool) -> types.SimpleNamespace: async def test_yandex_serp_wires_pool_when_flag_on(monkeypatch: pytest.MonkeyPatch) -> None: """proxy_provider + флаг on → BrowserFetcher получает провайдер + use_pool=True.""" rec_fetcher, calls = _make_recorder() - monkeypatch.setattr(yandex_serp, "BrowserFetcher", rec_fetcher) + monkeypatch.setattr("scraper_kit.providers._base.BrowserFetcher", rec_fetcher) provider = _FakeProxyProvider() scraper = yandex_serp.YandexRealtyScraper(_config(use_pool=True), proxy_provider=provider) @@ -108,7 +114,7 @@ async def test_yandex_serp_wires_pool_when_flag_on(monkeypatch: pytest.MonkeyPat async def test_yandex_serp_no_pool_by_default(monkeypatch: pytest.MonkeyPatch) -> None: """Без provider/флага → BrowserFetcher(proxy_provider=None, use_pool=False) — ship-dark.""" rec_fetcher, calls = _make_recorder() - monkeypatch.setattr(yandex_serp, "BrowserFetcher", rec_fetcher) + monkeypatch.setattr("scraper_kit.providers._base.BrowserFetcher", rec_fetcher) scraper = yandex_serp.YandexRealtyScraper(_config(use_pool=False)) async with scraper: @@ -125,7 +131,7 @@ async def test_yandex_serp_provider_without_flag_stays_dark( ) -> None: """provider задан, но флаг off → use_pool=False (BrowserFetcher пул не трогает).""" rec_fetcher, calls = _make_recorder() - monkeypatch.setattr(yandex_serp, "BrowserFetcher", rec_fetcher) + monkeypatch.setattr("scraper_kit.providers._base.BrowserFetcher", rec_fetcher) provider = _FakeProxyProvider() scraper = yandex_serp.YandexRealtyScraper(_config(use_pool=False), proxy_provider=provider) diff --git a/tradein-mvp/packages/scraper-kit/src/scraper_kit/providers/yandex/serp.py b/tradein-mvp/packages/scraper-kit/src/scraper_kit/providers/yandex/serp.py index 6781e640..8a42a6f1 100644 --- a/tradein-mvp/packages/scraper-kit/src/scraper_kit/providers/yandex/serp.py +++ b/tradein-mvp/packages/scraper-kit/src/scraper_kit/providers/yandex/serp.py @@ -51,6 +51,7 @@ from scraper_kit.browser_fetcher import BrowserFetcher from scraper_kit.house_type_normalizer import normalize_house_type from scraper_kit.price_brackets import get_price_seed_brackets from scraper_kit.pricing import BisectionConfig, ProbeFailPolicy, ProbeResult, walk_price_range +from scraper_kit.providers._base import build_browser_fetcher if TYPE_CHECKING: from scraper_kit.contracts import ProxyProvider, ScraperConfig @@ -506,13 +507,16 @@ class YandexRealtyScraper(BaseScraper): fetch_timeout_s=_YANDEX_BROWSER_FETCH_TIMEOUT_S (30s) ограничивает потери на один тайм-аутовый запрос: worst-case 30s×(1+retries)=90s на combo вместо 360s. + + Собрано через shared `build_browser_fetcher()` (#2358 Foundation, #2363 + миграция) — endpoint/use_pool берутся из `config` mandatory, footgun-класс + #2322/#2330 (пропущенный endpoint/use_pool) закрыт структурно. """ - self._browser = BrowserFetcher( + self._browser = build_browser_fetcher( + self._config, source="yandex", - fetch_timeout_s=_YANDEX_BROWSER_FETCH_TIMEOUT_S, - endpoint=self._config.browser_http_endpoint, proxy_provider=self._proxy_provider, - use_pool=self._config.use_proxy_pool_browser, + fetch_timeout_s=_YANDEX_BROWSER_FETCH_TIMEOUT_S, ) await self._browser.__aenter__() return self -- 2.45.3