python-backoff 2.3.0__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 +15 -13
- backoff/_async.py +228 -115
- backoff/_common.py +202 -31
- backoff/_decorator.py +220 -44
- backoff/_jitter.py +0 -2
- backoff/_sync.py +200 -92
- backoff/_typing.py +48 -25
- backoff/_wait_gen.py +36 -42
- backoff/types.py +1 -2
- {python_backoff-2.3.0.dist-info → python_backoff-2.4.0.dist-info}/METADATA +108 -59
- python_backoff-2.4.0.dist-info/RECORD +14 -0
- {python_backoff-2.3.0.dist-info → python_backoff-2.4.0.dist-info}/WHEEL +1 -1
- python_backoff-2.3.0.dist-info/RECORD +0 -14
- {python_backoff-2.3.0.dist-info → python_backoff-2.4.0.dist-info}/licenses/LICENSE +0 -0
backoff/__init__.py
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
# coding:utf-8
|
|
2
1
|
"""
|
|
3
2
|
Function decoration for backoff and retry
|
|
4
3
|
|
|
@@ -12,20 +11,23 @@ polling resources for externally generated content.
|
|
|
12
11
|
For examples and full documentation see the README at
|
|
13
12
|
https://github.com/python-backoff/backoff
|
|
14
13
|
"""
|
|
15
|
-
|
|
14
|
+
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
20
|
+
"aretry_context",
|
|
21
|
+
"constant",
|
|
22
|
+
"decay",
|
|
23
|
+
"expo",
|
|
24
|
+
"fibo",
|
|
25
|
+
"full_jitter",
|
|
26
|
+
"on_exception",
|
|
27
|
+
"on_predicate",
|
|
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,47 +1,101 @@
|
|
|
1
|
-
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
2
3
|
import asyncio
|
|
3
4
|
import functools
|
|
4
5
|
import inspect
|
|
5
|
-
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")
|
|
6
40
|
|
|
7
|
-
|
|
41
|
+
_AsyncHandler = Callable[["Details"], "Coroutine[Any, Any, None]"]
|
|
8
42
|
|
|
9
43
|
|
|
10
|
-
def _ensure_coroutine(
|
|
44
|
+
def _ensure_coroutine(
|
|
45
|
+
coro_or_func: Callable[..., Any],
|
|
46
|
+
) -> Callable[..., Coroutine[Any, Any, Any]]:
|
|
11
47
|
if inspect.iscoroutinefunction(coro_or_func):
|
|
12
48
|
return coro_or_func
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
49
|
+
|
|
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)
|
|
53
|
+
|
|
54
|
+
return f
|
|
18
55
|
|
|
19
56
|
|
|
20
|
-
def _ensure_coroutines(
|
|
57
|
+
def _ensure_coroutines(
|
|
58
|
+
coros_or_funcs: Iterable[Callable[..., Any]],
|
|
59
|
+
) -> list[Callable[..., Coroutine[Any, Any, Any]]]:
|
|
21
60
|
return [_ensure_coroutine(f) for f in coros_or_funcs]
|
|
22
61
|
|
|
23
62
|
|
|
24
|
-
async def _call_handlers(
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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 = {
|
|
74
|
+
"target": target,
|
|
75
|
+
"args": args,
|
|
76
|
+
"kwargs": kwargs,
|
|
77
|
+
"tries": tries,
|
|
78
|
+
"elapsed": elapsed,
|
|
34
79
|
}
|
|
80
|
+
# pyrefly: ignore [no-matching-overload]
|
|
35
81
|
details.update(extra)
|
|
36
82
|
for handler in handlers:
|
|
37
83
|
await handler(details)
|
|
38
84
|
|
|
39
85
|
|
|
40
|
-
def retry_predicate(
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
86
|
+
def retry_predicate(
|
|
87
|
+
target: Callable[P, T],
|
|
88
|
+
wait_gen: _WaitGenerator,
|
|
89
|
+
predicate: _Predicate[T],
|
|
90
|
+
*,
|
|
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]:
|
|
45
99
|
on_success = _ensure_coroutines(on_success)
|
|
46
100
|
on_backoff = _ensure_coroutines(on_backoff)
|
|
47
101
|
on_giveup = _ensure_coroutines(on_giveup)
|
|
@@ -53,45 +107,36 @@ def retry_predicate(target, wait_gen, predicate,
|
|
|
53
107
|
assert inspect.iscoroutinefunction(target)
|
|
54
108
|
|
|
55
109
|
@functools.wraps(target)
|
|
56
|
-
async def retry(*args, **kwargs):
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
start = time.monotonic()
|
|
64
|
-
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
|
+
)
|
|
65
117
|
while True:
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
details = {
|
|
118
|
+
state.start_attempt()
|
|
119
|
+
ret = await target(*args, **kwargs)
|
|
120
|
+
details: _BaseDetails = {
|
|
69
121
|
"target": target,
|
|
70
122
|
"args": args,
|
|
71
123
|
"kwargs": kwargs,
|
|
72
|
-
"tries": tries,
|
|
73
|
-
"elapsed":
|
|
124
|
+
"tries": state.tries,
|
|
125
|
+
"elapsed": state.record_elapsed(),
|
|
74
126
|
}
|
|
75
127
|
|
|
76
|
-
ret = await target(*args, **kwargs)
|
|
77
128
|
if predicate(ret):
|
|
78
|
-
|
|
79
|
-
max_time_exceeded = (max_time_value is not None and
|
|
80
|
-
elapsed >= max_time_value)
|
|
81
|
-
|
|
82
|
-
if max_tries_exceeded or max_time_exceeded:
|
|
129
|
+
if state.exhausted():
|
|
83
130
|
await _call_handlers(on_giveup, **details, value=ret)
|
|
84
131
|
break
|
|
85
132
|
|
|
86
133
|
try:
|
|
87
|
-
seconds =
|
|
88
|
-
max_time_value)
|
|
134
|
+
seconds = state.next_wait(ret, jitter)
|
|
89
135
|
except StopIteration:
|
|
90
136
|
await _call_handlers(on_giveup, **details, value=ret)
|
|
91
137
|
break
|
|
92
138
|
|
|
93
|
-
await _call_handlers(on_backoff, **details, value=ret,
|
|
94
|
-
wait=seconds)
|
|
139
|
+
await _call_handlers(on_backoff, **details, value=ret, wait=seconds)
|
|
95
140
|
|
|
96
141
|
# Note: there is no convenient way to pass explicit event
|
|
97
142
|
# loop to decorator, so here we assume that either default
|
|
@@ -104,20 +149,48 @@ def retry_predicate(target, wait_gen, predicate,
|
|
|
104
149
|
# <https://bugs.python.org/issue28613>
|
|
105
150
|
await asyncio.sleep(seconds)
|
|
106
151
|
continue
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
break
|
|
152
|
+
await _call_handlers(on_success, **details, value=ret)
|
|
153
|
+
break
|
|
110
154
|
|
|
111
155
|
return ret
|
|
112
156
|
|
|
113
|
-
return retry
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
def
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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]
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def retry_exception(
|
|
180
|
+
target: Callable[P, T],
|
|
181
|
+
wait_gen: _WaitGenerator,
|
|
182
|
+
exception: _MaybeTuple[type[Exception]],
|
|
183
|
+
*,
|
|
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]:
|
|
121
194
|
on_success = _ensure_coroutines(on_success)
|
|
122
195
|
on_backoff = _ensure_coroutines(on_backoff)
|
|
123
196
|
on_giveup = _ensure_coroutines(on_giveup)
|
|
@@ -128,61 +201,101 @@ def retry_exception(target, wait_gen, exception,
|
|
|
128
201
|
assert not inspect.iscoroutinefunction(jitter)
|
|
129
202
|
|
|
130
203
|
@functools.wraps(target)
|
|
131
|
-
async def retry(
|
|
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]
|
|
132
225
|
|
|
133
|
-
|
|
134
|
-
max_time_value = _maybe_call(max_time)
|
|
135
|
-
|
|
136
|
-
tries = 0
|
|
137
|
-
start = time.monotonic()
|
|
138
|
-
wait = _init_wait_gen(wait_gen, wait_gen_kwargs)
|
|
139
|
-
while True:
|
|
140
|
-
tries += 1
|
|
141
|
-
elapsed = time.monotonic() - start
|
|
142
|
-
details = {
|
|
143
|
-
"target": target,
|
|
144
|
-
"args": args,
|
|
145
|
-
"kwargs": kwargs,
|
|
146
|
-
"tries": tries,
|
|
147
|
-
"elapsed": elapsed,
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
try:
|
|
151
|
-
ret = await target(*args, **kwargs)
|
|
152
|
-
except exception as e:
|
|
153
|
-
giveup_result = await giveup(e)
|
|
154
|
-
max_tries_exceeded = (tries == max_tries_value)
|
|
155
|
-
max_time_exceeded = (max_time_value is not None and
|
|
156
|
-
elapsed >= max_time_value)
|
|
157
|
-
|
|
158
|
-
if giveup_result or max_tries_exceeded or max_time_exceeded:
|
|
159
|
-
await _call_handlers(on_giveup, **details, exception=e)
|
|
160
|
-
if raise_on_giveup:
|
|
161
|
-
raise
|
|
162
|
-
return None
|
|
163
|
-
|
|
164
|
-
try:
|
|
165
|
-
seconds = _next_wait(wait, e, jitter, elapsed,
|
|
166
|
-
max_time_value)
|
|
167
|
-
except StopIteration:
|
|
168
|
-
await _call_handlers(on_giveup, **details, exception=e)
|
|
169
|
-
raise e
|
|
170
|
-
|
|
171
|
-
await _call_handlers(on_backoff, **details, wait=seconds,
|
|
172
|
-
exception=e)
|
|
226
|
+
return ret
|
|
173
227
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
228
|
+
return retry # type: ignore[return-value] # ty:ignore[invalid-return-type]
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
async def _dispatch_handlers(
|
|
232
|
+
handlers: Iterable[_ContextHandler], **details: Any
|
|
233
|
+
) -> None:
|
|
234
|
+
for hdlr in _ensure_coroutines(handlers):
|
|
235
|
+
await hdlr(details)
|
|
236
|
+
|
|
237
|
+
|
|
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)
|
|
186
253
|
|
|
187
|
-
|
|
188
|
-
|
|
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)
|