Some checks failed
CI Trade-In / changes (pull_request) Successful in 8s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / changes (pull_request) Successful in 10s
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) Failing after 1m3s
#3034 свёл impersonate к единственной константе внутри scraper_kit и поднял профиль до chrome146. Сторож литерала сканирует только пакет, а его докстрока объявила остальное «отдельным периметром вне scope», сославшись на #2361 F4a. Периметр не спящий — он ходит в сеть каждый день, а #2361 к тому моменту был закрыт, то есть отсылка вела в никуда. На chrome120 оставались: app/services/cian_session.py:164 верификация куки Циана app/services/yandex_address_backfill.py:153 бэкфилл адресов app/tasks/yandex_detail_backfill.py:303 detail-бэкфилл Разрыв в 31 мажорную версию живёт в TLS-отпечатке (JA3/JA4), а не в строке User-Agent, поэтому сменой прокси он не лечится. ПРО ЦИАН ОТДЕЛЬНО. По #2673 оценка Циана мертва с 29 июня — «куки протухли, ни одной новой строки 37 дней». Путь, которым проверяется живость этих куки, всё это время представлялся площадке браузером двухлетней давности. Причину этим не объявляю: утверждаю, что при таком отпечатке отличить «куки протухли» от «нас узнали по рукопожатию» нечем. ТЕСТ ЗАКРЕПЛЯЛ ДЕФЕКТ. test_cian_session прибивал chrome120 гвоздём: подъём профиля в kit ронял бы этот тест, а «починкой» выглядел бы возврат к устаревшему профилю. Теперь тест сверяется с DEFAULT_IMPERSONATE. Сторож литерала расширен на backend/app — без этого периметр возвращается молча, что уже один раз и произошло. Намеренно НЕ входят tests/fixtures/** (номер профиля там — часть записи о том, чем снят фикстур-HTML) и scripts/** (разовые инструменты, в прод-путях не участвуют). Исторические замеры в комментариях сохранены как замеры: «curl_cffi с kit-профилем (на момент замера — Chrome 120)» вместо переписывания истории. Проверено: сканер сторожа на дереве даёт ноль нарушителей, на подсаженном литерале краснеет; все изменённые модули компилируются. Refs #3148
82 lines
4.9 KiB
Python
82 lines
4.9 KiB
Python
"""Единственный источник правды для curl_cffi impersonate-профиля (#3034).
|
||
|
||
До этого фикса `impersonate="chrome120"` был захардкожен литералом в 9+ местах
|
||
scraper_kit (`providers/avito/{serp,detail,imv,houses}.py`,
|
||
`orchestration/pipeline.py` x4, `providers/cian/valuation.py`,
|
||
`providers/yandex/valuation.py`) — обновить TLS-профиль значило найти и
|
||
поправить их все вручную, и один пропущенный литерал молча гонял бы устаревший
|
||
профиль. Теперь единственный источник — `DEFAULT_IMPERSONATE`
|
||
(`providers/_base.py`); любой caller обязан читать константу.
|
||
|
||
ПОЧЕМУ ГРЕП, А НЕ AST. Инвариант — «строка `chrome120` не встречается нигде в
|
||
дереве» (ни в коде, ни в docstring/комментарии) — буквально то, что нужно
|
||
поймать: сменится профиль — не должно остаться ни одного места, которое надо
|
||
будет вспомнить и найти руками. AST увидел бы только строковые константы в
|
||
выражениях (kwarg-значения, docstring), но не поймал бы упоминание в
|
||
`#`-комментарии — а именно в комментарии литерал незаметнее всего переживёт
|
||
следующий рефактор.
|
||
|
||
Область — ДВЕ директории:
|
||
|
||
* `packages/scraper-kit/src/scraper_kit/` — пакет, где живёт `DEFAULT_IMPERSONATE`
|
||
и все providers;
|
||
* `backend/app/` — прод-код бэкенда. Добавлен после того, как выяснилось, что
|
||
первая волна #3034 вычистила kit, а три живых прод-пути остались на
|
||
`chrome120`: верификация куки Циана (`services/cian_session.py`) и два
|
||
яндексовых бэкфилла. Оговорка «legacy-периметр вне scope» разошлась с фактом:
|
||
периметр не спит, он ходит в сеть каждый день, и та же ссылка (#2361 F4a)
|
||
к тому моменту была закрыта, то есть отсылать было уже некуда.
|
||
|
||
НЕ входят: `tests/fixtures/**` (там номер профиля — часть записи о том, чем
|
||
снят фикстур-HTML, историю переписывать нельзя) и `scripts/**` (разовые
|
||
инструменты, в прод-путях не участвуют).
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from pathlib import Path
|
||
|
||
_BACKEND_ROOT = Path(__file__).resolve().parents[1]
|
||
_KIT_SRC = _BACKEND_ROOT.parent / "packages" / "scraper-kit" / "src" / "scraper_kit"
|
||
_APP_SRC = _BACKEND_ROOT / "app"
|
||
|
||
_SCAN_ROOTS = (_KIT_SRC, _APP_SRC)
|
||
|
||
_BANNED_LITERAL = "chrome120"
|
||
|
||
|
||
def test_scan_area_exists() -> None:
|
||
"""Обе области сканирования живы — иначе сторож зелен вхолостую (путь съехал)."""
|
||
for root in _SCAN_ROOTS:
|
||
assert root.is_dir(), f"область сканирования съехала: {root}"
|
||
|
||
|
||
def test_detector_actually_detects(tmp_path: Path) -> None:
|
||
"""Сторож обязан уметь краснеть на литерале — иначе он зелен вхолостую."""
|
||
probe = tmp_path / "probe.py"
|
||
probe.write_text('impersonate = "chrome120"\n', encoding="utf-8")
|
||
assert _BANNED_LITERAL in probe.read_text(encoding="utf-8")
|
||
|
||
clean = tmp_path / "clean.py"
|
||
clean.write_text(
|
||
"from scraper_kit.providers._base import DEFAULT_IMPERSONATE\n", encoding="utf-8"
|
||
)
|
||
assert _BANNED_LITERAL not in clean.read_text(encoding="utf-8")
|
||
|
||
|
||
def test_default_impersonate_is_the_only_chrome_profile_literal() -> None:
|
||
"""Ни один файл scraper_kit не содержит строку "chrome120" — ни в коде, ни в
|
||
docstring/комментарии. Единственное разрешённое место для конкретного номера
|
||
профиля — значение `DEFAULT_IMPERSONATE` в `providers/_base.py`.
|
||
"""
|
||
offenders = [
|
||
str(path.relative_to(_BACKEND_ROOT.parent))
|
||
for root in _SCAN_ROOTS
|
||
for path in sorted(root.rglob("*.py"))
|
||
if _BANNED_LITERAL in path.read_text(encoding="utf-8")
|
||
]
|
||
assert offenders == [], (
|
||
f"{offenders}: литерал {_BANNED_LITERAL!r} обходит DEFAULT_IMPERSONATE "
|
||
"(providers/_base.py) — единственный источник правды для impersonate-"
|
||
"профиля. Прочитай константу вместо хардкода строки."
|
||
)
|