backon 3.0.0__tar.gz → 3.2.0__tar.gz
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.
- backon-3.2.0/PKG-INFO +353 -0
- backon-3.2.0/README.md +327 -0
- backon-3.2.0/backon/__init__.py +87 -0
- {backon-3.0.0 → backon-3.2.0}/backon/_async.py +8 -8
- backon-3.2.0/backon/_conditions.py +239 -0
- backon-3.2.0/backon/_decorator.py +271 -0
- backon-3.2.0/backon/_retry.py +844 -0
- backon-3.2.0/backon/_state.py +75 -0
- {backon-3.0.0 → backon-3.2.0}/backon/_sync.py +1 -1
- {backon-3.0.0 → backon-3.2.0}/backon/_typing.py +14 -21
- backon-3.2.0/backon/_wait_gen.py +212 -0
- {backon-3.0.0 → backon-3.2.0}/pyproject.toml +10 -5
- backon-3.2.0/tests/test_advanced_features.py +852 -0
- {backon-3.0.0 → backon-3.2.0}/tests/test_backon.py +28 -1
- {backon-3.0.0 → backon-3.2.0}/tests/test_retry.py +1 -1
- backon-3.0.0/PKG-INFO +0 -179
- backon-3.0.0/README.md +0 -153
- backon-3.0.0/backon/__init__.py +0 -23
- backon-3.0.0/backon/_decorator.py +0 -150
- backon-3.0.0/backon/_retry.py +0 -464
- backon-3.0.0/backon/_wait_gen.py +0 -62
- {backon-3.0.0 → backon-3.2.0}/LICENSE +0 -0
- {backon-3.0.0 → backon-3.2.0}/backon/_common.py +0 -0
- {backon-3.0.0 → backon-3.2.0}/backon/_jitter.py +0 -0
- {backon-3.0.0 → backon-3.2.0}/backon/py.typed +0 -0
- {backon-3.0.0 → backon-3.2.0}/backon/types.py +0 -0
- {backon-3.0.0 → backon-3.2.0}/tests/__init__.py +0 -0
- {backon-3.0.0 → backon-3.2.0}/tests/test_backon_async.py +0 -0
- {backon-3.0.0 → backon-3.2.0}/tests/test_backon_predicate.py +0 -0
- {backon-3.0.0 → backon-3.2.0}/tests/test_backon_sync.py +0 -0
- {backon-3.0.0 → backon-3.2.0}/tests/test_features.py +0 -0
- {backon-3.0.0 → backon-3.2.0}/tests/test_jitter.py +0 -0
- {backon-3.0.0 → backon-3.2.0}/tests/test_types.py +0 -0
- {backon-3.0.0 → backon-3.2.0}/tests/test_typing.py +0 -0
- {backon-3.0.0 → backon-3.2.0}/tests/test_wait_gen.py +0 -0
backon-3.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: backon
|
|
3
|
+
Version: 3.2.0
|
|
4
|
+
Summary: Function decoration for backoff and retry
|
|
5
|
+
Keywords: retry,backoff,decorators
|
|
6
|
+
Author-Email: Llucs <c307lucas@gmail.com>
|
|
7
|
+
License: MIT
|
|
8
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Natural Language :: English
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
|
+
Classifier: Topic :: Utilities
|
|
21
|
+
Project-URL: Homepage, https://github.com/Llucs/backon
|
|
22
|
+
Project-URL: Repository, https://github.com/Llucs/backon
|
|
23
|
+
Project-URL: Issues, https://github.com/Llucs/backon/issues
|
|
24
|
+
Requires-Python: >=3.10
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# backon
|
|
28
|
+
|
|
29
|
+
> Function decoration for backoff and retry — modern, fast, zero dependencies.
|
|
30
|
+
|
|
31
|
+
[](https://github.com/Llucs/backon/actions/workflows/ci.yml)
|
|
32
|
+
[](https://github.com/Llucs/backon/actions/workflows/codeql.yml)
|
|
33
|
+
[](https://pypi.org/project/backon/)
|
|
34
|
+
[](https://pypi.org/project/backon/)
|
|
35
|
+
[](https://github.com/Llucs/backon/blob/main/LICENSE)
|
|
36
|
+
|
|
37
|
+
backon is a modern evolution of [backoff](https://github.com/litl/backoff) — a zero-dependency Python library for retry with exponential backoff. It provides decorator, functional, and context manager APIs for both sync and async code.
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## Features
|
|
42
|
+
|
|
43
|
+
- **Zero dependencies** — pure Python, stdlib only
|
|
44
|
+
- **Three APIs** — decorator (`@on_exception`, `@on_predicate`), functional (`retry()`), context manager (`Retrying`)
|
|
45
|
+
- **Async native** — same API works for `async def` functions
|
|
46
|
+
- **Full type hints** — validated with mypy, strict mode compatible
|
|
47
|
+
- **Global toggle** — `backon.disable()` / `backon.enable()` for testing
|
|
48
|
+
- **Custom sleep** — inject your own sleep function (useful for testing with `asyncio.Event`)
|
|
49
|
+
- **Multiple wait strategies** — exponential, constant, Fibonacci, decay, runtime, and composable chains
|
|
50
|
+
- **Jitter** — full jitter, random jitter, or none
|
|
51
|
+
- **Rich callbacks** — `on_attempt`, `on_backoff`, `on_success`, `on_giveup`, `before_sleep`
|
|
52
|
+
- **Modern packaging** — PEP 621, PDM, py.typed
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Installation
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
pip install backon
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Requires Python 3.10+.
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Quick Start
|
|
67
|
+
|
|
68
|
+
### Retry on exception
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
import backon
|
|
72
|
+
|
|
73
|
+
@backon.on_exception(backon.expo, ValueError, max_tries=3)
|
|
74
|
+
def fetch_data():
|
|
75
|
+
return api.call()
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Retry on predicate
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
@backon.on_predicate(backon.constant, max_tries=5, interval=0.5)
|
|
82
|
+
def poll_status():
|
|
83
|
+
return check_ready()
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Functional API
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
result = backon.retry(
|
|
90
|
+
fetch_data,
|
|
91
|
+
backon.expo,
|
|
92
|
+
exception=ValueError,
|
|
93
|
+
max_tries=3,
|
|
94
|
+
)
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Context manager
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
with backon.Retrying(backon.expo, exception=ValueError, max_tries=3) as r:
|
|
101
|
+
result = r.call(fetch_data)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Async variant:
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
async with backon.Retrying(backon.constant, exception=ValueError, max_tries=3, interval=0.5) as r:
|
|
108
|
+
result = await r.async_call(fetch_data)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## API Reference
|
|
114
|
+
|
|
115
|
+
### Decorators
|
|
116
|
+
|
|
117
|
+
#### `@backon.on_exception(wait_gen, exception, ...)`
|
|
118
|
+
|
|
119
|
+
Retry when the decorated function raises one of the specified exceptions.
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
@backon.on_exception(backon.expo, (ValueError, TimeoutError), max_tries=5)
|
|
123
|
+
def fetch():
|
|
124
|
+
...
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Parameters:
|
|
128
|
+
|
|
129
|
+
| Argument | Type | Default | Description |
|
|
130
|
+
|---|---|---|---|
|
|
131
|
+
| `wait_gen` | `WaitGenerator` | — | Wait strategy (expo, constant, fibo, etc.) |
|
|
132
|
+
| `exception` | `type` or `tuple[type]` | — | Exception class(es) to retry on |
|
|
133
|
+
| `max_tries` | `int` | `None` | Maximum number of attempts |
|
|
134
|
+
| `max_time` | `float` | `None` | Maximum total elapsed time |
|
|
135
|
+
| `jitter` | `Jitterer` or `None` | `full_jitter` | Jitter function |
|
|
136
|
+
| `giveup` | `Callable[[Exception], bool]` | `lambda e: False` | Stop retrying for matching exceptions |
|
|
137
|
+
| `on_success` | `Handler` | `None` | Called after successful attempt |
|
|
138
|
+
| `on_backoff` | `Handler` | `None` | Called before each retry |
|
|
139
|
+
| `on_giveup` | `Handler` | `None` | Called when retries exhausted |
|
|
140
|
+
| `on_attempt` | `Handler` | `None` | Called before each attempt |
|
|
141
|
+
| `before_sleep` | `Handler` | `None` | Called before sleeping |
|
|
142
|
+
| `logger` | `str` or `Logger` | `"backon"` | Logger name or instance |
|
|
143
|
+
| `raise_on_giveup` | `bool` | `True` | Raise final exception when giving up |
|
|
144
|
+
| `sleep` | `Callable[[float], Any]` | `None` | Custom sleep function |
|
|
145
|
+
|
|
146
|
+
#### `@backon.on_predicate(wait_gen, predicate, ...)`
|
|
147
|
+
|
|
148
|
+
Retry while the predicate matches the return value.
|
|
149
|
+
|
|
150
|
+
```python
|
|
151
|
+
@backon.on_predicate(backon.constant, predicate=lambda x: x is None, max_tries=5)
|
|
152
|
+
def poll():
|
|
153
|
+
...
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Functional API
|
|
157
|
+
|
|
158
|
+
#### `backon.retry(target, wait_gen, ...)`
|
|
159
|
+
|
|
160
|
+
```python
|
|
161
|
+
result = backon.retry(
|
|
162
|
+
target=my_function,
|
|
163
|
+
wait_gen=backon.expo,
|
|
164
|
+
exception=ValueError,
|
|
165
|
+
max_tries=3,
|
|
166
|
+
jitter=backon.full_jitter,
|
|
167
|
+
)
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Accepts all the same parameters as the decorators, plus `wait_gen_kwargs` as extra keyword arguments (e.g. `interval=0.5` for `constant`).
|
|
171
|
+
|
|
172
|
+
### Context Manager
|
|
173
|
+
|
|
174
|
+
#### `backon.Retrying(wait_gen, ...)`
|
|
175
|
+
|
|
176
|
+
```python
|
|
177
|
+
with backon.Retrying(backon.expo, exception=ValueError, max_tries=3) as r:
|
|
178
|
+
r.call(my_function)
|
|
179
|
+
|
|
180
|
+
async with backon.Retrying(backon.constant, exception=ValueError, max_tries=3, interval=0.5) as r:
|
|
181
|
+
await r.async_call(my_async_function)
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Methods:
|
|
185
|
+
|
|
186
|
+
| Method | Description |
|
|
187
|
+
|---|---|
|
|
188
|
+
| `call(target, *args, **kwargs)` | Execute synchronously |
|
|
189
|
+
| `async_call(target, *args, **kwargs)` | Execute asynchronously |
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
## Wait Generators
|
|
194
|
+
|
|
195
|
+
| Generator | Signature | Description |
|
|
196
|
+
|---|---|---|
|
|
197
|
+
| `expo` | `(base=2, factor=1, max_value=None)` | Exponential backoff: `factor * base^n` |
|
|
198
|
+
| `constant` | `(interval=1)` | Fixed interval; accepts `float` or `Sequence[float]` |
|
|
199
|
+
| `fibo` | `(max_value=None)` | Fibonacci sequence |
|
|
200
|
+
| `runtime` | `(value=Callable)` | Dynamic wait from return value or exception |
|
|
201
|
+
| `decay` | `(initial_value=1, decay_factor=1, min_value=None)` | Exponential decay |
|
|
202
|
+
| `wait_random_exponential` | `(multiplier=1, max_value=None, exp_base=2, min_value=0)` | Randomized exponential |
|
|
203
|
+
| `wait_incrementing` | `(start=1, increment=1, max_value=None)` | Linear increment |
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
## Jitter
|
|
208
|
+
|
|
209
|
+
```python
|
|
210
|
+
@backon.on_exception(backon.expo, ValueError, jitter=backon.full_jitter)
|
|
211
|
+
def f():
|
|
212
|
+
...
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
| Jitter | Effect |
|
|
216
|
+
|---|---|
|
|
217
|
+
| `backon.full_jitter` | Random value between 0 and the wait time |
|
|
218
|
+
| `backon.random_jitter` | Random value within ±25% of the wait time |
|
|
219
|
+
| `None` | No jitter (deterministic waits) |
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
## Handlers
|
|
224
|
+
|
|
225
|
+
Handlers receive a `details` dict with contextual information:
|
|
226
|
+
|
|
227
|
+
```python
|
|
228
|
+
def handler(details):
|
|
229
|
+
print(f"Attempt {details['tries']}, elapsed {details['elapsed']:.2f}s")
|
|
230
|
+
|
|
231
|
+
@backon.on_exception(
|
|
232
|
+
backon.expo, ValueError, max_tries=3,
|
|
233
|
+
on_attempt=handler,
|
|
234
|
+
on_backoff=handler,
|
|
235
|
+
on_success=handler,
|
|
236
|
+
on_giveup=handler,
|
|
237
|
+
)
|
|
238
|
+
def f():
|
|
239
|
+
...
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
Available keys in `details`:
|
|
243
|
+
|
|
244
|
+
| Key | Available in |
|
|
245
|
+
|---|---|
|
|
246
|
+
| `target` | All |
|
|
247
|
+
| `args`, `kwargs` | All |
|
|
248
|
+
| `tries` | All |
|
|
249
|
+
| `elapsed` | All |
|
|
250
|
+
| `value` | `on_success`, `on_backoff`, `on_giveup` |
|
|
251
|
+
| `exception` | `on_backoff`, `on_giveup` |
|
|
252
|
+
| `wait` | `on_backoff` |
|
|
253
|
+
|
|
254
|
+
---
|
|
255
|
+
|
|
256
|
+
## Global Toggle
|
|
257
|
+
|
|
258
|
+
Useful in tests to disable retry logic:
|
|
259
|
+
|
|
260
|
+
```python
|
|
261
|
+
backon.disable() # skip retry, call function directly
|
|
262
|
+
backon.enable() # re-enable retry
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
---
|
|
266
|
+
|
|
267
|
+
## Async Support
|
|
268
|
+
|
|
269
|
+
All three APIs work with async functions transparently:
|
|
270
|
+
|
|
271
|
+
```python
|
|
272
|
+
@backon.on_exception(backon.expo, ValueError, max_tries=3)
|
|
273
|
+
async def fetch():
|
|
274
|
+
return await api.call()
|
|
275
|
+
|
|
276
|
+
result = await backon.retry(fetch, backon.expo, exception=ValueError, max_tries=3)
|
|
277
|
+
|
|
278
|
+
async with backon.Retrying(backon.expo, exception=ValueError, max_tries=3) as r:
|
|
279
|
+
result = await r.async_call(fetch)
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
---
|
|
283
|
+
|
|
284
|
+
## Custom Sleep
|
|
285
|
+
|
|
286
|
+
Replace the default sleep for testing or special environments:
|
|
287
|
+
|
|
288
|
+
```python
|
|
289
|
+
@backon.on_exception(
|
|
290
|
+
backon.expo, ValueError, max_tries=3,
|
|
291
|
+
sleep=lambda s: print(f"waiting {s}s"),
|
|
292
|
+
)
|
|
293
|
+
def f():
|
|
294
|
+
...
|
|
295
|
+
|
|
296
|
+
# With asyncio.Event for testing
|
|
297
|
+
import asyncio
|
|
298
|
+
|
|
299
|
+
event = asyncio.Event()
|
|
300
|
+
@backon.on_exception(
|
|
301
|
+
backon.expo, ValueError, max_tries=3,
|
|
302
|
+
sleep=backon.sleep_using_event(event),
|
|
303
|
+
)
|
|
304
|
+
async def f():
|
|
305
|
+
...
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
---
|
|
309
|
+
|
|
310
|
+
## Migrating from backoff
|
|
311
|
+
|
|
312
|
+
backon is a near-drop-in replacement. Change your imports:
|
|
313
|
+
|
|
314
|
+
```diff
|
|
315
|
+
- import backoff
|
|
316
|
+
+ import backon
|
|
317
|
+
|
|
318
|
+
- @backoff.on_exception(backoff.expo, ValueError, max_tries=3)
|
|
319
|
+
+ @backon.on_exception(backon.expo, ValueError, max_tries=3)
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
Key differences:
|
|
323
|
+
|
|
324
|
+
| Area | backoff | backon |
|
|
325
|
+
|---|---|---|
|
|
326
|
+
| Python support | 3.7+ | 3.10+ |
|
|
327
|
+
| Type hints | Partial | Full |
|
|
328
|
+
| `on_attempt` callback | Not supported | Supported |
|
|
329
|
+
| Context manager | Not supported | `Retrying` class |
|
|
330
|
+
| Functional API | Not supported | `retry()` function |
|
|
331
|
+
| Global toggle | Not supported | `disable()` / `enable()` |
|
|
332
|
+
| Custom sleep | Not supported | `sleep=` parameter |
|
|
333
|
+
| Build system | Poetry | PDM (PEP 621) |
|
|
334
|
+
|
|
335
|
+
---
|
|
336
|
+
|
|
337
|
+
## Contributing
|
|
338
|
+
|
|
339
|
+
```bash
|
|
340
|
+
git clone https://github.com/Llucs/backon.git
|
|
341
|
+
cd backon
|
|
342
|
+
pip install pdm
|
|
343
|
+
pdm install
|
|
344
|
+
pdm run ruff check backon/ tests/
|
|
345
|
+
pdm run mypy backon/
|
|
346
|
+
pdm run pytest tests/
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
---
|
|
350
|
+
|
|
351
|
+
## License
|
|
352
|
+
|
|
353
|
+
[MIT](https://github.com/Llucs/backon/blob/main/LICENSE)
|
backon-3.2.0/README.md
ADDED
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
# backon
|
|
2
|
+
|
|
3
|
+
> Function decoration for backoff and retry — modern, fast, zero dependencies.
|
|
4
|
+
|
|
5
|
+
[](https://github.com/Llucs/backon/actions/workflows/ci.yml)
|
|
6
|
+
[](https://github.com/Llucs/backon/actions/workflows/codeql.yml)
|
|
7
|
+
[](https://pypi.org/project/backon/)
|
|
8
|
+
[](https://pypi.org/project/backon/)
|
|
9
|
+
[](https://github.com/Llucs/backon/blob/main/LICENSE)
|
|
10
|
+
|
|
11
|
+
backon is a modern evolution of [backoff](https://github.com/litl/backoff) — a zero-dependency Python library for retry with exponential backoff. It provides decorator, functional, and context manager APIs for both sync and async code.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Features
|
|
16
|
+
|
|
17
|
+
- **Zero dependencies** — pure Python, stdlib only
|
|
18
|
+
- **Three APIs** — decorator (`@on_exception`, `@on_predicate`), functional (`retry()`), context manager (`Retrying`)
|
|
19
|
+
- **Async native** — same API works for `async def` functions
|
|
20
|
+
- **Full type hints** — validated with mypy, strict mode compatible
|
|
21
|
+
- **Global toggle** — `backon.disable()` / `backon.enable()` for testing
|
|
22
|
+
- **Custom sleep** — inject your own sleep function (useful for testing with `asyncio.Event`)
|
|
23
|
+
- **Multiple wait strategies** — exponential, constant, Fibonacci, decay, runtime, and composable chains
|
|
24
|
+
- **Jitter** — full jitter, random jitter, or none
|
|
25
|
+
- **Rich callbacks** — `on_attempt`, `on_backoff`, `on_success`, `on_giveup`, `before_sleep`
|
|
26
|
+
- **Modern packaging** — PEP 621, PDM, py.typed
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Installation
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install backon
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Requires Python 3.10+.
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Quick Start
|
|
41
|
+
|
|
42
|
+
### Retry on exception
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
import backon
|
|
46
|
+
|
|
47
|
+
@backon.on_exception(backon.expo, ValueError, max_tries=3)
|
|
48
|
+
def fetch_data():
|
|
49
|
+
return api.call()
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Retry on predicate
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
@backon.on_predicate(backon.constant, max_tries=5, interval=0.5)
|
|
56
|
+
def poll_status():
|
|
57
|
+
return check_ready()
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Functional API
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
result = backon.retry(
|
|
64
|
+
fetch_data,
|
|
65
|
+
backon.expo,
|
|
66
|
+
exception=ValueError,
|
|
67
|
+
max_tries=3,
|
|
68
|
+
)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Context manager
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
with backon.Retrying(backon.expo, exception=ValueError, max_tries=3) as r:
|
|
75
|
+
result = r.call(fetch_data)
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Async variant:
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
async with backon.Retrying(backon.constant, exception=ValueError, max_tries=3, interval=0.5) as r:
|
|
82
|
+
result = await r.async_call(fetch_data)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## API Reference
|
|
88
|
+
|
|
89
|
+
### Decorators
|
|
90
|
+
|
|
91
|
+
#### `@backon.on_exception(wait_gen, exception, ...)`
|
|
92
|
+
|
|
93
|
+
Retry when the decorated function raises one of the specified exceptions.
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
@backon.on_exception(backon.expo, (ValueError, TimeoutError), max_tries=5)
|
|
97
|
+
def fetch():
|
|
98
|
+
...
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Parameters:
|
|
102
|
+
|
|
103
|
+
| Argument | Type | Default | Description |
|
|
104
|
+
|---|---|---|---|
|
|
105
|
+
| `wait_gen` | `WaitGenerator` | — | Wait strategy (expo, constant, fibo, etc.) |
|
|
106
|
+
| `exception` | `type` or `tuple[type]` | — | Exception class(es) to retry on |
|
|
107
|
+
| `max_tries` | `int` | `None` | Maximum number of attempts |
|
|
108
|
+
| `max_time` | `float` | `None` | Maximum total elapsed time |
|
|
109
|
+
| `jitter` | `Jitterer` or `None` | `full_jitter` | Jitter function |
|
|
110
|
+
| `giveup` | `Callable[[Exception], bool]` | `lambda e: False` | Stop retrying for matching exceptions |
|
|
111
|
+
| `on_success` | `Handler` | `None` | Called after successful attempt |
|
|
112
|
+
| `on_backoff` | `Handler` | `None` | Called before each retry |
|
|
113
|
+
| `on_giveup` | `Handler` | `None` | Called when retries exhausted |
|
|
114
|
+
| `on_attempt` | `Handler` | `None` | Called before each attempt |
|
|
115
|
+
| `before_sleep` | `Handler` | `None` | Called before sleeping |
|
|
116
|
+
| `logger` | `str` or `Logger` | `"backon"` | Logger name or instance |
|
|
117
|
+
| `raise_on_giveup` | `bool` | `True` | Raise final exception when giving up |
|
|
118
|
+
| `sleep` | `Callable[[float], Any]` | `None` | Custom sleep function |
|
|
119
|
+
|
|
120
|
+
#### `@backon.on_predicate(wait_gen, predicate, ...)`
|
|
121
|
+
|
|
122
|
+
Retry while the predicate matches the return value.
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
@backon.on_predicate(backon.constant, predicate=lambda x: x is None, max_tries=5)
|
|
126
|
+
def poll():
|
|
127
|
+
...
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Functional API
|
|
131
|
+
|
|
132
|
+
#### `backon.retry(target, wait_gen, ...)`
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
result = backon.retry(
|
|
136
|
+
target=my_function,
|
|
137
|
+
wait_gen=backon.expo,
|
|
138
|
+
exception=ValueError,
|
|
139
|
+
max_tries=3,
|
|
140
|
+
jitter=backon.full_jitter,
|
|
141
|
+
)
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Accepts all the same parameters as the decorators, plus `wait_gen_kwargs` as extra keyword arguments (e.g. `interval=0.5` for `constant`).
|
|
145
|
+
|
|
146
|
+
### Context Manager
|
|
147
|
+
|
|
148
|
+
#### `backon.Retrying(wait_gen, ...)`
|
|
149
|
+
|
|
150
|
+
```python
|
|
151
|
+
with backon.Retrying(backon.expo, exception=ValueError, max_tries=3) as r:
|
|
152
|
+
r.call(my_function)
|
|
153
|
+
|
|
154
|
+
async with backon.Retrying(backon.constant, exception=ValueError, max_tries=3, interval=0.5) as r:
|
|
155
|
+
await r.async_call(my_async_function)
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Methods:
|
|
159
|
+
|
|
160
|
+
| Method | Description |
|
|
161
|
+
|---|---|
|
|
162
|
+
| `call(target, *args, **kwargs)` | Execute synchronously |
|
|
163
|
+
| `async_call(target, *args, **kwargs)` | Execute asynchronously |
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
## Wait Generators
|
|
168
|
+
|
|
169
|
+
| Generator | Signature | Description |
|
|
170
|
+
|---|---|---|
|
|
171
|
+
| `expo` | `(base=2, factor=1, max_value=None)` | Exponential backoff: `factor * base^n` |
|
|
172
|
+
| `constant` | `(interval=1)` | Fixed interval; accepts `float` or `Sequence[float]` |
|
|
173
|
+
| `fibo` | `(max_value=None)` | Fibonacci sequence |
|
|
174
|
+
| `runtime` | `(value=Callable)` | Dynamic wait from return value or exception |
|
|
175
|
+
| `decay` | `(initial_value=1, decay_factor=1, min_value=None)` | Exponential decay |
|
|
176
|
+
| `wait_random_exponential` | `(multiplier=1, max_value=None, exp_base=2, min_value=0)` | Randomized exponential |
|
|
177
|
+
| `wait_incrementing` | `(start=1, increment=1, max_value=None)` | Linear increment |
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
## Jitter
|
|
182
|
+
|
|
183
|
+
```python
|
|
184
|
+
@backon.on_exception(backon.expo, ValueError, jitter=backon.full_jitter)
|
|
185
|
+
def f():
|
|
186
|
+
...
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
| Jitter | Effect |
|
|
190
|
+
|---|---|
|
|
191
|
+
| `backon.full_jitter` | Random value between 0 and the wait time |
|
|
192
|
+
| `backon.random_jitter` | Random value within ±25% of the wait time |
|
|
193
|
+
| `None` | No jitter (deterministic waits) |
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
## Handlers
|
|
198
|
+
|
|
199
|
+
Handlers receive a `details` dict with contextual information:
|
|
200
|
+
|
|
201
|
+
```python
|
|
202
|
+
def handler(details):
|
|
203
|
+
print(f"Attempt {details['tries']}, elapsed {details['elapsed']:.2f}s")
|
|
204
|
+
|
|
205
|
+
@backon.on_exception(
|
|
206
|
+
backon.expo, ValueError, max_tries=3,
|
|
207
|
+
on_attempt=handler,
|
|
208
|
+
on_backoff=handler,
|
|
209
|
+
on_success=handler,
|
|
210
|
+
on_giveup=handler,
|
|
211
|
+
)
|
|
212
|
+
def f():
|
|
213
|
+
...
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Available keys in `details`:
|
|
217
|
+
|
|
218
|
+
| Key | Available in |
|
|
219
|
+
|---|---|
|
|
220
|
+
| `target` | All |
|
|
221
|
+
| `args`, `kwargs` | All |
|
|
222
|
+
| `tries` | All |
|
|
223
|
+
| `elapsed` | All |
|
|
224
|
+
| `value` | `on_success`, `on_backoff`, `on_giveup` |
|
|
225
|
+
| `exception` | `on_backoff`, `on_giveup` |
|
|
226
|
+
| `wait` | `on_backoff` |
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
## Global Toggle
|
|
231
|
+
|
|
232
|
+
Useful in tests to disable retry logic:
|
|
233
|
+
|
|
234
|
+
```python
|
|
235
|
+
backon.disable() # skip retry, call function directly
|
|
236
|
+
backon.enable() # re-enable retry
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
---
|
|
240
|
+
|
|
241
|
+
## Async Support
|
|
242
|
+
|
|
243
|
+
All three APIs work with async functions transparently:
|
|
244
|
+
|
|
245
|
+
```python
|
|
246
|
+
@backon.on_exception(backon.expo, ValueError, max_tries=3)
|
|
247
|
+
async def fetch():
|
|
248
|
+
return await api.call()
|
|
249
|
+
|
|
250
|
+
result = await backon.retry(fetch, backon.expo, exception=ValueError, max_tries=3)
|
|
251
|
+
|
|
252
|
+
async with backon.Retrying(backon.expo, exception=ValueError, max_tries=3) as r:
|
|
253
|
+
result = await r.async_call(fetch)
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
---
|
|
257
|
+
|
|
258
|
+
## Custom Sleep
|
|
259
|
+
|
|
260
|
+
Replace the default sleep for testing or special environments:
|
|
261
|
+
|
|
262
|
+
```python
|
|
263
|
+
@backon.on_exception(
|
|
264
|
+
backon.expo, ValueError, max_tries=3,
|
|
265
|
+
sleep=lambda s: print(f"waiting {s}s"),
|
|
266
|
+
)
|
|
267
|
+
def f():
|
|
268
|
+
...
|
|
269
|
+
|
|
270
|
+
# With asyncio.Event for testing
|
|
271
|
+
import asyncio
|
|
272
|
+
|
|
273
|
+
event = asyncio.Event()
|
|
274
|
+
@backon.on_exception(
|
|
275
|
+
backon.expo, ValueError, max_tries=3,
|
|
276
|
+
sleep=backon.sleep_using_event(event),
|
|
277
|
+
)
|
|
278
|
+
async def f():
|
|
279
|
+
...
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
---
|
|
283
|
+
|
|
284
|
+
## Migrating from backoff
|
|
285
|
+
|
|
286
|
+
backon is a near-drop-in replacement. Change your imports:
|
|
287
|
+
|
|
288
|
+
```diff
|
|
289
|
+
- import backoff
|
|
290
|
+
+ import backon
|
|
291
|
+
|
|
292
|
+
- @backoff.on_exception(backoff.expo, ValueError, max_tries=3)
|
|
293
|
+
+ @backon.on_exception(backon.expo, ValueError, max_tries=3)
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
Key differences:
|
|
297
|
+
|
|
298
|
+
| Area | backoff | backon |
|
|
299
|
+
|---|---|---|
|
|
300
|
+
| Python support | 3.7+ | 3.10+ |
|
|
301
|
+
| Type hints | Partial | Full |
|
|
302
|
+
| `on_attempt` callback | Not supported | Supported |
|
|
303
|
+
| Context manager | Not supported | `Retrying` class |
|
|
304
|
+
| Functional API | Not supported | `retry()` function |
|
|
305
|
+
| Global toggle | Not supported | `disable()` / `enable()` |
|
|
306
|
+
| Custom sleep | Not supported | `sleep=` parameter |
|
|
307
|
+
| Build system | Poetry | PDM (PEP 621) |
|
|
308
|
+
|
|
309
|
+
---
|
|
310
|
+
|
|
311
|
+
## Contributing
|
|
312
|
+
|
|
313
|
+
```bash
|
|
314
|
+
git clone https://github.com/Llucs/backon.git
|
|
315
|
+
cd backon
|
|
316
|
+
pip install pdm
|
|
317
|
+
pdm install
|
|
318
|
+
pdm run ruff check backon/ tests/
|
|
319
|
+
pdm run mypy backon/
|
|
320
|
+
pdm run pytest tests/
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
---
|
|
324
|
+
|
|
325
|
+
## License
|
|
326
|
+
|
|
327
|
+
[MIT](https://github.com/Llucs/backon/blob/main/LICENSE)
|