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.
Files changed (35) hide show
  1. backon-3.2.0/PKG-INFO +353 -0
  2. backon-3.2.0/README.md +327 -0
  3. backon-3.2.0/backon/__init__.py +87 -0
  4. {backon-3.0.0 → backon-3.2.0}/backon/_async.py +8 -8
  5. backon-3.2.0/backon/_conditions.py +239 -0
  6. backon-3.2.0/backon/_decorator.py +271 -0
  7. backon-3.2.0/backon/_retry.py +844 -0
  8. backon-3.2.0/backon/_state.py +75 -0
  9. {backon-3.0.0 → backon-3.2.0}/backon/_sync.py +1 -1
  10. {backon-3.0.0 → backon-3.2.0}/backon/_typing.py +14 -21
  11. backon-3.2.0/backon/_wait_gen.py +212 -0
  12. {backon-3.0.0 → backon-3.2.0}/pyproject.toml +10 -5
  13. backon-3.2.0/tests/test_advanced_features.py +852 -0
  14. {backon-3.0.0 → backon-3.2.0}/tests/test_backon.py +28 -1
  15. {backon-3.0.0 → backon-3.2.0}/tests/test_retry.py +1 -1
  16. backon-3.0.0/PKG-INFO +0 -179
  17. backon-3.0.0/README.md +0 -153
  18. backon-3.0.0/backon/__init__.py +0 -23
  19. backon-3.0.0/backon/_decorator.py +0 -150
  20. backon-3.0.0/backon/_retry.py +0 -464
  21. backon-3.0.0/backon/_wait_gen.py +0 -62
  22. {backon-3.0.0 → backon-3.2.0}/LICENSE +0 -0
  23. {backon-3.0.0 → backon-3.2.0}/backon/_common.py +0 -0
  24. {backon-3.0.0 → backon-3.2.0}/backon/_jitter.py +0 -0
  25. {backon-3.0.0 → backon-3.2.0}/backon/py.typed +0 -0
  26. {backon-3.0.0 → backon-3.2.0}/backon/types.py +0 -0
  27. {backon-3.0.0 → backon-3.2.0}/tests/__init__.py +0 -0
  28. {backon-3.0.0 → backon-3.2.0}/tests/test_backon_async.py +0 -0
  29. {backon-3.0.0 → backon-3.2.0}/tests/test_backon_predicate.py +0 -0
  30. {backon-3.0.0 → backon-3.2.0}/tests/test_backon_sync.py +0 -0
  31. {backon-3.0.0 → backon-3.2.0}/tests/test_features.py +0 -0
  32. {backon-3.0.0 → backon-3.2.0}/tests/test_jitter.py +0 -0
  33. {backon-3.0.0 → backon-3.2.0}/tests/test_types.py +0 -0
  34. {backon-3.0.0 → backon-3.2.0}/tests/test_typing.py +0 -0
  35. {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
+ [![CI](https://github.com/Llucs/backon/actions/workflows/ci.yml/badge.svg)](https://github.com/Llucs/backon/actions/workflows/ci.yml)
32
+ [![CodeQL](https://github.com/Llucs/backon/actions/workflows/codeql.yml/badge.svg)](https://github.com/Llucs/backon/actions/workflows/codeql.yml)
33
+ [![PyPI](https://img.shields.io/pypi/v/backon.svg)](https://pypi.org/project/backon/)
34
+ [![Python](https://img.shields.io/pypi/pyversions/backon.svg)](https://pypi.org/project/backon/)
35
+ [![License](https://img.shields.io/pypi/l/backon.svg)](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
+ [![CI](https://github.com/Llucs/backon/actions/workflows/ci.yml/badge.svg)](https://github.com/Llucs/backon/actions/workflows/ci.yml)
6
+ [![CodeQL](https://github.com/Llucs/backon/actions/workflows/codeql.yml/badge.svg)](https://github.com/Llucs/backon/actions/workflows/codeql.yml)
7
+ [![PyPI](https://img.shields.io/pypi/v/backon.svg)](https://pypi.org/project/backon/)
8
+ [![Python](https://img.shields.io/pypi/pyversions/backon.svg)](https://pypi.org/project/backon/)
9
+ [![License](https://img.shields.io/pypi/l/backon.svg)](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)