chore(tradein/proxy): последние две ручки admin.py — через фабрику фетчера; снят форс pool-режима у cian-history (#3197, #3386)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI / changes (pull_request) Successful in 10s
CI Trade-In / browser-tests (pull_request) Has been skipped
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 5m5s
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI / changes (pull_request) Successful in 10s
CI Trade-In / browser-tests (pull_request) Has been skipped
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 5m5s
Два хвоста одной темы — проводка пула прокси в контейнере backend. #3197: `cian-login` и `domclick-detail-debug` были последними прямыми конструкциями `BrowserFetcher(source=, endpoint=)` мимо `build_browser_fetcher`. Без `proxy_provider`/`use_pool`/`environment` сайдкар брал свой env-узел `SCRAPER_PROXY_URL` (на проде выключенный узел 9: 407 → camoufox `InvalidIP`), а прод-отказ «пул пуст» (#2616) на этих путях был мёртв — он смотрит на `environment`, который до конструктора не доезжал. Соседи по эпику уже переведены (#3382 cian, #3389 yandex). Прямых конструкций без провайдера вне тестов больше не осталось: остальные (backfill-задачи, pipeline) пул получают своими kwargs, а `endpoint=None`-ветки providers — это документированный `config=None` для офлайн-тестов. #3386: `_PoolCurlConfig` в `cian_price_history` форсил `use_proxy_pool_curl=True`, потому что у контейнера `backend` не было переменной. #3387 задал `USE_PROXY_POOL_CURL: "true"` сервису `backend` в compose — зашитая константа стала лишней и делала рубильник неотключаемым ровно на этом пути (докстринг при этом описывал уже неверную причину). Теперь `RealScraperConfig()` напрямую. Тесты меряют значения, а не наличие kwarg'а: на откате исходников красные 4 параметризации нового `test_3197_admin_debug_browser_pool_wiring` (`assert None is not None` — провайдер не передан) и `test_price_history_honours_flag_off` (`assert ['cian'] == []` — пул дёргался при выключенном флаге).
This commit is contained in:
parent
f52a39e3ad
commit
8761602e9b
6 changed files with 212 additions and 45 deletions
|
|
@ -40,7 +40,6 @@ from pydantic import BaseModel, Field, field_validator
|
||||||
# убрал legacy app.services.scheduler.scheduler_loop fallback) — тот же orchestrator,
|
# убрал legacy app.services.scheduler.scheduler_loop fallback) — тот же orchestrator,
|
||||||
# что и debug-роуты этого файла.
|
# что и debug-роуты этого файла.
|
||||||
from scraper_kit.base import save_listings
|
from scraper_kit.base import save_listings
|
||||||
from scraper_kit.browser_fetcher import BrowserFetcher
|
|
||||||
from scraper_kit.orchestration.pipeline import (
|
from scraper_kit.orchestration.pipeline import (
|
||||||
DEFAULT_REGION_CODE,
|
DEFAULT_REGION_CODE,
|
||||||
run_avito_city_sweep,
|
run_avito_city_sweep,
|
||||||
|
|
@ -49,6 +48,7 @@ from scraper_kit.orchestration.pipeline import (
|
||||||
run_yandex_city_sweep,
|
run_yandex_city_sweep,
|
||||||
run_yandex_full_load,
|
run_yandex_full_load,
|
||||||
)
|
)
|
||||||
|
from scraper_kit.providers._base import build_browser_fetcher
|
||||||
from scraper_kit.providers.avito.detail import fetch_detail, save_detail_enrichment
|
from scraper_kit.providers.avito.detail import fetch_detail, save_detail_enrichment
|
||||||
from scraper_kit.providers.avito.houses import fetch_house_catalog, save_house_catalog_enrichment
|
from scraper_kit.providers.avito.houses import fetch_house_catalog, save_house_catalog_enrichment
|
||||||
from scraper_kit.providers.avito.imv import (
|
from scraper_kit.providers.avito.imv import (
|
||||||
|
|
@ -508,8 +508,12 @@ async def cian_auto_login(
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
async with BrowserFetcher(
|
# #3197 (хвост): через фабрику, а не BrowserFetcher(source=, endpoint=) напрямую —
|
||||||
source="cian", endpoint=settings.browser_http_endpoint
|
# прямая конструкция не проставляла proxy_provider/use_pool/environment, и сайдкар
|
||||||
|
# логинился со своего env-узла SCRAPER_PROXY_URL мимо пула (на проде это выключенный
|
||||||
|
# узел 9: 407 → camoufox InvalidIP), а прод-отказ «пул пуст» (#2616) тут был мёртв.
|
||||||
|
async with build_browser_fetcher(
|
||||||
|
RealScraperConfig(), "cian", proxy_provider=_kit_proxy_provider()
|
||||||
) as fetcher:
|
) as fetcher:
|
||||||
raw_cookies = await fetcher.login(
|
raw_cookies = await fetcher.login(
|
||||||
url=settings.cian_login_url,
|
url=settings.cian_login_url,
|
||||||
|
|
@ -699,7 +703,12 @@ async def debug_domclick_detail_fetch(
|
||||||
# построен и verified live именно 2026-07-04 (815КБ __SSR_STATE__ через свежий IP).
|
# построен и verified live именно 2026-07-04 (815КБ __SSR_STATE__ через свежий IP).
|
||||||
# Заменяет прежний source="cian" (docstring domclick/detail.py — устаревшая
|
# Заменяет прежний source="cian" (docstring domclick/detail.py — устаревшая
|
||||||
# рекомендация от 2026-06-27, до появления выделенного пула).
|
# рекомендация от 2026-06-27, до появления выделенного пула).
|
||||||
async with BrowserFetcher(source="domclick", endpoint=settings.browser_http_endpoint) as bf:
|
# #3197 (хвост): фабрика вместо прямой конструкции — она и есть то место, где
|
||||||
|
# proxy_provider/use_pool/environment попадают в тело POST /fetch. Без них узел
|
||||||
|
# выбирал сайдкар из своего env, а не пул с provider_affinity='domclick'.
|
||||||
|
async with build_browser_fetcher(
|
||||||
|
RealScraperConfig(), "domclick", proxy_provider=_kit_proxy_provider()
|
||||||
|
) as bf:
|
||||||
try:
|
try:
|
||||||
enrichment = await domclick_fetch_detail(
|
enrichment = await domclick_fetch_detail(
|
||||||
body.card_url, browser_fetcher=bf, cookies=cookies
|
body.card_url, browser_fetcher=bf, cookies=cookies
|
||||||
|
|
|
||||||
|
|
@ -34,25 +34,6 @@ from app.services.scraper_settings import get_scraper_delay
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class _PoolCurlConfig(RealScraperConfig):
|
|
||||||
"""RealScraperConfig с принудительно включённым pool-режимом curl (#2830).
|
|
||||||
|
|
||||||
`USE_PROXY_POOL_CURL` задан только контейнеру `scraper` (docker-compose.prod.yml
|
|
||||||
services.scraper.environment), а этот бэкфилл запускается ручкой
|
|
||||||
`POST /admin/scrape/cian-price-history` в контейнере `backend`, где переменной нет
|
|
||||||
→ `settings.use_proxy_pool_curl` = False. С ней `providers/_proxy.py::curl_proxy_url`
|
|
||||||
ИГНОРИРУЕТ переданный `proxy_provider` и уходит на статичный `SCRAPER_PROXY_URL`:
|
|
||||||
один `proxy_provider=` был бы правкой без эффекта (зелёный тест, нулевой прод).
|
|
||||||
|
|
||||||
Флаг — рубильник раскатки pool-режима для планировщика, а не решение «этому пути
|
|
||||||
пул не нужен»: инцидент 2026-08-10 (#2830) — ровно про то, что нужен именно ему.
|
|
||||||
"""
|
|
||||||
|
|
||||||
@property
|
|
||||||
def use_proxy_pool_curl(self) -> bool:
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class CianPriceHistoryResult:
|
class CianPriceHistoryResult:
|
||||||
checked: int = 0
|
checked: int = 0
|
||||||
|
|
@ -84,7 +65,13 @@ async def backfill_cian_price_history(
|
||||||
# Egress через пул с учётом `scrape_proxy_source_bans` (#2830): узел выбирает
|
# Egress через пул с учётом `scrape_proxy_source_bans` (#2830): узел выбирает
|
||||||
# `curl_proxy_url` внутри `fetch_detail`, он же на выходе возвращает вердикт
|
# `curl_proxy_url` внутри `fetch_detail`, он же на выходе возвращает вердикт
|
||||||
# (mark_banned на CianBlockedError / mark_health / release).
|
# (mark_banned на CianBlockedError / mark_health / release).
|
||||||
scraper_config = _PoolCurlConfig()
|
#
|
||||||
|
# Флаг читается из окружения как у всех (#3386 хвост): до #3387 у контейнера
|
||||||
|
# `backend` не было `USE_PROXY_POOL_CURL`, и здесь стоял подкласс с зашитым
|
||||||
|
# `use_proxy_pool_curl = True` — иначе `curl_proxy_url` игнорировал бы
|
||||||
|
# `proxy_provider`. Теперь переменная задана и сервису `backend` (compose), а
|
||||||
|
# зашитая константа делала рубильник неотключаемым ровно на этом пути.
|
||||||
|
scraper_config = RealScraperConfig()
|
||||||
proxy_provider = RealProxyProvider()
|
proxy_provider = RealProxyProvider()
|
||||||
|
|
||||||
if listing_id is not None:
|
if listing_id is not None:
|
||||||
|
|
|
||||||
|
|
@ -14,9 +14,12 @@
|
||||||
kwarg'а в вызове. Красные на старом коде:
|
kwarg'а в вызове. Красные на старом коде:
|
||||||
|
|
||||||
* (1) `fetch_detail` вызывался без `proxy_provider` → lease не брался, 403 никому не
|
* (1) `fetch_detail` вызывался без `proxy_provider` → lease не брался, 403 никому не
|
||||||
сообщался: `mark_banned_calls == []`. Плюс ловушка «правка без эффекта»:
|
сообщался: `mark_banned_calls == []`. Ловушка «правка без эффекта» была в том, что
|
||||||
`USE_PROXY_POOL_CURL` задан только контейнеру `scraper`, а ручка живёт в `backend`,
|
`USE_PROXY_POOL_CURL` задавался только контейнеру `scraper`, а ручка живёт в
|
||||||
где флага нет — один `proxy_provider=` пул бы не включил (см. `_PoolCurlConfig`).
|
`backend`, где флага не было — один `proxy_provider=` пул бы не включил. Отсюда взялся
|
||||||
|
подкласс конфига с зашитым `use_proxy_pool_curl = True`; #3387 задал переменную и
|
||||||
|
сервису `backend`, костыль снят (#3386 хвост), флаг снова управляет путём в обе
|
||||||
|
стороны — см. `test_price_history_honours_flag_off`.
|
||||||
* (2) `_provider_proxy_url(source)` возвращал `settings.scraper_proxy_url` и не имел
|
* (2) `_provider_proxy_url(source)` возвращал `settings.scraper_proxy_url` и не имел
|
||||||
параметра `db` — вызов из теста падал бы на сигнатуре, а исход «пул исчерпан»
|
параметра `db` — вызов из теста падал бы на сигнатуре, а исход «пул исчерпан»
|
||||||
выражения не имел вообще.
|
выражения не имел вообще.
|
||||||
|
|
@ -123,7 +126,10 @@ def _price_history_db(n_listings: int) -> MagicMock:
|
||||||
return db
|
return db
|
||||||
|
|
||||||
|
|
||||||
async def _run_price_history(pool: _SpyPool, *, status_code: int, n: int = 1) -> Any:
|
async def _run_price_history(
|
||||||
|
pool: _SpyPool, *, status_code: int, n: int = 1, use_pool: bool = True
|
||||||
|
) -> Any:
|
||||||
|
from app.core.config import settings
|
||||||
from app.services import scraper_adapters
|
from app.services import scraper_adapters
|
||||||
from app.services.cian_price_history import backfill_cian_price_history
|
from app.services.cian_price_history import backfill_cian_price_history
|
||||||
|
|
||||||
|
|
@ -132,6 +138,7 @@ async def _run_price_history(pool: _SpyPool, *, status_code: int, n: int = 1) ->
|
||||||
cian_detail, "build_curl_cffi_session", return_value=_session_returning(status_code)
|
cian_detail, "build_curl_cffi_session", return_value=_session_returning(status_code)
|
||||||
),
|
),
|
||||||
patch("app.services.cian_price_history.get_scraper_delay", return_value=0.0),
|
patch("app.services.cian_price_history.get_scraper_delay", return_value=0.0),
|
||||||
|
patch.object(settings, "use_proxy_pool_curl", use_pool),
|
||||||
patch.object(scraper_adapters, "_proxy_pool", pool),
|
patch.object(scraper_adapters, "_proxy_pool", pool),
|
||||||
patch.object(scraper_adapters, "_SessionLocal", MagicMock()),
|
patch.object(scraper_adapters, "_SessionLocal", MagicMock()),
|
||||||
):
|
):
|
||||||
|
|
@ -139,23 +146,33 @@ async def _run_price_history(pool: _SpyPool, *, status_code: int, n: int = 1) ->
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_price_history_takes_pool_node_despite_flag_off() -> None:
|
async def test_price_history_takes_pool_node() -> None:
|
||||||
"""Узел берётся из пула даже при выключенном USE_PROXY_POOL_CURL (контейнер backend).
|
"""Узел берётся из пула, вердикт возвращается, lease не течёт.
|
||||||
|
|
||||||
Красный на старом коде дважды: не было ни `proxy_provider=`, ни принудительного
|
Красный на коде до #2830: не было `proxy_provider=` — `curl_proxy_url` уходил на
|
||||||
pool-режима — `curl_proxy_url` уходил на статичный env-узел и `acquire` не звал.
|
статичный env-узел и `acquire` не звал.
|
||||||
"""
|
"""
|
||||||
from app.core.config import settings
|
|
||||||
|
|
||||||
assert settings.use_proxy_pool_curl is False, (
|
|
||||||
"тест обязан идти тем же путём, что прод-контейнер backend: без USE_PROXY_POOL_CURL"
|
|
||||||
)
|
|
||||||
pool = _SpyPool()
|
pool = _SpyPool()
|
||||||
await _run_price_history(pool, status_code=200)
|
await _run_price_history(pool, status_code=200)
|
||||||
assert pool.acquire_calls == ["cian"]
|
assert pool.acquire_calls == ["cian"]
|
||||||
assert pool.release_calls == [9] # lease не течёт
|
assert pool.release_calls == [9] # lease не течёт
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_price_history_honours_flag_off() -> None:
|
||||||
|
"""USE_PROXY_POOL_CURL=false → честный env-путь, а не пул через зашитую константу.
|
||||||
|
|
||||||
|
#3386 (хвост): пока в сервисе жил подкласс `RealScraperConfig` с
|
||||||
|
`use_proxy_pool_curl = True`, рубильник на этом пути был неотключаем — тест красный
|
||||||
|
на main (`acquire_calls == ["cian"]`). После #3387 переменная задана контейнеру
|
||||||
|
`backend` в compose, костыль лишний, и флаг снова управляет обеими сторонами.
|
||||||
|
"""
|
||||||
|
pool = _SpyPool()
|
||||||
|
result = await _run_price_history(pool, status_code=200, use_pool=False)
|
||||||
|
assert pool.acquire_calls == [], "при выключенном флаге пул не трогаем"
|
||||||
|
assert result.checked == 1, "запрос всё равно идёт — просто env-прокси, как раньше"
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_price_history_403_bans_the_node_for_cian() -> None:
|
async def test_price_history_403_bans_the_node_for_cian() -> None:
|
||||||
"""403 от Циана снимает узел с выдачи ИМЕННО Циану. Красный: было `mark_banned` = []."""
|
"""403 от Циана снимает узел с выдачи ИМЕННО Циану. Красный: было `mark_banned` = []."""
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,150 @@
|
||||||
|
"""#3197 (хвост) — две служебные ручки admin.py строили BrowserFetcher мимо пула.
|
||||||
|
|
||||||
|
`POST /admin/scrape/cian/auto-login` и `POST /admin/scrape/domclick/debug/detail-fetch`
|
||||||
|
конструировали `BrowserFetcher(source=..., endpoint=settings.browser_http_endpoint)`
|
||||||
|
напрямую — без `proxy_provider`/`use_pool`/`environment`, единственных трёх аргументов,
|
||||||
|
которые кладут "proxy" в тело POST /fetch сайдкара. Без них сайдкар берёт свой env-узел
|
||||||
|
(`SCRAPER_PROXY_URL`), на проде это выключенный узел 9 (407 → camoufox `InvalidIP`), а
|
||||||
|
прод-отказ «пул пуст → не ходить на env/direct» (#2616) на этих путях был мёртв: он
|
||||||
|
смотрит на `environment`, который до конструктора не доезжал. Соседние точки того же
|
||||||
|
эпика уже переведены: yandex newbuilding (#3389), cian history (#3197), domclick/avito.
|
||||||
|
|
||||||
|
Тест меряет ЗНАЧЕНИЯ kwargs, а не то, из какого модуля взят класс: подделка ставится и на
|
||||||
|
`scraper_kit.providers._base.BrowserFetcher` (путь через фабрику), и на
|
||||||
|
`app.api.v1.admin.BrowserFetcher` (прямая конструкция, как было до правки, `create=True` —
|
||||||
|
после правки такого имени в модуле нет). Поэтому на откате красное читается как «в
|
||||||
|
конструктор не передан провайдер», а не как «мок не сработал».
|
||||||
|
|
||||||
|
Сеть/БД/камуфокс замоканы; в сеть тест не ходит.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
from contextlib import ExitStack
|
||||||
|
from types import SimpleNamespace
|
||||||
|
from typing import Any, ClassVar
|
||||||
|
from unittest.mock import AsyncMock, MagicMock, patch
|
||||||
|
|
||||||
|
os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://test:test@localhost:5432/test")
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from app.api.v1 import admin
|
||||||
|
from app.services import cian_session as cian_session_svc
|
||||||
|
|
||||||
|
_TARGETS = ("scraper_kit.providers._base.BrowserFetcher", "app.api.v1.admin.BrowserFetcher")
|
||||||
|
|
||||||
|
|
||||||
|
class _CapturingFetcher:
|
||||||
|
"""Собирает kwargs каждой конструкции; login() отдаёт валидный набор куки."""
|
||||||
|
|
||||||
|
captured: ClassVar[list[dict[str, Any]]] = []
|
||||||
|
|
||||||
|
def __init__(self, **kwargs: Any) -> None:
|
||||||
|
_CapturingFetcher.captured.append(kwargs)
|
||||||
|
|
||||||
|
async def __aenter__(self) -> _CapturingFetcher:
|
||||||
|
return self
|
||||||
|
|
||||||
|
async def __aexit__(self, *_: object) -> None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def login(self, **_kwargs: Any) -> dict[str, str]:
|
||||||
|
return {name: "v" for name in cian_session_svc.CIAN_REQUIRED_COOKIES}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def _pool_on(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
"""Curl-флаг включён всегда: это он открывает `_kit_proxy_provider()` (гейт #2163).
|
||||||
|
|
||||||
|
Провайдер обязан доезжать до конструктора при ЛЮБОМ значении browser-флага —
|
||||||
|
`use_pool=False` фетчер его просто игнорирует, но call-site у dev и прода один.
|
||||||
|
"""
|
||||||
|
from app.core.config import settings
|
||||||
|
|
||||||
|
monkeypatch.setattr(settings, "use_proxy_pool_curl", True)
|
||||||
|
_CapturingFetcher.captured = []
|
||||||
|
|
||||||
|
|
||||||
|
def _assert_wiring(source: str, *, use_pool: bool, environment: str) -> None:
|
||||||
|
assert len(_CapturingFetcher.captured) == 1, "ручка обязана построить ровно один фетчер"
|
||||||
|
kwargs = _CapturingFetcher.captured[0]
|
||||||
|
assert kwargs["source"] == source
|
||||||
|
# .get(), а не [] — красное должно читаться как «значение не то», а не KeyError.
|
||||||
|
assert kwargs.get("proxy_provider") is not None, "без провайдера пул не подключится"
|
||||||
|
assert kwargs.get("use_pool") is use_pool, "флаг пула должен доезжать из конфига"
|
||||||
|
# #2616 шаг 1: без environment отказ «пул пуст» на этом пути мёртв.
|
||||||
|
assert kwargs.get("environment") == environment
|
||||||
|
# endpoint не должен потеряться при переезде на фабрику (#2322: без него TypeError).
|
||||||
|
from app.core.config import settings
|
||||||
|
|
||||||
|
assert kwargs.get("endpoint") == settings.browser_http_endpoint
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(("use_pool", "environment"), [(True, "production"), (False, "dev")])
|
||||||
|
@pytest.mark.usefixtures("_pool_on")
|
||||||
|
async def test_cian_auto_login_wires_proxy_pool(
|
||||||
|
monkeypatch: pytest.MonkeyPatch, use_pool: bool, environment: str
|
||||||
|
) -> None:
|
||||||
|
"""Логин-браузер ходит через узел пула, а не через env-прокси сайдкара."""
|
||||||
|
from app.core.config import settings
|
||||||
|
|
||||||
|
monkeypatch.setattr(settings, "use_proxy_pool_browser", use_pool)
|
||||||
|
monkeypatch.setattr(settings, "environment", environment)
|
||||||
|
monkeypatch.setattr(settings, "cookie_encryption_key", "k" * 32)
|
||||||
|
monkeypatch.setattr(settings, "cian_login_email", "a@b.c")
|
||||||
|
monkeypatch.setattr(settings, "cian_login_password", "pw")
|
||||||
|
|
||||||
|
with ExitStack() as stack:
|
||||||
|
for target in _TARGETS:
|
||||||
|
stack.enter_context(patch(target, _CapturingFetcher, create=True))
|
||||||
|
stack.enter_context(
|
||||||
|
patch.object(
|
||||||
|
cian_session_svc, "verify_session", AsyncMock(return_value={"user": {"userId": 7}})
|
||||||
|
)
|
||||||
|
)
|
||||||
|
stack.enter_context(patch.object(cian_session_svc, "save_session", MagicMock()))
|
||||||
|
result = await admin.cian_auto_login(db=MagicMock(), body=None)
|
||||||
|
|
||||||
|
assert result["ok"] is True and result["userId"] == 7
|
||||||
|
_assert_wiring("cian", use_pool=use_pool, environment=environment)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(("use_pool", "environment"), [(True, "production"), (False, "dev")])
|
||||||
|
@pytest.mark.usefixtures("_pool_on")
|
||||||
|
async def test_domclick_debug_detail_wires_proxy_pool(
|
||||||
|
monkeypatch: pytest.MonkeyPatch, use_pool: bool, environment: str
|
||||||
|
) -> None:
|
||||||
|
"""Debug-карточка DomClick берёт узел с provider_affinity='domclick', а не env."""
|
||||||
|
from scraper_kit.providers.domclick import detail as domclick_detail
|
||||||
|
|
||||||
|
from app.core.config import settings
|
||||||
|
|
||||||
|
monkeypatch.setattr(settings, "use_proxy_pool_browser", use_pool)
|
||||||
|
monkeypatch.setattr(settings, "environment", environment)
|
||||||
|
|
||||||
|
enrichment = SimpleNamespace(
|
||||||
|
item_id="1",
|
||||||
|
repair_state=None,
|
||||||
|
living_area_m2=None,
|
||||||
|
year_built=None,
|
||||||
|
price_changes=[],
|
||||||
|
raw_extra={},
|
||||||
|
)
|
||||||
|
body = admin.DomClickDebugDetailFetchRequest(
|
||||||
|
card_url="https://ekaterinburg.domclick.ru/card/sale__flat__1"
|
||||||
|
)
|
||||||
|
with ExitStack() as stack:
|
||||||
|
for target in _TARGETS:
|
||||||
|
stack.enter_context(patch(target, _CapturingFetcher, create=True))
|
||||||
|
stack.enter_context(
|
||||||
|
patch.object(domclick_detail, "fetch_detail", AsyncMock(return_value=enrichment))
|
||||||
|
)
|
||||||
|
stack.enter_context(
|
||||||
|
patch("app.services.domclick_session.load_session", MagicMock(return_value=None))
|
||||||
|
)
|
||||||
|
result = await admin.debug_domclick_detail_fetch(body=body, db=MagicMock())
|
||||||
|
|
||||||
|
assert result.ok is True
|
||||||
|
_assert_wiring("domclick", use_pool=use_pool, environment=environment)
|
||||||
|
|
@ -6,7 +6,7 @@ VERIFY_BAN_SENTINEL / VERIFY_SOURCE_UNAVAILABLE_SENTINEL / VERIFY_MARKUP_CHANGED
|
||||||
(403) и недоступность источника (5xx) выглядели как "куки протухли" — человек в
|
(403) и недоступность источника (5xx) выглядели как "куки протухли" — человек в
|
||||||
момент инцидента перезаливал заведомо валидные куки вместо починки egress/прокси.
|
момент инцидента перезаливал заведомо валидные куки вместо починки egress/прокси.
|
||||||
|
|
||||||
Покрытие 3 эндпоинтов (db/verify_session/BrowserFetcher мокаются, NO live network/DB),
|
Покрытие 3 эндпоинтов (db/verify_session/build_browser_fetcher мокаются, NO live network/DB),
|
||||||
зеркалит паттерн test_domclick_admin_apis.py (dependency_overrides[get_db] + TestClient):
|
зеркалит паттерн test_domclick_admin_apis.py (dependency_overrides[get_db] + TestClient):
|
||||||
- POST /api/v1/admin/scrape/cian/upload-cookies
|
- POST /api/v1/admin/scrape/cian/upload-cookies
|
||||||
- POST /api/v1/admin/scrape/cian/auto-login
|
- POST /api/v1/admin/scrape/cian/auto-login
|
||||||
|
|
@ -178,7 +178,9 @@ def test_auto_login_ban_returns_503(client: TestClient) -> None:
|
||||||
patch("app.api.v1.admin.settings.cian_login_email", "user@example.com"),
|
patch("app.api.v1.admin.settings.cian_login_email", "user@example.com"),
|
||||||
patch("app.api.v1.admin.settings.cian_login_password", "secret"),
|
patch("app.api.v1.admin.settings.cian_login_password", "secret"),
|
||||||
patch(
|
patch(
|
||||||
"app.api.v1.admin.BrowserFetcher",
|
# #3197 (хвост): ручка строит фетчер фабрикой, а не BrowserFetcher напрямую —
|
||||||
|
# только так до сайдкара доезжают proxy_provider/use_pool/environment.
|
||||||
|
"app.api.v1.admin.build_browser_fetcher",
|
||||||
return_value=_mock_browser_fetcher(_RAW_COOKIES),
|
return_value=_mock_browser_fetcher(_RAW_COOKIES),
|
||||||
),
|
),
|
||||||
patch(
|
patch(
|
||||||
|
|
@ -197,7 +199,9 @@ def test_auto_login_success_saves_and_returns_200(client: TestClient) -> None:
|
||||||
patch("app.api.v1.admin.settings.cian_login_email", "user@example.com"),
|
patch("app.api.v1.admin.settings.cian_login_email", "user@example.com"),
|
||||||
patch("app.api.v1.admin.settings.cian_login_password", "secret"),
|
patch("app.api.v1.admin.settings.cian_login_password", "secret"),
|
||||||
patch(
|
patch(
|
||||||
"app.api.v1.admin.BrowserFetcher",
|
# #3197 (хвост): ручка строит фетчер фабрикой, а не BrowserFetcher напрямую —
|
||||||
|
# только так до сайдкара доезжают proxy_provider/use_pool/environment.
|
||||||
|
"app.api.v1.admin.build_browser_fetcher",
|
||||||
return_value=_mock_browser_fetcher(_RAW_COOKIES),
|
return_value=_mock_browser_fetcher(_RAW_COOKIES),
|
||||||
),
|
),
|
||||||
patch(
|
patch(
|
||||||
|
|
|
||||||
|
|
@ -209,11 +209,11 @@ def build_browser_fetcher(
|
||||||
`proxy_provider=None` (дефолт) — валидно, но у providers больше не встречается:
|
`proxy_provider=None` (дефолт) — валидно, но у providers больше не встречается:
|
||||||
после #3382 (cian newbuilding) и #3389 (yandex newbuilding) провайдер передают
|
после #3382 (cian newbuilding) и #3389 (yandex newbuilding) провайдер передают
|
||||||
ВСЕ call-site'ы providers (avito/cian/domclick/yandex, serp и newbuilding).
|
ВСЕ call-site'ы providers (avito/cian/domclick/yandex, serp и newbuilding).
|
||||||
Без пула остались только прямые конструкции `BrowserFetcher(...)` мимо этой
|
Служебные ручки `app/api/v1/admin.py` (cian-login, domclick-detail-debug) были
|
||||||
фабрики — служебные ручки `app/api/v1/admin.py` (511 cian-login, 702
|
последними прямыми конструкциями мимо фабрики — переведены сюда же (#3197 хвост);
|
||||||
domclick-detail-debug); прямые конструкции в `orchestration/pipeline.py` и в
|
прямые конструкции в `orchestration/pipeline.py` и в backfill-задачах пул получают
|
||||||
backfill-задачах пул получают. `use_pool` при `proxy_provider is None` эффективно
|
своими kwargs. `use_pool` при `proxy_provider is None` эффективно игнорируется
|
||||||
игнорируется `BrowserFetcher` (env-fallback, см. `browser_fetcher.py::_pool_proxy`).
|
`BrowserFetcher` (env-fallback, см. `browser_fetcher.py::_pool_proxy`).
|
||||||
|
|
||||||
`fetch_timeout_s=None` (дефолт) → используется дефолт `BrowserFetcher`
|
`fetch_timeout_s=None` (дефолт) → используется дефолт `BrowserFetcher`
|
||||||
(120s). Явный таймаут передаёт ровно один call-site — `yandex/serp.py` (30s);
|
(120s). Явный таймаут передаёт ровно один call-site — `yandex/serp.py` (30s);
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue