0
# Retry Callers
1
2
Reusable retry caller classes that enable pre-configuring retry parameters and applying them to multiple functions. These classes are ideal when you need consistent retry behavior across different operations or want to separate retry configuration from business logic.
3
4
## Capabilities
5
6
### Synchronous Retry Caller
7
8
The `RetryingCaller` class provides a reusable interface for calling functions with consistent retry parameters.
9
10
```python { .api }
11
class RetryingCaller:
12
"""
13
Reusable caller for retrying functions with pre-configured parameters.
14
15
Instances can be reused as they create new retry contexts on each call.
16
"""
17
18
def __init__(
19
self,
20
attempts: int | None = 10,
21
timeout: float | datetime.timedelta | None = 45.0,
22
wait_initial: float | datetime.timedelta = 0.1,
23
wait_max: float | datetime.timedelta = 5.0,
24
wait_jitter: float | datetime.timedelta = 1.0,
25
wait_exp_base: float = 2.0,
26
):
27
"""
28
Initialize retry caller with default parameters.
29
30
Parameters: Same as retry() decorator
31
"""
32
33
def __call__(
34
self,
35
on: ExcOrPredicate,
36
callable_: Callable[P, T],
37
/,
38
*args: P.args,
39
**kwargs: P.kwargs,
40
) -> T:
41
"""
42
Call callable with retries if specified exceptions are raised.
43
44
Parameters:
45
- on: Exception(s) to retry on
46
- callable_: Function to call
47
- args: Positional arguments for the function
48
- kwargs: Keyword arguments for the function
49
50
Returns:
51
Return value of the callable
52
"""
53
54
def on(self, on: ExcOrPredicate, /) -> BoundRetryingCaller:
55
"""
56
Create bound caller pre-configured for specific exception types.
57
58
Returns:
59
BoundRetryingCaller instance bound to the exception type
60
"""
61
```
62
63
**Usage Examples:**
64
65
```python
66
import stamina
67
import httpx
68
69
# Create reusable caller with custom parameters
70
http_caller = stamina.RetryingCaller(
71
attempts=5,
72
timeout=30.0,
73
wait_initial=0.5,
74
wait_max=10.0
75
)
76
77
# Call different functions with same retry behavior
78
def fetch_user(user_id):
79
response = httpx.get(f"/users/{user_id}")
80
response.raise_for_status()
81
return response.json()
82
83
def fetch_posts(user_id):
84
response = httpx.get(f"/users/{user_id}/posts")
85
response.raise_for_status()
86
return response.json()
87
88
# Use the same caller for multiple operations
89
user = http_caller(httpx.HTTPError, fetch_user, 123)
90
posts = http_caller(httpx.HTTPError, fetch_posts, 123)
91
92
# Direct function calls
93
result = http_caller(
94
httpx.HTTPError,
95
lambda: httpx.get("https://api.example.com/data").json(),
96
)
97
98
# Call with additional arguments
99
result = http_caller(
100
ValueError,
101
process_data,
102
input_data,
103
format="json",
104
validate=True
105
)
106
```
107
108
### Bound Retry Caller
109
110
The `BoundRetryingCaller` class is created by calling `RetryingCaller.on()` and provides a simpler interface when you always retry on the same exception types.
111
112
```python { .api }
113
class BoundRetryingCaller:
114
"""
115
RetryingCaller pre-bound to specific exception types.
116
117
Created by RetryingCaller.on() - do not instantiate directly.
118
"""
119
120
def __call__(
121
self,
122
callable_: Callable[P, T],
123
/,
124
*args: P.args,
125
**kwargs: P.kwargs
126
) -> T:
127
"""
128
Call callable with bound exception handling.
129
130
Parameters:
131
- callable_: Function to call
132
- args: Positional arguments for the function
133
- kwargs: Keyword arguments for the function
134
135
Returns:
136
Return value of the callable
137
"""
138
```
139
140
**Usage Examples:**
141
142
```python
143
# Create caller bound to specific exception
144
http_caller = stamina.RetryingCaller(attempts=3, timeout=15.0)
145
bound_caller = http_caller.on(httpx.HTTPError)
146
147
# Cleaner syntax for repeated operations
148
user = bound_caller(fetch_user, 123)
149
posts = bound_caller(fetch_posts, 123)
150
profile = bound_caller(fetch_profile, 123)
151
152
# Method chaining
153
result = (stamina.RetryingCaller(attempts=5)
154
.on(ConnectionError)
155
(connect_to_service, host="localhost", port=8080))
156
```
157
158
### Asynchronous Retry Caller
159
160
The `AsyncRetryingCaller` provides the same interface as `RetryingCaller` but for asynchronous functions.
161
162
```python { .api }
163
class AsyncRetryingCaller:
164
"""
165
Async version of RetryingCaller for async functions.
166
"""
167
168
def __init__(
169
self,
170
attempts: int | None = 10,
171
timeout: float | datetime.timedelta | None = 45.0,
172
wait_initial: float | datetime.timedelta = 0.1,
173
wait_max: float | datetime.timedelta = 5.0,
174
wait_jitter: float | datetime.timedelta = 1.0,
175
wait_exp_base: float = 2.0,
176
):
177
"""Initialize async retry caller with default parameters."""
178
179
async def __call__(
180
self,
181
on: ExcOrPredicate,
182
callable_: Callable[P, Awaitable[T]],
183
/,
184
*args: P.args,
185
**kwargs: P.kwargs,
186
) -> T:
187
"""
188
Async call callable with retries.
189
190
Parameters: Same as RetryingCaller.__call__ but callable_ must be async
191
192
Returns:
193
Awaited return value of the callable
194
"""
195
196
def on(self, on: ExcOrPredicate, /) -> BoundAsyncRetryingCaller:
197
"""Create bound async caller for specific exception types."""
198
```
199
200
**Usage Examples:**
201
202
```python
203
import asyncio
204
import stamina
205
import httpx
206
207
# Create async retry caller
208
async_caller = stamina.AsyncRetryingCaller(
209
attempts=3,
210
timeout=20.0,
211
wait_max=5.0
212
)
213
214
async def fetch_async(url):
215
async with httpx.AsyncClient() as client:
216
response = await client.get(url)
217
response.raise_for_status()
218
return response.json()
219
220
async def process_async(data):
221
# Simulate async processing
222
await asyncio.sleep(0.1)
223
if not data:
224
raise ValueError("Invalid data")
225
return {"processed": data}
226
227
# Use async caller
228
async def main():
229
# Call different async functions
230
data = await async_caller(httpx.HTTPError, fetch_async, "https://api.example.com/data")
231
result = await async_caller(ValueError, process_async, data)
232
233
# With bound caller
234
bound_caller = async_caller.on(httpx.HTTPError)
235
user = await bound_caller(fetch_async, "https://api.example.com/user/123")
236
237
return result
238
239
# Run async code
240
result = asyncio.run(main())
241
```
242
243
### Bound Async Retry Caller
244
245
The `BoundAsyncRetryingCaller` is the async equivalent of `BoundRetryingCaller`.
246
247
```python { .api }
248
class BoundAsyncRetryingCaller:
249
"""
250
AsyncRetryingCaller pre-bound to specific exception types.
251
252
Created by AsyncRetryingCaller.on() - do not instantiate directly.
253
"""
254
255
async def __call__(
256
self,
257
callable_: Callable[P, Awaitable[T]],
258
/,
259
*args: P.args,
260
**kwargs: P.kwargs,
261
) -> T:
262
"""Async call callable with bound exception handling."""
263
```
264
265
**Usage Examples:**
266
267
```python
268
# Bound async caller usage
269
async_caller = stamina.AsyncRetryingCaller(attempts=5)
270
http_bound = async_caller.on(httpx.HTTPError)
271
272
async def fetch_multiple_endpoints():
273
# Use same bound caller for multiple async operations
274
users = await http_bound(fetch_async, "https://api.example.com/users")
275
posts = await http_bound(fetch_async, "https://api.example.com/posts")
276
comments = await http_bound(fetch_async, "https://api.example.com/comments")
277
278
return {"users": users, "posts": posts, "comments": comments}
279
```
280
281
## Advanced Usage Patterns
282
283
### Configuration Factories
284
285
Create caller factories for different retry strategies:
286
287
```python
288
def create_http_caller(timeout=30.0):
289
"""Factory for HTTP-specific retry callers."""
290
return stamina.RetryingCaller(
291
attempts=3,
292
timeout=timeout,
293
wait_initial=0.5,
294
wait_max=5.0
295
).on(httpx.HTTPError)
296
297
def create_db_caller():
298
"""Factory for database-specific retry callers."""
299
return stamina.RetryingCaller(
300
attempts=5,
301
timeout=60.0,
302
wait_initial=1.0,
303
wait_max=30.0
304
).on(DatabaseError)
305
306
# Use factories
307
http_caller = create_http_caller(timeout=15.0)
308
db_caller = create_db_caller()
309
310
user = http_caller(fetch_user, 123)
311
records = db_caller(query_database, "SELECT * FROM users")
312
```
313
314
### Caller Composition
315
316
Combine callers for complex scenarios:
317
318
```python
319
# Different retry strategies for different operations
320
fast_caller = stamina.RetryingCaller(attempts=3, wait_max=2.0)
321
slow_caller = stamina.RetryingCaller(attempts=10, wait_max=30.0)
322
323
def robust_data_processing(data):
324
# Quick retries for validation
325
validated = fast_caller(ValueError, validate_data, data)
326
327
# Longer retries for expensive processing
328
processed = slow_caller(ProcessingError, process_data, validated)
329
330
# Quick retries for saving
331
result = fast_caller(IOError, save_result, processed)
332
333
return result
334
```
335
336
### Context-Aware Callers
337
338
Use caller state for contextual retry decisions:
339
340
```python
341
class ContextualRetryingCaller:
342
def __init__(self, base_caller):
343
self.base_caller = base_caller
344
self.attempt_counts = {}
345
346
def __call__(self, operation_id, on, callable_, *args, **kwargs):
347
# Track attempts per operation
348
self.attempt_counts[operation_id] = self.attempt_counts.get(operation_id, 0) + 1
349
350
try:
351
return self.base_caller(on, callable_, *args, **kwargs)
352
except Exception:
353
print(f"Operation {operation_id} failed after {self.attempt_counts[operation_id]} attempts")
354
raise
355
356
# Usage with context
357
contextual_caller = ContextualRetryingCaller(
358
stamina.RetryingCaller(attempts=3)
359
)
360
361
result = contextual_caller(
362
"fetch_user_123",
363
httpx.HTTPError,
364
fetch_user,
365
123
366
)
367
```