python-backoff 2.3.1__py3-none-any.whl → 3.0.0__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.
- backoff/__init__.py +11 -7
- backoff/_async.py +219 -110
- backoff/_common.py +183 -36
- backoff/_decorator.py +279 -64
- backoff/_sync.py +193 -97
- backoff/_typing.py +43 -28
- backoff/_wait_gen.py +58 -39
- backoff/types.py +2 -1
- {python_backoff-2.3.1.dist-info → python_backoff-3.0.0.dist-info}/METADATA +4 -7
- python_backoff-3.0.0.dist-info/RECORD +14 -0
- {python_backoff-2.3.1.dist-info → python_backoff-3.0.0.dist-info}/WHEEL +1 -1
- python_backoff-2.3.1.dist-info/RECORD +0 -14
- {python_backoff-2.3.1.dist-info → python_backoff-3.0.0.dist-info}/licenses/LICENSE +0 -0
backoff/__init__.py
CHANGED
|
@@ -12,20 +12,24 @@ For examples and full documentation see the README at
|
|
|
12
12
|
https://github.com/python-backoff/backoff
|
|
13
13
|
"""
|
|
14
14
|
|
|
15
|
-
from backoff.
|
|
15
|
+
from backoff._common import Attempt
|
|
16
|
+
from backoff._decorator import aretry_context, on_exception, on_predicate, retry_context
|
|
16
17
|
from backoff._jitter import full_jitter, random_jitter
|
|
17
|
-
from backoff._wait_gen import constant, expo, fibo, runtime
|
|
18
|
+
from backoff._wait_gen import constant, decay, expo, fibo, runtime
|
|
18
19
|
|
|
19
20
|
__all__ = [
|
|
20
|
-
"
|
|
21
|
-
"
|
|
21
|
+
"Attempt",
|
|
22
|
+
"aretry_context",
|
|
22
23
|
"constant",
|
|
23
|
-
"expo",
|
|
24
24
|
"decay",
|
|
25
|
+
"expo",
|
|
25
26
|
"fibo",
|
|
26
|
-
"runtime",
|
|
27
27
|
"full_jitter",
|
|
28
|
+
"on_exception",
|
|
29
|
+
"on_predicate",
|
|
28
30
|
"random_jitter",
|
|
31
|
+
"retry_context",
|
|
32
|
+
"runtime",
|
|
29
33
|
]
|
|
30
34
|
|
|
31
|
-
__version__ = "
|
|
35
|
+
__version__ = "3.0.0"
|
backoff/_async.py
CHANGED
|
@@ -1,53 +1,99 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import asyncio
|
|
2
4
|
import functools
|
|
3
5
|
import inspect
|
|
4
|
-
import
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from typing import TYPE_CHECKING, Any, TypeVar
|
|
8
|
+
|
|
9
|
+
from backoff._common import Attempt, _RetryState
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
import sys
|
|
13
|
+
from collections.abc import AsyncGenerator, Coroutine, Iterable
|
|
14
|
+
from typing import ParamSpec
|
|
15
|
+
|
|
16
|
+
from backoff._typing import (
|
|
17
|
+
ContextDetails,
|
|
18
|
+
Details,
|
|
19
|
+
_BaseDetails,
|
|
20
|
+
_CallDetails,
|
|
21
|
+
_ContextHandler,
|
|
22
|
+
_Handler,
|
|
23
|
+
_Jitterer,
|
|
24
|
+
_MaybeCallable,
|
|
25
|
+
_MaybeTuple,
|
|
26
|
+
_Predicate,
|
|
27
|
+
_WaitGenerator,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
if sys.version_info >= (3, 11):
|
|
31
|
+
from typing import Unpack
|
|
32
|
+
else:
|
|
33
|
+
from typing_extensions import Unpack
|
|
34
|
+
|
|
35
|
+
T = TypeVar("T")
|
|
36
|
+
P = ParamSpec("P")
|
|
5
37
|
|
|
6
|
-
|
|
38
|
+
_AsyncHandler = Callable[["Details"], "Coroutine[Any, Any, None]"]
|
|
7
39
|
|
|
8
40
|
|
|
9
|
-
def _ensure_coroutine(
|
|
41
|
+
def _ensure_coroutine(
|
|
42
|
+
coro_or_func: Callable[..., Any],
|
|
43
|
+
) -> Callable[..., Coroutine[Any, Any, Any]]:
|
|
10
44
|
if inspect.iscoroutinefunction(coro_or_func):
|
|
11
45
|
return coro_or_func
|
|
12
|
-
else:
|
|
13
46
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
47
|
+
@functools.wraps(coro_or_func)
|
|
48
|
+
async def f(*args: Any, **kwargs: Any) -> Any: # ruff:ignore[unused-async]
|
|
49
|
+
return coro_or_func(*args, **kwargs)
|
|
17
50
|
|
|
18
|
-
|
|
51
|
+
return f
|
|
19
52
|
|
|
20
53
|
|
|
21
|
-
def _ensure_coroutines(
|
|
54
|
+
def _ensure_coroutines(
|
|
55
|
+
coros_or_funcs: Iterable[Callable[..., Any]],
|
|
56
|
+
) -> list[Callable[..., Coroutine[Any, Any, Any]]]:
|
|
22
57
|
return [_ensure_coroutine(f) for f in coros_or_funcs]
|
|
23
58
|
|
|
24
59
|
|
|
25
|
-
async def _call_handlers(
|
|
26
|
-
|
|
60
|
+
async def _call_handlers(
|
|
61
|
+
handlers: Iterable[_AsyncHandler],
|
|
62
|
+
*,
|
|
63
|
+
target: Callable[..., Any],
|
|
64
|
+
args: tuple[Any, ...],
|
|
65
|
+
kwargs: dict[str, Any],
|
|
66
|
+
tries: int,
|
|
67
|
+
elapsed: float,
|
|
68
|
+
**extra: Unpack[_CallDetails],
|
|
69
|
+
) -> None:
|
|
70
|
+
details: Details = {
|
|
27
71
|
"target": target,
|
|
28
72
|
"args": args,
|
|
29
73
|
"kwargs": kwargs,
|
|
30
74
|
"tries": tries,
|
|
31
75
|
"elapsed": elapsed,
|
|
76
|
+
**extra,
|
|
32
77
|
}
|
|
33
|
-
details.update(extra)
|
|
34
78
|
for handler in handlers:
|
|
35
79
|
await handler(details)
|
|
36
80
|
|
|
37
81
|
|
|
38
82
|
def retry_predicate(
|
|
39
|
-
target,
|
|
40
|
-
wait_gen,
|
|
41
|
-
predicate,
|
|
83
|
+
target: Callable[P, Coroutine[object, object, T]],
|
|
84
|
+
wait_gen: _WaitGenerator,
|
|
85
|
+
predicate: _Predicate[T],
|
|
42
86
|
*,
|
|
43
|
-
max_tries,
|
|
44
|
-
max_time,
|
|
45
|
-
jitter,
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
87
|
+
max_tries: _MaybeCallable[int] | None,
|
|
88
|
+
max_time: _MaybeCallable[float] | None,
|
|
89
|
+
jitter: _Jitterer | None,
|
|
90
|
+
on_try: Iterable[_Handler],
|
|
91
|
+
on_success: Iterable[_Handler],
|
|
92
|
+
on_backoff: Iterable[_Handler],
|
|
93
|
+
on_giveup: Iterable[_Handler],
|
|
94
|
+
wait_gen_kwargs: dict[str, Any],
|
|
95
|
+
) -> Callable[P, Coroutine[object, object, T]]:
|
|
96
|
+
on_try = _ensure_coroutines(on_try)
|
|
51
97
|
on_success = _ensure_coroutines(on_success)
|
|
52
98
|
on_backoff = _ensure_coroutines(on_backoff)
|
|
53
99
|
on_giveup = _ensure_coroutines(on_giveup)
|
|
@@ -59,38 +105,33 @@ def retry_predicate(
|
|
|
59
105
|
assert inspect.iscoroutinefunction(target)
|
|
60
106
|
|
|
61
107
|
@functools.wraps(target)
|
|
62
|
-
async def retry(*args, **kwargs):
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
wait = _init_wait_gen(wait_gen, wait_gen_kwargs)
|
|
108
|
+
async def retry(*args: P.args, **kwargs: P.kwargs) -> T:
|
|
109
|
+
state = _RetryState(
|
|
110
|
+
wait_gen,
|
|
111
|
+
wait_gen_kwargs,
|
|
112
|
+
max_tries=max_tries,
|
|
113
|
+
max_time=max_time,
|
|
114
|
+
)
|
|
70
115
|
while True:
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
details = {
|
|
116
|
+
state.start_attempt()
|
|
117
|
+
details: _BaseDetails = {
|
|
74
118
|
"target": target,
|
|
75
119
|
"args": args,
|
|
76
120
|
"kwargs": kwargs,
|
|
77
|
-
"tries": tries,
|
|
78
|
-
"elapsed": elapsed,
|
|
121
|
+
"tries": state.tries,
|
|
122
|
+
"elapsed": state.elapsed,
|
|
79
123
|
}
|
|
80
|
-
|
|
124
|
+
await _call_handlers(on_try, **details)
|
|
81
125
|
ret = await target(*args, **kwargs)
|
|
82
|
-
|
|
83
|
-
max_tries_exceeded = tries == max_tries_value
|
|
84
|
-
max_time_exceeded = (
|
|
85
|
-
max_time_value is not None and elapsed >= max_time_value
|
|
86
|
-
)
|
|
126
|
+
details["elapsed"] = state.record_elapsed()
|
|
87
127
|
|
|
88
|
-
|
|
128
|
+
if predicate(ret):
|
|
129
|
+
if state.exhausted():
|
|
89
130
|
await _call_handlers(on_giveup, **details, value=ret)
|
|
90
131
|
break
|
|
91
132
|
|
|
92
133
|
try:
|
|
93
|
-
seconds =
|
|
134
|
+
seconds = state.next_wait(ret, jitter)
|
|
94
135
|
except StopIteration:
|
|
95
136
|
await _call_handlers(on_giveup, **details, value=ret)
|
|
96
137
|
break
|
|
@@ -108,30 +149,50 @@ def retry_predicate(
|
|
|
108
149
|
# <https://bugs.python.org/issue28613>
|
|
109
150
|
await asyncio.sleep(seconds)
|
|
110
151
|
continue
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
break
|
|
152
|
+
await _call_handlers(on_success, **details, value=ret)
|
|
153
|
+
break
|
|
114
154
|
|
|
115
155
|
return ret
|
|
116
156
|
|
|
117
157
|
return retry
|
|
118
158
|
|
|
119
159
|
|
|
160
|
+
def _adapt_context_handlers(
|
|
161
|
+
handlers: Iterable[_AsyncHandler],
|
|
162
|
+
target: Callable[..., Any],
|
|
163
|
+
args: tuple[Any, ...],
|
|
164
|
+
kwargs: dict[str, Any],
|
|
165
|
+
) -> list[_ContextHandler]:
|
|
166
|
+
async def adapted(details: ContextDetails) -> None:
|
|
167
|
+
full_details: Details = {
|
|
168
|
+
"target": target,
|
|
169
|
+
"args": args,
|
|
170
|
+
"kwargs": kwargs,
|
|
171
|
+
**details,
|
|
172
|
+
}
|
|
173
|
+
for hdlr in handlers:
|
|
174
|
+
await hdlr(full_details)
|
|
175
|
+
|
|
176
|
+
return [adapted]
|
|
177
|
+
|
|
178
|
+
|
|
120
179
|
def retry_exception(
|
|
121
|
-
target,
|
|
122
|
-
wait_gen,
|
|
123
|
-
exception,
|
|
180
|
+
target: Callable[P, Coroutine[object, object, T]],
|
|
181
|
+
wait_gen: _WaitGenerator,
|
|
182
|
+
exception: _MaybeTuple[type[Exception]],
|
|
124
183
|
*,
|
|
125
|
-
max_tries,
|
|
126
|
-
max_time,
|
|
127
|
-
jitter,
|
|
128
|
-
giveup,
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
184
|
+
max_tries: _MaybeCallable[int] | None,
|
|
185
|
+
max_time: _MaybeCallable[float] | None,
|
|
186
|
+
jitter: _Jitterer | None,
|
|
187
|
+
giveup: _Predicate[Exception],
|
|
188
|
+
on_try: Iterable[_Handler],
|
|
189
|
+
on_success: Iterable[_Handler],
|
|
190
|
+
on_backoff: Iterable[_Handler],
|
|
191
|
+
on_giveup: Iterable[_Handler],
|
|
192
|
+
raise_on_giveup: bool,
|
|
193
|
+
wait_gen_kwargs: dict[str, Any],
|
|
194
|
+
) -> Callable[P, Coroutine[object, object, T]]:
|
|
195
|
+
on_try = _ensure_coroutines(on_try)
|
|
135
196
|
on_success = _ensure_coroutines(on_success)
|
|
136
197
|
on_backoff = _ensure_coroutines(on_backoff)
|
|
137
198
|
on_giveup = _ensure_coroutines(on_giveup)
|
|
@@ -142,60 +203,108 @@ def retry_exception(
|
|
|
142
203
|
assert not inspect.iscoroutinefunction(jitter)
|
|
143
204
|
|
|
144
205
|
@functools.wraps(target)
|
|
145
|
-
async def retry(
|
|
146
|
-
|
|
147
|
-
|
|
206
|
+
async def retry(
|
|
207
|
+
*args: P.args,
|
|
208
|
+
**kwargs: P.kwargs,
|
|
209
|
+
) -> T:
|
|
210
|
+
ret: T = None # type: ignore[assignment] # ty:ignore[invalid-assignment]
|
|
211
|
+
|
|
212
|
+
async for attempt in aretry_context(
|
|
213
|
+
exception,
|
|
214
|
+
wait_gen,
|
|
215
|
+
max_tries=max_tries,
|
|
216
|
+
max_time=max_time,
|
|
217
|
+
jitter=jitter,
|
|
218
|
+
giveup=giveup,
|
|
219
|
+
on_try=_adapt_context_handlers(on_try, target, args, kwargs),
|
|
220
|
+
on_success=_adapt_context_handlers(on_success, target, args, kwargs),
|
|
221
|
+
on_backoff=_adapt_context_handlers(on_backoff, target, args, kwargs),
|
|
222
|
+
on_giveup=_adapt_context_handlers(on_giveup, target, args, kwargs),
|
|
223
|
+
raise_on_giveup=raise_on_giveup,
|
|
224
|
+
wait_gen_kwargs=wait_gen_kwargs,
|
|
225
|
+
):
|
|
226
|
+
with attempt:
|
|
227
|
+
ret = await target(*args, **kwargs)
|
|
148
228
|
|
|
149
|
-
|
|
150
|
-
start = time.monotonic()
|
|
151
|
-
wait = _init_wait_gen(wait_gen, wait_gen_kwargs)
|
|
152
|
-
while True:
|
|
153
|
-
tries += 1
|
|
154
|
-
elapsed = time.monotonic() - start
|
|
155
|
-
details = {
|
|
156
|
-
"target": target,
|
|
157
|
-
"args": args,
|
|
158
|
-
"kwargs": kwargs,
|
|
159
|
-
"tries": tries,
|
|
160
|
-
"elapsed": elapsed,
|
|
161
|
-
}
|
|
229
|
+
return ret
|
|
162
230
|
|
|
163
|
-
|
|
164
|
-
ret = await target(*args, **kwargs)
|
|
165
|
-
except exception as e:
|
|
166
|
-
giveup_result = await giveup(e)
|
|
167
|
-
max_tries_exceeded = tries == max_tries_value
|
|
168
|
-
max_time_exceeded = (
|
|
169
|
-
max_time_value is not None and elapsed >= max_time_value
|
|
170
|
-
)
|
|
171
|
-
|
|
172
|
-
if giveup_result or max_tries_exceeded or max_time_exceeded:
|
|
173
|
-
await _call_handlers(on_giveup, **details, exception=e)
|
|
174
|
-
if raise_on_giveup:
|
|
175
|
-
raise
|
|
176
|
-
return None
|
|
231
|
+
return retry
|
|
177
232
|
|
|
178
|
-
try:
|
|
179
|
-
seconds = _next_wait(wait, e, jitter, elapsed, max_time_value)
|
|
180
|
-
except StopIteration:
|
|
181
|
-
await _call_handlers(on_giveup, **details, exception=e)
|
|
182
|
-
raise e
|
|
183
233
|
|
|
184
|
-
|
|
234
|
+
async def _dispatch_handlers(
|
|
235
|
+
handlers: Iterable[_ContextHandler], **details: Any
|
|
236
|
+
) -> None:
|
|
237
|
+
for hdlr in _ensure_coroutines(handlers):
|
|
238
|
+
await hdlr(details)
|
|
185
239
|
|
|
186
|
-
# Note: there is no convenient way to pass explicit event
|
|
187
|
-
# loop to decorator, so here we assume that either default
|
|
188
|
-
# thread event loop is set and correct (it mostly is
|
|
189
|
-
# by default), or Python >= 3.5.3 or Python >= 3.6 is used
|
|
190
|
-
# where loop.get_event_loop() in coroutine guaranteed to
|
|
191
|
-
# return correct value.
|
|
192
|
-
# See for details:
|
|
193
|
-
# <https://groups.google.com/forum/#!topic/python-tulip/yF9C-rFpiKk>
|
|
194
|
-
# <https://bugs.python.org/issue28613>
|
|
195
|
-
await asyncio.sleep(seconds)
|
|
196
|
-
else:
|
|
197
|
-
await _call_handlers(on_success, **details)
|
|
198
240
|
|
|
199
|
-
|
|
241
|
+
async def aretry_context(
|
|
242
|
+
exception: _MaybeTuple[type[Exception]],
|
|
243
|
+
wait_gen: _WaitGenerator,
|
|
244
|
+
*,
|
|
245
|
+
max_tries: _MaybeCallable[int] | None,
|
|
246
|
+
max_time: _MaybeCallable[float] | None,
|
|
247
|
+
jitter: _Jitterer | None,
|
|
248
|
+
giveup: _Predicate[BaseException],
|
|
249
|
+
on_try: Iterable[_ContextHandler],
|
|
250
|
+
on_success: Iterable[_ContextHandler],
|
|
251
|
+
on_backoff: Iterable[_ContextHandler],
|
|
252
|
+
on_giveup: Iterable[_ContextHandler],
|
|
253
|
+
raise_on_giveup: bool,
|
|
254
|
+
wait_gen_kwargs: dict[str, Any],
|
|
255
|
+
) -> AsyncGenerator[Attempt, None]:
|
|
256
|
+
giveup = _ensure_coroutine(giveup)
|
|
200
257
|
|
|
201
|
-
|
|
258
|
+
state = _RetryState(
|
|
259
|
+
wait_gen,
|
|
260
|
+
wait_gen_kwargs,
|
|
261
|
+
max_tries=max_tries,
|
|
262
|
+
max_time=max_time,
|
|
263
|
+
)
|
|
264
|
+
while True:
|
|
265
|
+
state.start_attempt()
|
|
266
|
+
attempt = Attempt(exception)
|
|
267
|
+
await _dispatch_handlers(
|
|
268
|
+
handlers=on_try,
|
|
269
|
+
tries=state.tries,
|
|
270
|
+
elapsed=state.elapsed,
|
|
271
|
+
)
|
|
272
|
+
yield attempt
|
|
273
|
+
elapsed = state.record_elapsed()
|
|
274
|
+
|
|
275
|
+
exc = attempt.exception
|
|
276
|
+
if exc is None:
|
|
277
|
+
await _dispatch_handlers(on_success, tries=state.tries, elapsed=elapsed)
|
|
278
|
+
return
|
|
279
|
+
|
|
280
|
+
if await giveup(exc) or state.exhausted():
|
|
281
|
+
await _dispatch_handlers(
|
|
282
|
+
on_giveup,
|
|
283
|
+
tries=state.tries,
|
|
284
|
+
elapsed=elapsed,
|
|
285
|
+
exception=exc,
|
|
286
|
+
)
|
|
287
|
+
if raise_on_giveup:
|
|
288
|
+
raise exc
|
|
289
|
+
return
|
|
290
|
+
|
|
291
|
+
try:
|
|
292
|
+
seconds = state.next_wait(exc, jitter)
|
|
293
|
+
except StopIteration:
|
|
294
|
+
await _dispatch_handlers(
|
|
295
|
+
on_giveup,
|
|
296
|
+
tries=state.tries,
|
|
297
|
+
elapsed=elapsed,
|
|
298
|
+
exception=exc,
|
|
299
|
+
)
|
|
300
|
+
raise exc from None
|
|
301
|
+
|
|
302
|
+
await _dispatch_handlers(
|
|
303
|
+
on_backoff,
|
|
304
|
+
tries=state.tries,
|
|
305
|
+
elapsed=elapsed,
|
|
306
|
+
wait=seconds,
|
|
307
|
+
exception=exc,
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
await asyncio.sleep(seconds)
|