Files
ohm_streaming/app/scrapers/http.py
T
Roman 65761885ba Corrections : test Torznab périmé et retries sur erreurs définitives
- test_torznab_search_requires_query attendait un 400 mais le commit
  853b4e0 a volontairement fait servir les nouveautés quand q est
  absent (RSS sync Sonarr) — le test valide désormais ce comportement.
- fetch() ne retente plus les 4xx définitifs (sauf 408/429) : un
  lecteur VoirAnime en échec 422 coûtait 2 requêtes et 3 s de pauses
  avant de basculer sur le lecteur suivant.
- 4 nouveaux tests unitaires du client HTTP (fail-fast + retries).
2026-09-23 12:42:24 +00:00

75 lines
2.4 KiB
Python

"""Client HTTP partagé pour le scraping (httpx async, headers navigateur, retries)."""
import asyncio
import logging
import httpx
from bs4 import BeautifulSoup
from app.config import get_settings
from app.scrapers.base import ScrapeError
logger = logging.getLogger(__name__)
_client: httpx.AsyncClient | None = None
def get_client() -> httpx.AsyncClient:
global _client
if _client is None:
settings = get_settings()
_client = httpx.AsyncClient(
timeout=settings.http_timeout,
follow_redirects=True,
headers={
"User-Agent": settings.user_agent,
"Accept-Language": "fr-FR,fr;q=0.9,en;q=0.8",
},
)
return _client
async def close_client() -> None:
global _client
if _client is not None:
await _client.aclose()
_client = None
async def fetch(
url: str,
*,
referer: str | None = None,
retries: int = 2,
headers: dict[str, str] | None = None,
) -> str:
"""GET d'une page avec retries ; lève ScrapeError en cas d'échec définitif."""
request_headers = dict(headers) if headers else {}
if referer:
request_headers.setdefault("Referer", referer)
last_error: Exception | None = None
for attempt in range(retries + 1):
try:
response = await get_client().get(url, headers=request_headers)
response.raise_for_status()
return response.text
except httpx.HTTPStatusError as exc:
status = exc.response.status_code
if 400 <= status < 500 and status not in (408, 429):
raise ScrapeError(f"Échec de récupération de {url} : HTTP {status} (définitif)") from exc
last_error = exc
logger.warning("fetch %s — HTTP %d, tentative %d/%d", url, status, attempt + 1, retries + 1)
if attempt < retries:
await asyncio.sleep(1.0 * (attempt + 1))
except (httpx.HTTPError, httpx.InvalidURL) as exc:
last_error = exc
logger.warning("fetch %s — tentative %d/%d : %s", url, attempt + 1, retries + 1, exc)
if attempt < retries:
await asyncio.sleep(1.0 * (attempt + 1))
raise ScrapeError(f"Échec de récupération de {url} : {last_error}")
async def fetch_soup(url: str, *, referer: str | None = None) -> BeautifulSoup:
html = await fetch(url, referer=referer)
return BeautifulSoup(html, "lxml")