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