You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

84 lines
1.9 KiB
Python

"""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",
]