"""Классификация HTTP-статусов IMV: временное не должно становиться приговором. Замер на проде 01.09.2026: 93 дома заперты в терминальном imv_status='error', из них 19 — с причиной «HTTP 429». Rate-limit — канонический ВРЕМЕННЫЙ отказ, и механика повтора для таких существует (transient_error + retry-lane), но 429 проваливался в общий 4xx-fall-through `_raise_for_status_categorized` и приезжал в house_imv_backfill голым RuntimeError. Тот типизирует только IMV*-исключения → дом получал 'error' и выпадал из повторных пакетов навсегда. Тест держит ГРАНИЦУ классификации с обеих сторон: 429/408 — transient, 400 — по-прежнему НЕ transient (тело безликое «Bad Request», оснований считать его временным нет; сделать 400 ретраебельным значило бы вечно долбить дома с реально кривыми параметрами). """ from __future__ import annotations import pytest from scraper_kit.providers.avito.imv import ( IMVAuthError, IMVTransientError, _raise_for_status_categorized, ) class _Resp: def __init__(self, status: int, body: str = "") -> None: self.status_code = status self.text = body def raise_for_status(self) -> None: if self.status_code >= 400: raise RuntimeError(f"HTTP {self.status_code}") @pytest.mark.parametrize("status", [429, 408]) def test_rate_limit_and_timeout_are_transient(status: int) -> None: with pytest.raises(IMVTransientError): _raise_for_status_categorized(_Resp(status), "imv-evaluate") @pytest.mark.parametrize("status", [500, 502, 503]) def test_5xx_stays_transient(status: int) -> None: with pytest.raises(IMVTransientError): _raise_for_status_categorized(_Resp(status), "imv-evaluate") @pytest.mark.parametrize("status", [401, 403]) def test_auth_stays_auth(status: int) -> None: with pytest.raises(IMVAuthError): _raise_for_status_categorized(_Resp(status), "imv-evaluate") def test_400_is_not_transient() -> None: # Обратная сторона границы: безликий Bad Request не становится # ретраебельным — иначе дома с кривыми параметрами долбились бы вечно. with pytest.raises(RuntimeError) as ei: _raise_for_status_categorized(_Resp(400, '{"code":400}'), "imv-evaluate") assert not isinstance(ei.value, IMVTransientError) assert "400" in str(ei.value) def test_2xx_passes_silently() -> None: _raise_for_status_categorized(_Resp(200), "imv-evaluate")