""" Retry utilities with exponential backoff. """ import asyncio from functools import wraps from typing import Callable, TypeVar, Any, Tuple, Type import logging from tenacity import ( retry, stop_after_attempt, wait_exponential, retry_if_exception_type, before_sleep_log, ) logger = logging.getLogger(__name__) T = TypeVar("T") def retry_with_backoff( max_attempts: int = 3, min_wait: float = 1.0, max_wait: float = 60.0, exceptions: Tuple[Type[Exception], ...] = (Exception,), ) -> Callable: """ Decorator for retrying functions with exponential backoff. Args: max_attempts: Maximum number of retry attempts min_wait: Minimum wait time between retries (seconds) max_wait: Maximum wait time between retries (seconds) exceptions: Tuple of exception types to retry on Returns: Decorated function with retry logic """ return retry( stop=stop_after_attempt(max_attempts), wait=wait_exponential(multiplier=1, min=min_wait, max=max_wait), retry=retry_if_exception_type(exceptions), before_sleep=before_sleep_log(logger, logging.WARNING), reraise=True, ) async def async_retry_with_backoff( func: Callable[..., T], *args, max_attempts: int = 3, min_wait: float = 1.0, max_wait: float = 60.0, exceptions: Tuple[Type[Exception], ...] = (Exception,), **kwargs, ) -> T: """ Async function with retry logic and exponential backoff. Args: func: Async function to call *args: Positional arguments for func max_attempts: Maximum number of retry attempts min_wait: Minimum wait time between retries (seconds) max_wait: Maximum wait time between retries (seconds) exceptions: Tuple of exception types to retry on **kwargs: Keyword arguments for func Returns: Result of the function call """ last_exception = None wait_time = min_wait for attempt in range(1, max_attempts + 1): try: return await func(*args, **kwargs) except exceptions as e: last_exception = e if attempt < max_attempts: logger.warning( f"Attempt {attempt}/{max_attempts} failed: {e}. " f"Retrying in {wait_time:.1f}s..." ) await asyncio.sleep(wait_time) wait_time = min(wait_time * 2, max_wait) else: logger.error(f"All {max_attempts} attempts failed: {e}") raise last_exception