"""Retry wrappers and exception hierarchy.""" from __future__ import annotations import functools from collections.abc import Callable from typing import Any, TypeVar from tenacity import ( RetryError, retry, retry_if_exception_type, stop_after_attempt, wait_exponential, ) F = TypeVar("F", bound=Callable[..., Any]) class ACEFError(Exception): """Base error for ACE-F.""" def __init__( self, message: str, source: str = "", entity: str = "", context: dict[str, Any] | None = None, ) -> None: super().__init__(message) self.source = source self.entity = entity self.context = context or {} class RetryableError(ACEFError): """Transient error that can be retried (network, timeout, 5xx).""" class NonRetryableError(ACEFError): """Permanent error that must not be retried (404, business rule).""" class ValidationError(ACEFError): """Schema or data validation failure — never retried.""" class DependencyError(ACEFError): """Required upstream dependency unavailable.""" def with_retry( max_attempts: int = 3, min_wait: float = 1.0, max_wait: float = 30.0, multiplier: float = 2.0, ) -> Callable[[F], F]: """Decorator: retry on RetryableError with exponential backoff.""" def decorator(func: F) -> F: @retry( retry=retry_if_exception_type(RetryableError), stop=stop_after_attempt(max_attempts), wait=wait_exponential(multiplier=multiplier, min=min_wait, max=max_wait), reraise=True, ) @functools.wraps(func) async def wrapper(*args: Any, **kwargs: Any) -> Any: return await func(*args, **kwargs) return wrapper # type: ignore[return-value] return decorator __all__ = [ "ACEFError", "RetryableError", "NonRetryableError", "ValidationError", "DependencyError", "with_retry", "RetryError", ]