jararaca 0.3.11a15__py3-none-any.whl → 0.3.12a0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of jararaca might be problematic. Click here for more details.

@@ -0,0 +1,141 @@
1
+ import asyncio
2
+ import logging
3
+ import random
4
+ from functools import wraps
5
+ from typing import Awaitable, Callable, Optional, ParamSpec, TypeVar
6
+
7
+ logger = logging.getLogger(__name__)
8
+
9
+ P = ParamSpec("P")
10
+ T = TypeVar("T")
11
+
12
+
13
+ class RetryConfig:
14
+ """Configuration for the retry mechanism."""
15
+
16
+ def __init__(
17
+ self,
18
+ max_retries: int = 5,
19
+ initial_delay: float = 1.0,
20
+ max_delay: float = 60.0,
21
+ backoff_factor: float = 2.0,
22
+ jitter: bool = True,
23
+ ):
24
+ """
25
+ Initialize retry configuration.
26
+
27
+ Args:
28
+ max_retries: Maximum number of retry attempts (default: 5)
29
+ initial_delay: Initial delay in seconds between retries (default: 1.0)
30
+ max_delay: Maximum delay in seconds between retries (default: 60.0)
31
+ backoff_factor: Multiplier for the delay after each retry (default: 2.0)
32
+ jitter: Whether to add randomness to the delay to prevent thundering herd (default: True)
33
+ """
34
+ self.max_retries = max_retries
35
+ self.initial_delay = initial_delay
36
+ self.max_delay = max_delay
37
+ self.backoff_factor = backoff_factor
38
+ self.jitter = jitter
39
+
40
+
41
+ E = TypeVar("E", bound=Exception)
42
+
43
+
44
+ async def retry_with_backoff(
45
+ fn: Callable[[], Awaitable[T]],
46
+ # args: P.args,
47
+ # kwargs: P.kwargs,
48
+ retry_config: Optional[RetryConfig] = None,
49
+ on_retry_callback: Optional[Callable[[int, E, float], None]] = None,
50
+ retry_exceptions: tuple[type[E], ...] = (),
51
+ ) -> T:
52
+ """
53
+ Execute a function with exponential backoff retry mechanism.
54
+
55
+ Args:
56
+ fn: The async function to execute with retry
57
+ *args: Arguments to pass to the function
58
+ retry_config: Configuration for the retry mechanism
59
+ on_retry_callback: Optional callback function called on each retry with retry count, exception, and next delay
60
+ retry_exceptions: Tuple of exception types that should trigger a retry
61
+ **kwargs: Keyword arguments to pass to the function
62
+
63
+ Returns:
64
+ The result of the function if successful
65
+
66
+ Raises:
67
+ The last exception encountered if all retries fail
68
+ """
69
+ if retry_config is None:
70
+ retry_config = RetryConfig()
71
+
72
+ last_exception = None
73
+ delay = retry_config.initial_delay
74
+
75
+ for retry_count in range(retry_config.max_retries + 1):
76
+ try:
77
+ return await fn()
78
+ except retry_exceptions as e:
79
+ last_exception = e
80
+
81
+ if retry_count >= retry_config.max_retries:
82
+ logger.error(
83
+ f"Max retries ({retry_config.max_retries}) exceeded: {str(e)}"
84
+ )
85
+ raise
86
+
87
+ # Calculate next delay with exponential backoff
88
+ if retry_count > 0: # Don't increase delay on the first failure
89
+ delay = min(delay * retry_config.backoff_factor, retry_config.max_delay)
90
+
91
+ # Apply jitter if configured (±25% randomness)
92
+ if retry_config.jitter:
93
+ jitter_amount = delay * 0.25
94
+ delay = delay + random.uniform(-jitter_amount, jitter_amount)
95
+ # Ensure delay doesn't go negative due to jitter
96
+ delay = max(delay, 0.1)
97
+
98
+ logger.warning(
99
+ f"Retry {retry_count+1}/{retry_config.max_retries} after error: {str(e)}. "
100
+ f"Retrying in {delay:.2f}s"
101
+ )
102
+
103
+ # Call the optional retry callback if provided
104
+ if on_retry_callback:
105
+ on_retry_callback(retry_count, e, delay)
106
+
107
+ await asyncio.sleep(delay)
108
+
109
+ # This should never be reached with the current implementation
110
+ if last_exception:
111
+ raise last_exception
112
+ raise RuntimeError("Unexpected error in retry logic")
113
+
114
+
115
+ def with_retry(
116
+ retry_config: Optional[RetryConfig] = None,
117
+ retry_exceptions: tuple[type[Exception], ...] = (Exception,),
118
+ ) -> Callable[[Callable[P, Awaitable[T]]], Callable[P, Awaitable[T]]]:
119
+ """
120
+ Decorator to wrap an async function with retry logic.
121
+
122
+ Args:
123
+ retry_config: Configuration for the retry mechanism
124
+ retry_exceptions: Tuple of exception types that should trigger a retry
125
+
126
+ Returns:
127
+ Decorated function with retry mechanism
128
+ """
129
+
130
+ def decorator(fn: Callable[P, Awaitable[T]]) -> Callable[P, Awaitable[T]]:
131
+ @wraps(fn)
132
+ async def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
133
+ return await retry_with_backoff(
134
+ lambda: fn(*args, **kwargs),
135
+ retry_config=retry_config,
136
+ retry_exceptions=retry_exceptions,
137
+ )
138
+
139
+ return wrapper
140
+
141
+ return decorator