Скрапер: newbuilding-провайдеры без config больше не уходят мимо пула прокси, IMV Авито строит сессию общей фабрикой #3562

Merged
bot-backend merged 2 commits from fix/kit-providers-config into main 2026-09-17 09:23:58 +00:00
6 changed files with 74 additions and 102 deletions
Showing only changes of commit eade88bc66 - Show all commits

View file

@ -15,6 +15,7 @@ from __future__ import annotations
import os import os
import sys import sys
from pathlib import Path from pathlib import Path
from types import SimpleNamespace
from unittest.mock import AsyncMock, MagicMock from unittest.mock import AsyncMock, MagicMock
# DATABASE_URL required by config before any app import. # DATABASE_URL required by config before any app import.
@ -28,6 +29,8 @@ from scraper_kit.providers.cian.newbuilding import ( # noqa: E402
resolve_cian_zhk_url_via_search, resolve_cian_zhk_url_via_search,
) )
# config обязателен (#2385); на пути с переданной session он не читается.
_CONFIG = SimpleNamespace(cian_proxy_url=None)
_FIXTURE = Path(__file__).resolve().parents[2] / "fixtures" / "cian_catph_newbuilding_serp.html" _FIXTURE = Path(__file__).resolve().parents[2] / "fixtures" / "cian_catph_newbuilding_serp.html"
@ -114,7 +117,7 @@ def test_extract_zhk_url_no_match_returns_none() -> None:
async def test_resolve_via_search_happy_path() -> None: async def test_resolve_via_search_happy_path() -> None:
"""HTTP 200 + a SERP containing the slug → canonical url; session reused, not closed.""" """HTTP 200 + a SERP containing the slug → canonical url; session reused, not closed."""
session = _mock_session(status_code=200, text=_serp_fixture()) session = _mock_session(status_code=200, text=_serp_fixture())
url = await resolve_cian_zhk_url_via_search(48853, session=session) url = await resolve_cian_zhk_url_via_search(48853, config=_CONFIG, session=session)
assert url == "https://zhk-parkovyy-kvartal-ekb-i.cian.ru" assert url == "https://zhk-parkovyy-kvartal-ekb-i.cian.ru"
# caller owns a passed-in session — resolver must NOT close it. # caller owns a passed-in session — resolver must NOT close it.
session.close.assert_not_awaited() session.close.assert_not_awaited()
@ -128,14 +131,14 @@ async def test_resolve_via_search_happy_path() -> None:
async def test_resolve_via_search_no_match_returns_none() -> None: async def test_resolve_via_search_no_match_returns_none() -> None:
"""HTTP 200 but no zhk-slug in the body → None (empty SERP / markup drift).""" """HTTP 200 but no zhk-slug in the body → None (empty SERP / markup drift)."""
session = _mock_session(status_code=200, text="<html>no results</html>") session = _mock_session(status_code=200, text="<html>no results</html>")
assert await resolve_cian_zhk_url_via_search(999, session=session) is None assert await resolve_cian_zhk_url_via_search(999, config=_CONFIG, session=session) is None
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_resolve_via_search_non_200_returns_none() -> None: async def test_resolve_via_search_non_200_returns_none() -> None:
"""A non-200 (block / 404) → None without attempting extraction.""" """A non-200 (block / 404) → None without attempting extraction."""
session = _mock_session(status_code=429, text="too many requests") session = _mock_session(status_code=429, text="too many requests")
assert await resolve_cian_zhk_url_via_search(48853, session=session) is None assert await resolve_cian_zhk_url_via_search(48853, config=_CONFIG, session=session) is None
@pytest.mark.asyncio @pytest.mark.asyncio
@ -144,4 +147,4 @@ async def test_resolve_via_search_swallows_request_error() -> None:
session = MagicMock() session = MagicMock()
session.get = AsyncMock(side_effect=ConnectionError("proxy refused")) session.get = AsyncMock(side_effect=ConnectionError("proxy refused"))
session.close = AsyncMock() session.close = AsyncMock()
assert await resolve_cian_zhk_url_via_search(48853, session=session) is None assert await resolve_cian_zhk_url_via_search(48853, config=_CONFIG, session=session) is None

