gendesign/tradein-mvp/packages/scraper-kit/src/scraper_kit/avito_exceptions.py
bot-backend 4902d1e29e
All checks were successful
CI Trade-In / changes (pull_request) Successful in 10s
CI / changes (pull_request) Successful in 13s
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 5m12s
chore(tradein): четыре символа, на которые нет ни одной ссылки
Найдено сверкой двух независимых источников: подсчёт упоминаний по всем
python-файлам (включая тесты) дал имена, встречающиеся ровно один раз — в
собственном объявлении; каждое затем подтверждено serena find_referencing_symbols
(LSP, видит и косвенные ссылки) и проверено grep'ом по yml/sql/ts/md на случай
ссылки строкой.

Удалено:
  * _qr_code_data_url (trade_in_pdf) — QR как SVG data URL, не вызывался
    ниоткуда; вместе с ним ушёл осиротевший import segno. `io` ОСТАВЛЕН —
    io.BytesIO используется дальше по файлу (строка 774);
  * _local_name (gar_flats_loader) — снятие namespace с имени тега;
  * AvitoParseError — класс никто не поднимает и не ловит;
  * IMVCityMismatchError — то же.

Что НЕ тронуто, хотя фильтр их показал:
  * ~200 обработчиков FastAPI и celery-задач — их поднимает декоратор, по имени
    их действительно никто не зовёт;
  * refresh_ddu_price_indicator — точка ручного обслуживания, задокументирована
    в комментарии к матвью (data/sql/152_mv_ddu_price_indicator.sql: «Refresh:
    ... (не в beat)»);
  * schemas/parcel.py::MarketPrice — половина контракта, фронт использует
    (frontend/src/types/site-finder.ts: market_price?: MarketPrice);
  * JobSetting — SQLAlchemy-модель, живёт через metadata Base.

segno остался в backend/pyproject.toml и больше нигде не используется — снятие
зависимости требует пересборки лока, поэтому отдельным PR.

ruff clean, 4974 passed / 37 skipped.
2026-08-29 15:59:54 +03:00

73 lines
4.5 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Avito-specific exceptions для anti-bot detection."""
from scraper_kit.proxy_errors import ProxyBanError
class AvitoError(Exception):
"""Base для всех Avito scraper exceptions."""
class AvitoBlockedError(AvitoError, ProxyBanError):
"""HTTP 403 от Avito — IP-level block detected.
Дополнительно наследует `ProxyBanError` (#2600 п.1) — генерик curl-путь
(`providers/_proxy.py::curl_proxy_url`) распознаёт это как «площадка забанила
прокси» через `isinstance(exc, ProxyBanError)`, не импортируя avito_exceptions
напрямую. `except AvitoBlockedError:` во всём остальном коде не меняется —
mixin ничего не убирает, только добавляет.
"""
class AvitoRateLimitedError(AvitoError):
"""HTTP 429 от Avito — rate limit triggered."""
class AvitoSidecarUnavailableError(AvitoRateLimitedError):
"""НАШ браузерный сайдкар не отдал страницу — площадка ни при чём (#2686).
Поднимается только там, где отказ ПОРОЖДЁН нашей инфраструктурой: sidecar
вернул 503 «browser unavailable», упал внутри (Page.goto Timeout, браузер не
поднялся) либо соединение до него оборвалось (ReadTimeout/ConnectError). Это
отличается от AvitoBlockedError (площадка показала firewall) и от честного
HTTP 429 самой площадки, хотя все три раньше давали ОДИН статус 'banned'
92 прогона из 114 (81%) за 45 суток были нашим сайдкаром, и по этому статусу
приняли решение замедлить сбор вдвое (миграция 206).
Наследует AvitoRateLimitedError НАМЕРЕННО: всё поведение прогона обязано
остаться прежним (`except (AvitoBlockedError, AvitoRateLimitedError)` в
pipeline/full-load/backfill ловят его без изменений → mark_banned →
done_buckets-чекпоинт сохраняется). Разводятся не пути, а ДИАГНОЗ:
pipeline пишет его в scrape_runs.ban_kind ('infra' vs 'platform').
"""
class AvitoListingGoneError(AvitoError):
"""Объявление удалено / вернуло 404 (страница «Ошибка 404» вместо item-view).
Отдельный сигнал от AvitoBlockedError/AvitoRateLimitedError: 404 — НЕ анти-бот
deflect, а мёртвый листинг (его незачем ротировать/ретраить). Наследует общий
AvitoError, но НЕ AvitoBlockedError — чтобы backfill-ловушка
`except (AvitoBlockedError, AvitoRateLimitedError)` его НЕ перехватывала и не крутила
consecutive-block breaker на мёртвых координатных дырах (run 458: 5 dead → abort).
"""
class AvitoContentBlockedError(AvitoBlockedError):
"""HTTP 200 от Avito, но 0 карточек на первой странице — content-block / captcha.
Наследует AvitoBlockedError, чтобы существующие except-блоки на AvitoBlockedError
продолжали ловить этот сигнал (mark_banned, pipeline abort и т.д.).
"""
class AvitoWarmupCookiesMissingError(AvitoBlockedError):
"""Прогрев вернул HTTP 200 без firewall-маркеров, но антибот-cookies
(`__zzatw-*`/`cfidsw-*`) НЕ появились в сессии (#3034).
Тихий soft-block, не пойманный `_is_firewall_page`/`_is_detail_soft_block`: страница
отдалась, но сессия не помечена площадкой как прогретая — детейл-батч на ней сжигал
бы прокси на обречённых запросах (403 на каждой карточке). Наследует AvitoBlockedError,
чтобы существующие `except (AvitoBlockedError, AvitoRateLimitedError)` (ban_kind,
proxy-ротация, consecutive-block breaker в pipeline/backfill) отработали как на обычном
блоке площадки, без отдельного except-блока на call-сайтах.
"""