import time from typing import Callable, Optional, TypeVar import httpx from infrastructure.epc_client.exceptions import EpcRateLimitError T = TypeVar("T") def call_with_retry( fn: Callable[[], T], max_retries: int = 5, backoff_base: float = 1.0, backoff_multiplier: float = 2.0, max_backoff: float = 60.0, ) -> T: """Retry `fn` on transient EPC-API failures: HTTP 429 rate limits and transport errors (read/connect timeouts, connection resets). A 429 honours the server's `Retry-After`; transport errors back off exponentially. Non- transient failures (other 4xx/5xx, mapping errors) propagate immediately.""" last_exc: Optional[Exception] = None for attempt in range(max_retries + 1): try: return fn() except (EpcRateLimitError, httpx.TransportError) as exc: last_exc = exc if attempt < max_retries: if isinstance(exc, EpcRateLimitError) and exc.retry_after is not None: delay = exc.retry_after else: delay = backoff_base * (backoff_multiplier**attempt) time.sleep(min(delay, max_backoff)) assert last_exc is not None raise last_exc