from __future__ import annotations import time from collections.abc import Callable from typing import TypeVar T = TypeVar("T") class RetryError(RuntimeError): def __init__(self, message: str, attempts: int, last_error: BaseException | None = None) -> None: super().__init__(message) self.attempts = attempts self.last_error = last_error def retry_call( fn: Callable[[], T], attempts: int = 5, base_delay: float = 0.2, max_delay: float = 8.0, sleeper: Callable[[float], None] = time.sleep, retry_on: tuple[type[BaseException], ...] = (Exception,), ) -> T: last: BaseException | None = None for attempt in range(1, max(1, attempts) + 1): try: return fn() except retry_on as exc: last = exc if attempt >= attempts: break delay = min(max_delay, base_delay * (2 ** (attempt - 1))) sleeper(delay) raise RetryError(f"retry exhausted after {attempts} attempts: {last}", attempts, last)