View file

@ -23,6 +23,10 @@ through `build_curl_cffi_session` — so a missing `config=` does NOT raise like
(non-proxied) connection (`cian_proxy_url` dropped). That is much harder to notice in (non-proxied) connection (`cian_proxy_url` dropped). That is much harder to notice in
prod (no crash, just quietly unproxied traffic that anti-bot can throttle/block), so a prod (no crash, just quietly unproxied traffic that anti-bot can throttle/block), so a
caller that forgets `config=` here gets no signal at all. caller that forgets `config=` here gets no signal at all.
#2385: `config` стал обязательным во всех четырёх точках входа. Прежние кейсы
`*_without_config` (ждали endpoint=None / proxy_url=None) заменены одним тестом
«без config TypeError, фетчер и сессия не строятся».
""" """
from __future__ import annotations from __future__ import annotations
@ -42,6 +46,7 @@ from scraper_kit.providers.yandex.newbuilding import (
_TEST_ENDPOINT = "http://test-tradein-browser:9009/fetch" _TEST_ENDPOINT = "http://test-tradein-browser:9009/fetch"
_TEST_PROXY = "http://mobile-proxy.example:8080" _TEST_PROXY = "http://mobile-proxy.example:8080"
_CONFIG = SimpleNamespace(browser_http_endpoint=_TEST_ENDPOINT, use_proxy_pool_browser=False)
def _spy_browser_fetcher(html: str) -> MagicMock: def _spy_browser_fetcher(html: str) -> MagicMock:
@ -77,21 +82,6 @@ async def test_cian_fetch_newbuilding_passes_endpoint_from_config(monkeypatch):
assert kwargs.get("endpoint") == _TEST_ENDPOINT assert kwargs.get("endpoint") == _TEST_ENDPOINT
@pytest.mark.asyncio
async def test_cian_fetch_newbuilding_endpoint_none_without_config(monkeypatch):
"""Без config= (backward-compat) endpoint=None передаётся явным kwarg'ом — сигнатура
больше не роняет TypeError на отсутствующем обязательном параметре конструктора."""
spy = _spy_browser_fetcher("<html></html>")
monkeypatch.setattr("scraper_kit.providers.cian.newbuilding.BrowserFetcher", spy)
await fetch_newbuilding("https://zhk-test-ekb-i.cian.ru/")
spy.assert_called_once()
_, kwargs = spy.call_args
assert kwargs.get("source") == "cian"
assert kwargs.get("endpoint") is None
# ── yandex.newbuilding.YandexNewbuildingScraper.fetch_jk ───────────────────── # ── yandex.newbuilding.YandexNewbuildingScraper.fetch_jk ─────────────────────
@ -115,21 +105,6 @@ async def test_yandex_fetch_jk_passes_endpoint_from_config(monkeypatch):
assert kwargs.get("endpoint") == _TEST_ENDPOINT assert kwargs.get("endpoint") == _TEST_ENDPOINT
@pytest.mark.asyncio
async def test_yandex_fetch_jk_endpoint_none_without_config(monkeypatch):
"""Без config= (backward-compat) endpoint=None передаётся явным kwarg'ом."""
spy = _spy_browser_fetcher("<html></html>")
monkeypatch.setattr("scraper_kit.providers.yandex.newbuilding.BrowserFetcher", spy)
scraper = YandexNewbuildingScraper()
await scraper.fetch_jk(jk_slug="tatlin", jk_id="1592987")
spy.assert_called_once()
_, kwargs = spy.call_args
assert kwargs.get("source") == "yandex"
assert kwargs.get("endpoint") is None
# ── yandex.newbuilding.resolve_yandex_jk_slug ──────────────────────────────── # ── yandex.newbuilding.resolve_yandex_jk_slug ────────────────────────────────
@ -151,20 +126,6 @@ async def test_resolve_yandex_jk_slug_passes_endpoint_from_config(monkeypatch):
assert kwargs.get("endpoint") == _TEST_ENDPOINT assert kwargs.get("endpoint") == _TEST_ENDPOINT
@pytest.mark.asyncio
async def test_resolve_yandex_jk_slug_endpoint_none_without_config(monkeypatch):
"""Без config= (backward-compat) endpoint=None передаётся явным kwarg'ом."""
spy = _spy_browser_fetcher("<html></html>")
monkeypatch.setattr("scraper_kit.providers.yandex.newbuilding.BrowserFetcher", spy)
await resolve_yandex_jk_slug("1592987")
spy.assert_called_once()
_, kwargs = spy.call_args
assert kwargs.get("source") == "yandex"
assert kwargs.get("endpoint") is None
# ── #2860: slug ищется по СТРАНИЦЕ ЖК, а не по фильтру выдачи ──────────────── # ── #2860: slug ищется по СТРАНИЦЕ ЖК, а не по фильтру выдачи ────────────────
# #
# Замер 19.08.2026 через браузерный сайдкар прода: `?siteId=<id>` Яндексом # Замер 19.08.2026 через браузерный сайдкар прода: `?siteId=<id>` Яндексом
@ -195,9 +156,9 @@ async def test_resolve_yandex_jk_slug_requests_jk_page_not_serp_filter(monkeypat
Это и есть суть правки: адрес запроса, а не разбор ответа. Это и есть суть правки: адрес запроса, а не разбор ответа.
""" """
spy = _spy_browser_fetcher(_JK_PAGE_HTML) spy = _spy_browser_fetcher(_JK_PAGE_HTML)
monkeypatch.setattr("scraper_kit.providers.yandex.newbuilding.BrowserFetcher", spy) monkeypatch.setattr("scraper_kit.providers._base.BrowserFetcher", spy)
await resolve_yandex_jk_slug("286394") await resolve_yandex_jk_slug("286394", config=_CONFIG)
url = spy.return_value.fetch.call_args[0][0] url = spy.return_value.fetch.call_args[0][0]
assert "siteId=" not in url, f"фильтр выдачи больше не работает, а запрос идёт туда: {url}" assert "siteId=" not in url, f"фильтр выдачи больше не работает, а запрос идёт туда: {url}"
@ -208,9 +169,9 @@ async def test_resolve_yandex_jk_slug_requests_jk_page_not_serp_filter(monkeypat
async def test_resolve_yandex_jk_slug_reads_real_slug_from_page(monkeypatch): async def test_resolve_yandex_jk_slug_reads_real_slug_from_page(monkeypatch):
"""Со страницы ЖК берётся настоящий slug, а не плейсхолдер из нашего же URL.""" """Со страницы ЖК берётся настоящий slug, а не плейсхолдер из нашего же URL."""
spy = _spy_browser_fetcher(_JK_PAGE_HTML) spy = _spy_browser_fetcher(_JK_PAGE_HTML)
monkeypatch.setattr("scraper_kit.providers.yandex.newbuilding.BrowserFetcher", spy) monkeypatch.setattr("scraper_kit.providers._base.BrowserFetcher", spy)
assert await resolve_yandex_jk_slug("286394") == "uspenskij" assert await resolve_yandex_jk_slug("286394", config=_CONFIG) == "uspenskij"
@pytest.mark.asyncio @pytest.mark.asyncio
@ -221,9 +182,9 @@ async def test_resolve_yandex_jk_slug_returns_none_on_general_list(monkeypatch):
Без привязки к id функция вернула бы «shishkinn» для чужого дома. Без привязки к id функция вернула бы «shishkinn» для чужого дома.
""" """
spy = _spy_browser_fetcher(_GENERAL_LIST_HTML) spy = _spy_browser_fetcher(_GENERAL_LIST_HTML)
monkeypatch.setattr("scraper_kit.providers.yandex.newbuilding.BrowserFetcher", spy) monkeypatch.setattr("scraper_kit.providers._base.BrowserFetcher", spy)
assert await resolve_yandex_jk_slug("286394") is None assert await resolve_yandex_jk_slug("286394", config=_CONFIG) is None
@pytest.mark.asyncio @pytest.mark.asyncio
@ -232,9 +193,9 @@ async def test_resolve_yandex_jk_slug_does_not_return_placeholder(monkeypatch):
spy = _spy_browser_fetcher( spy = _spy_browser_fetcher(
'<a href="/ekaterinburg/kupit/novostrojka/zhk-286394/">сама страница</a>' '<a href="/ekaterinburg/kupit/novostrojka/zhk-286394/">сама страница</a>'
) )
monkeypatch.setattr("scraper_kit.providers.yandex.newbuilding.BrowserFetcher", spy) monkeypatch.setattr("scraper_kit.providers._base.BrowserFetcher", spy)
assert await resolve_yandex_jk_slug("286394") is None assert await resolve_yandex_jk_slug("286394", config=_CONFIG) is None
# ── cian.newbuilding.resolve_cian_zhk_url_via_search ───────────────────────── # ── cian.newbuilding.resolve_cian_zhk_url_via_search ─────────────────────────
@ -276,17 +237,33 @@ async def test_resolve_cian_zhk_url_via_search_passes_proxy_from_config(monkeypa
assert kwargs.get("proxy_url") == _TEST_PROXY assert kwargs.get("proxy_url") == _TEST_PROXY
# ── #2385: config обязателен — без него ни фетчера, ни сессии ────────────────
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_resolve_cian_zhk_url_via_search_proxy_none_without_config(monkeypatch): @pytest.mark.parametrize(
"""Без config= (SILENT footgun): вызов НЕ падает (в отличие от BrowserFetcher "call",
assertion выше в этом файле), но proxy_url молча становится None резолвер уходит [
на прямое (непроксированное) соединение вместо настроенного мобильного прокси.""" lambda: fetch_newbuilding("https://zhk-test-ekb-i.cian.ru/"),
spy = _spy_curl_session(_SERP_HTML) lambda: YandexNewbuildingScraper().fetch_jk(jk_slug="tatlin", jk_id="1592987"),
monkeypatch.setattr("scraper_kit.providers.cian.newbuilding.build_curl_cffi_session", spy) lambda: resolve_yandex_jk_slug("1592987"),
lambda: resolve_cian_zhk_url_via_search(48853),
],
ids=["cian_fetch_newbuilding", "yandex_fetch_jk", "resolve_yandex_slug", "resolve_cian_url"],
)
async def test_newbuilding_entrypoints_refuse_to_run_without_config(monkeypatch, call):
"""Вызов без config= падает TypeError ДО сети: браузерный фетчер и curl-сессия не
строятся. Раньше тот же вызов молча уходил на env-узел сайдкара мимо пула (#2767,
#3197) — отказ по сигнатуре единственное, что ловит забывчивого вызывающего."""
browser_spy = _spy_browser_fetcher("<html></html>")
monkeypatch.setattr("scraper_kit.providers._base.BrowserFetcher", browser_spy)
session_spy = _spy_curl_session(_SERP_HTML)
monkeypatch.setattr(
"scraper_kit.providers.cian.newbuilding.build_curl_cffi_session", session_spy
)
url = await resolve_cian_zhk_url_via_search(48853) with pytest.raises(TypeError, match="config"):
await call()
assert url == "https://zhk-parkovyy-kvartal-ekb-i.cian.ru" # "works" — silently unproxied assert browser_spy.call_count == 0
spy.assert_called_once() assert session_spy.call_count == 0
_, kwargs = spy.call_args
assert kwargs.get("proxy_url") is None

View file

@ -199,6 +199,10 @@ async def test_yandex_valuation_session_no_proxies_when_none():
# После #972 fetch_newbuilding тянет страницу ЖК через BrowserFetcher (camoufox), # После #972 fetch_newbuilding тянет страницу ЖК через BrowserFetcher (camoufox),
# а НЕ через curl_cffi AsyncSession. Прокси применяются на уровне браузер-контейнера, # а НЕ через curl_cffi AsyncSession. Прокси применяются на уровне браузер-контейнера,
# а не через proxies= kwarg. AsyncSession для page-fetch больше НЕ создаётся. # а не через proxies= kwarg. AsyncSession для page-fetch больше НЕ создаётся.
# config обязателен (#2385): фетчер строит фабрика _base.build_browser_fetcher.
_NB_CONFIG = SimpleNamespace(
browser_http_endpoint="http://tradein-browser:3000", use_proxy_pool_browser=False
)
@pytest.mark.asyncio @pytest.mark.asyncio
@ -222,8 +226,8 @@ async def test_cian_newbuilding_own_session_receives_proxies():
mock_fetcher.__aenter__ = AsyncMock(return_value=mock_fetcher) mock_fetcher.__aenter__ = AsyncMock(return_value=mock_fetcher)
mock_fetcher.__aexit__ = AsyncMock(return_value=None) mock_fetcher.__aexit__ = AsyncMock(return_value=None)
with patch.object(cian_newbuilding, "BrowserFetcher", lambda *a, **k: mock_fetcher): with patch("scraper_kit.providers._base.BrowserFetcher", lambda *a, **k: mock_fetcher):
await cian_newbuilding.fetch_newbuilding(zhk_url) await cian_newbuilding.fetch_newbuilding(zhk_url, config=_NB_CONFIG)
# BrowserFetcher.fetch вызван с ЖК-url # BrowserFetcher.fetch вызван с ЖК-url
assert fetch_calls == [zhk_url] assert fetch_calls == [zhk_url]
@ -253,8 +257,10 @@ async def test_cian_newbuilding_shared_session_not_recreated():
sentinel_session = MagicMock(name="external_session") sentinel_session = MagicMock(name="external_session")
with patch.object(cian_newbuilding, "BrowserFetcher", lambda *a, **k: mock_fetcher): with patch("scraper_kit.providers._base.BrowserFetcher", lambda *a, **k: mock_fetcher):
await cian_newbuilding.fetch_newbuilding(zhk_url, session=sentinel_session) await cian_newbuilding.fetch_newbuilding(
zhk_url, config=_NB_CONFIG, session=sentinel_session
)
# BrowserFetcher.fetch вызван — именно через него тянется страница # BrowserFetcher.fetch вызван — именно через него тянется страница
assert fetch_calls == [zhk_url] assert fetch_calls == [zhk_url]

View file

@ -31,7 +31,7 @@ def test_yandex_detail_picks_up_delay():
def test_yandex_newbuilding_picks_up_delay(): def test_yandex_newbuilding_picks_up_delay():
"""Kit YandexNewbuildingScraper: delay_provider callable вызывается с self.name.""" """Kit YandexNewbuildingScraper: delay_provider callable вызывается с self.name."""
s = YandexNewbuildingScraper(delay_provider=lambda _name: 9.5) s = YandexNewbuildingScraper(config=_KIT_CONFIG, delay_provider=lambda _name: 9.5)
assert s.request_delay_sec == 9.5 assert s.request_delay_sec == 9.5

View file

@ -24,7 +24,6 @@ import re
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Any from typing import TYPE_CHECKING, Any
from scraper_kit.browser_fetcher import BrowserFetcher
from scraper_kit.cian_exceptions import CianBlockedError from scraper_kit.cian_exceptions import CianBlockedError
from scraper_kit.cian_state_parser import extract_all_states, extract_state from scraper_kit.cian_state_parser import extract_all_states, extract_state
from scraper_kit.providers._base import build_browser_fetcher, build_curl_cffi_session from scraper_kit.providers._base import build_browser_fetcher, build_curl_cffi_session
@ -214,7 +213,7 @@ class NewbuildingEnrichment:
async def fetch_newbuilding( async def fetch_newbuilding(
zhk_url: str, zhk_url: str,
*, *,
config: ScraperConfig | None = None, config: ScraperConfig,
session: AsyncSession | None = None, # kept for backward-compat; unused for page fetch session: AsyncSession | None = None, # kept for backward-compat; unused for page fetch
proxy_provider: ProxyProvider | None = None, proxy_provider: ProxyProvider | None = None,
) -> NewbuildingEnrichment | None: ) -> NewbuildingEnrichment | None:
@ -225,9 +224,8 @@ async def fetch_newbuilding(
Args: Args:
zhk_url: e.g. 'https://zhk-ekaterininskiy-park-ekb-i.cian.ru/' zhk_url: e.g. 'https://zhk-ekaterininskiy-park-ekb-i.cian.ru/'
config: ScraperConfig источник browser_http_endpoint для BrowserFetcher config: ScraperConfig источник browser_http_endpoint/use_pool для BrowserFetcher.
(#2322 fix: раньше не принимался вообще, BrowserFetcher(source="cian") Обязателен (#2385): без него фетчер строился с endpoint=None мимо пула.
конструировался без endpoint= TypeError на любом вызове).
session: зарезервирован для обратной совместимости с вызывающими; session: зарезервирован для обратной совместимости с вызывающими;
больше не используется для fetch страницы ЖК. Resolve-функции больше не используется для fetch страницы ЖК. Resolve-функции
(resolve_cian_zhk_url_via_search) по-прежнему используют curl_cffi. (resolve_cian_zhk_url_via_search) по-прежнему используют curl_cffi.
@ -254,14 +252,10 @@ async def fetch_newbuilding(
# Разметка не менялась: MFE 'newbuilding-card-desktop-frontend'/'initialState' # Разметка не менялась: MFE 'newbuilding-card-desktop-frontend'/'initialState'
# разобрался ТЕКУЩИМ кодом на всех трёх мобильных узлах. Ломался тракт, не разбор. # разобрался ТЕКУЩИМ кодом на всех трёх мобильных узлах. Ломался тракт, не разбор.
# #
# config=None остаётся законным (admin.py debug-роут и # config обязателен (#2385): запасная ветка `config=None` строила
# test_cian_fetch_newbuilding_endpoint_none_without_config зовут без config= и # BrowserFetcher(endpoint=None) мимо фабрики — новый вызывающий без config молча уходил
# ждут graceful endpoint=None, а не AttributeError), поэтому фабрика применяется # на env-узел сайдкара, тот самый путь, что стоил 8 суток выше.
# только когда config есть — она требует ScraperConfig mandatory by design. browser = build_browser_fetcher(config, "cian", proxy_provider=proxy_provider)
if config is not None:
browser = build_browser_fetcher(config, "cian", proxy_provider=proxy_provider)
else:
browser = BrowserFetcher(source="cian", endpoint=None) # type: ignore[arg-type]
# ponytail: lease берётся на ОДИН дом (фетчер живёт внутри этой функции), то есть # ponytail: lease берётся на ОДИН дом (фетчер живёт внутри этой функции), то есть
# прогон из 25 домов даёт до 25 acquire → до 25 релончей camoufox (~8 с каждый). # прогон из 25 домов даёт до 25 acquire → до 25 релончей camoufox (~8 с каждый).
@ -883,7 +877,7 @@ async def _fetch_zhk_slug(session: AsyncSession, nb_id: int) -> str | None:
async def resolve_cian_zhk_url_via_search( async def resolve_cian_zhk_url_via_search(
nb_id: int, nb_id: int,
*, *,
config: ScraperConfig | None = None, config: ScraperConfig,
session: AsyncSession | None = None, session: AsyncSession | None = None,
proxy_provider: ProxyProvider | None = None, proxy_provider: ProxyProvider | None = None,
) -> str | None: ) -> str | None:
@ -928,7 +922,7 @@ async def resolve_cian_zhk_url_via_search(
# Own-session path. Прокси: пул за флагом use_proxy_pool_curl (#2163), иначе env # Own-session path. Прокси: пул за флагом use_proxy_pool_curl (#2163), иначе env
# cian_proxy_url; пусто → прямое подключение (dev/no-op). # cian_proxy_url; пусто → прямое подключение (dev/no-op).
# curl_proxy_url: mark_banned на CianBlockedError + mark_health + release на выходе. # curl_proxy_url: mark_banned на CianBlockedError + mark_health + release на выходе.
_env = config.cian_proxy_url if config is not None else None _env = config.cian_proxy_url
with curl_proxy_url(config, proxy_provider, "cian", env_fallback_url=_env) as _proxy_url: with curl_proxy_url(config, proxy_provider, "cian", env_fallback_url=_env) as _proxy_url:
own_session = build_curl_cffi_session( own_session = build_curl_cffi_session(
proxy_url=_proxy_url, proxy_url=_proxy_url,

View file

@ -28,7 +28,6 @@ from pydantic import BaseModel, Field
from selectolax.parser import HTMLParser, Node from selectolax.parser import HTMLParser, Node
from scraper_kit.base import BaseScraper from scraper_kit.base import BaseScraper
from scraper_kit.browser_fetcher import BrowserFetcher
from scraper_kit.providers._base import build_browser_fetcher from scraper_kit.providers._base import build_browser_fetcher
from scraper_kit.proxy_errors import caused_by_no_proxy from scraper_kit.proxy_errors import caused_by_no_proxy
from scraper_kit.yandex_helpers import ( from scraper_kit.yandex_helpers import (
@ -158,7 +157,7 @@ class YandexNewbuildingScraper(BaseScraper):
def __init__( def __init__(
self, self,
*, *,
config: ScraperConfig | None = None, config: ScraperConfig,
delay_provider: Callable[[str], float] | None = None, delay_provider: Callable[[str], float] | None = None,
proxy_provider: ProxyProvider | None = None, proxy_provider: ProxyProvider | None = None,
) -> None: ) -> None:
@ -193,14 +192,10 @@ class YandexNewbuildingScraper(BaseScraper):
""" """
url = f"{self.base_url}/{city}/kupit/novostrojka/{jk_slug}-{jk_id}/" url = f"{self.base_url}/{city}/kupit/novostrojka/{jk_slug}-{jk_id}/"
# #3197: через фабрику — endpoint/use_pool/environment из config mandatory # #3197: через фабрику — endpoint/use_pool/environment из config mandatory
# (образец: yandex/serp.py::__aenter__). config=None (dev/офлайн-тесты) — # (образец: yandex/serp.py::__aenter__). Запасной ветки config=None больше нет (#2385).
# прежний путь без пула, как в cian/newbuilding.py. fetcher_cm = build_browser_fetcher(
if self._config is not None: self._config, "yandex", proxy_provider=self._proxy_provider
fetcher_cm = build_browser_fetcher( )
self._config, "yandex", proxy_provider=self._proxy_provider
)
else:
fetcher_cm = BrowserFetcher(source="yandex", endpoint=None) # type: ignore[arg-type]
try: try:
async with fetcher_cm as fetcher: async with fetcher_cm as fetcher:
html = await fetcher.fetch(url) html = await fetcher.fetch(url)
@ -359,7 +354,7 @@ async def resolve_yandex_jk_slug(
jk_id: str, jk_id: str,
city: str = "ekaterinburg", city: str = "ekaterinburg",
*, *,
config: ScraperConfig | None = None, config: ScraperConfig,
proxy_provider: ProxyProvider | None = None, proxy_provider: ProxyProvider | None = None,
) -> str | None: ) -> str | None:
"""Найти Yandex Realty slug для ЖК по его ext_id (jk_id). """Найти Yandex Realty slug для ЖК по его ext_id (jk_id).
@ -402,10 +397,7 @@ async def resolve_yandex_jk_slug(
""" """
page_url = f"https://realty.yandex.ru/{city}/kupit/novostrojka/{_JK_SLUG_PLACEHOLDER}-{jk_id}/" page_url = f"https://realty.yandex.ru/{city}/kupit/novostrojka/{_JK_SLUG_PLACEHOLDER}-{jk_id}/"
# #3197: см. fetch_jk — та же проводка пула через фабрику. # #3197: см. fetch_jk — та же проводка пула через фабрику.
if config is not None: fetcher_cm = build_browser_fetcher(config, "yandex", proxy_provider=proxy_provider)
fetcher_cm = build_browser_fetcher(config, "yandex", proxy_provider=proxy_provider)
else:
fetcher_cm = BrowserFetcher(source="yandex", endpoint=None) # type: ignore[arg-type]
try: try:
async with fetcher_cm as fetcher: async with fetcher_cm as fetcher:
html = await fetcher.fetch(page_url) html = await fetcher.fetch(page_url)