fix(tradein): config обязателен в newbuilding-провайдерах Циана и Яндекса (#2385)
fetch_newbuilding, resolve_cian_zhk_url_via_search, YandexNewbuildingScraper и resolve_yandex_jk_slug принимали config=None и в этом случае строили BrowserFetcher(endpoint=None) мимо фабрики, а резолвер Циана — сессию без прокси. Все боевые вызовы config уже передают, но новый вызывающий без config молча ушёл бы на env-узел сайдкара мимо пула — именно так потеряли 8 суток в #2767 и сломались в #3197. Теперь config обязателен по сигнатуре, запасные ветки удалены, фетчер строит только build_browser_fetcher. Тесты: кейсы *_without_config (ждали endpoint=None) заменены одним — без config TypeError до сети, фетчер и сессия не строятся; остальные тестовые вызовы получили config. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
34642e1dd5
commit
eade88bc66
6 changed files with 74 additions and 102 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
|
||||||
|
|
@ -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]
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue