"""Small, bounded retry primitives shared by outbound provider requests.""" from __future__ import annotations import time from collections.abc import Callable import requests _RETRYABLE_STATUS_CODES = frozenset({408, 425, 429, 500, 502, 503, 504}) def retry_delay(attempt: int, *, base: float = 0.5, cap: float = 8.0) -> float: """Return bounded exponential backoff for the zero-based retry attempt.""" return min(float(cap), max(0.0, float(base)) * (2 ** max(0, int(attempt)))) def request_with_retries(method: str, url: str, *, attempts: int = 3, sleep: Callable[[float], None] | None = None, **kwargs): """Make a request, retrying only transient transport/upstream failures. The final response or exception is returned to the caller unchanged. Any response discarded before a retry is closed so streaming sockets cannot accumulate while a provider is cold or rate-limiting. """ attempts = max(1, min(int(attempts), 5)) sleep = sleep or time.sleep last_response = None for attempt in range(attempts): try: response = requests.request(method, url, **kwargs) last_response = response if response.status_code not in _RETRYABLE_STATUS_CODES or attempt >= attempts - 1: return response response.close() except (requests.exceptions.ConnectionError, requests.exceptions.Timeout): if attempt >= attempts - 1: raise if attempt < attempts - 1: sleep(retry_delay(attempt)) return last_response