python-backoff 2.4.0__py3-none-any.whl → 3.1.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 +8 -5
- backoff/_async.py +29 -20
- backoff/_common.py +24 -49
- backoff/_decorator.py +117 -63
- backoff/_sync.py +16 -14
- backoff/_typing.py +13 -23
- backoff/_wait_gen.py +82 -8
- backoff/types.py +2 -1
- {python_backoff-2.4.0.dist-info → python_backoff-3.1.0.dist-info}/METADATA +12 -7
- python_backoff-3.1.0.dist-info/RECORD +14 -0
- {python_backoff-2.4.0.dist-info → python_backoff-3.1.0.dist-info}/WHEEL +1 -1
- python_backoff-2.4.0.dist-info/RECORD +0 -14
- {python_backoff-2.4.0.dist-info → python_backoff-3.1.0.dist-info}/licenses/LICENSE +0 -0
backoff/__init__.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"""
|
|
2
|
-
Function decoration for backoff and retry
|
|
2
|
+
Function decoration for backoff and retry.
|
|
3
3
|
|
|
4
4
|
This module provides function decorators which can be used to wrap a
|
|
5
5
|
function such that it will be retried until some condition is met. It
|
|
@@ -8,16 +8,19 @@ potential for intermittent failures i.e. network resources and external
|
|
|
8
8
|
APIs. Somewhat more generally, it may also be of use for dynamically
|
|
9
9
|
polling resources for externally generated content.
|
|
10
10
|
|
|
11
|
-
For examples and full documentation see
|
|
12
|
-
https://
|
|
11
|
+
For examples and full documentation, see
|
|
12
|
+
[the official documentation](https://backoff.readthedocs.io/en/latest/).
|
|
13
13
|
"""
|
|
14
14
|
|
|
15
|
+
from backoff._common import Attempt
|
|
15
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, decay, expo, fibo, runtime
|
|
18
|
+
from backoff._wait_gen import capped, constant, decay, expo, fibo, runtime
|
|
18
19
|
|
|
19
20
|
__all__ = [
|
|
21
|
+
"Attempt",
|
|
20
22
|
"aretry_context",
|
|
23
|
+
"capped",
|
|
21
24
|
"constant",
|
|
22
25
|
"decay",
|
|
23
26
|
"expo",
|
|
@@ -30,4 +33,4 @@ __all__ = [
|
|
|
30
33
|
"runtime",
|
|
31
34
|
]
|
|
32
35
|
|
|
33
|
-
__version__ = "
|
|
36
|
+
__version__ = "3.1.0"
|
backoff/_async.py
CHANGED
|
@@ -3,13 +3,15 @@ from __future__ import annotations
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import functools
|
|
5
5
|
import inspect
|
|
6
|
-
from
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from typing import TYPE_CHECKING, Any, TypeVar
|
|
7
8
|
|
|
8
|
-
from backoff._common import
|
|
9
|
+
from backoff._common import Attempt, _RetryState
|
|
9
10
|
|
|
10
11
|
if TYPE_CHECKING:
|
|
11
12
|
import sys
|
|
12
13
|
from collections.abc import AsyncGenerator, Coroutine, Iterable
|
|
14
|
+
from typing import ParamSpec
|
|
13
15
|
|
|
14
16
|
from backoff._typing import (
|
|
15
17
|
ContextDetails,
|
|
@@ -25,11 +27,6 @@ if TYPE_CHECKING:
|
|
|
25
27
|
_WaitGenerator,
|
|
26
28
|
)
|
|
27
29
|
|
|
28
|
-
if sys.version_info >= (3, 10):
|
|
29
|
-
from typing import ParamSpec
|
|
30
|
-
else:
|
|
31
|
-
from typing_extensions import ParamSpec
|
|
32
|
-
|
|
33
30
|
if sys.version_info >= (3, 11):
|
|
34
31
|
from typing import Unpack
|
|
35
32
|
else:
|
|
@@ -76,26 +73,27 @@ async def _call_handlers(
|
|
|
76
73
|
"kwargs": kwargs,
|
|
77
74
|
"tries": tries,
|
|
78
75
|
"elapsed": elapsed,
|
|
76
|
+
**extra,
|
|
79
77
|
}
|
|
80
|
-
# pyrefly: ignore [no-matching-overload]
|
|
81
|
-
details.update(extra)
|
|
82
78
|
for handler in handlers:
|
|
83
79
|
await handler(details)
|
|
84
80
|
|
|
85
81
|
|
|
86
82
|
def retry_predicate(
|
|
87
|
-
target: Callable[P, T],
|
|
83
|
+
target: Callable[P, Coroutine[object, object, T]],
|
|
88
84
|
wait_gen: _WaitGenerator,
|
|
89
85
|
predicate: _Predicate[T],
|
|
90
86
|
*,
|
|
91
87
|
max_tries: _MaybeCallable[int] | None,
|
|
92
88
|
max_time: _MaybeCallable[float] | None,
|
|
93
89
|
jitter: _Jitterer | None,
|
|
90
|
+
on_try: Iterable[_Handler],
|
|
94
91
|
on_success: Iterable[_Handler],
|
|
95
92
|
on_backoff: Iterable[_Handler],
|
|
96
93
|
on_giveup: Iterable[_Handler],
|
|
97
94
|
wait_gen_kwargs: dict[str, Any],
|
|
98
|
-
) -> Callable[P, T]:
|
|
95
|
+
) -> Callable[P, Coroutine[object, object, T]]:
|
|
96
|
+
on_try = _ensure_coroutines(on_try)
|
|
99
97
|
on_success = _ensure_coroutines(on_success)
|
|
100
98
|
on_backoff = _ensure_coroutines(on_backoff)
|
|
101
99
|
on_giveup = _ensure_coroutines(on_giveup)
|
|
@@ -116,14 +114,16 @@ def retry_predicate(
|
|
|
116
114
|
)
|
|
117
115
|
while True:
|
|
118
116
|
state.start_attempt()
|
|
119
|
-
ret = await target(*args, **kwargs)
|
|
120
117
|
details: _BaseDetails = {
|
|
121
118
|
"target": target,
|
|
122
119
|
"args": args,
|
|
123
120
|
"kwargs": kwargs,
|
|
124
121
|
"tries": state.tries,
|
|
125
|
-
"elapsed": state.
|
|
122
|
+
"elapsed": state.elapsed,
|
|
126
123
|
}
|
|
124
|
+
await _call_handlers(on_try, **details)
|
|
125
|
+
ret = await target(*args, **kwargs)
|
|
126
|
+
details["elapsed"] = state.record_elapsed()
|
|
127
127
|
|
|
128
128
|
if predicate(ret):
|
|
129
129
|
if state.exhausted():
|
|
@@ -154,7 +154,7 @@ def retry_predicate(
|
|
|
154
154
|
|
|
155
155
|
return ret
|
|
156
156
|
|
|
157
|
-
return retry
|
|
157
|
+
return retry
|
|
158
158
|
|
|
159
159
|
|
|
160
160
|
def _adapt_context_handlers(
|
|
@@ -177,7 +177,7 @@ def _adapt_context_handlers(
|
|
|
177
177
|
|
|
178
178
|
|
|
179
179
|
def retry_exception(
|
|
180
|
-
target: Callable[P, T],
|
|
180
|
+
target: Callable[P, Coroutine[object, object, T]],
|
|
181
181
|
wait_gen: _WaitGenerator,
|
|
182
182
|
exception: _MaybeTuple[type[Exception]],
|
|
183
183
|
*,
|
|
@@ -185,12 +185,14 @@ def retry_exception(
|
|
|
185
185
|
max_time: _MaybeCallable[float] | None,
|
|
186
186
|
jitter: _Jitterer | None,
|
|
187
187
|
giveup: _Predicate[Exception],
|
|
188
|
+
on_try: Iterable[_Handler],
|
|
188
189
|
on_success: Iterable[_Handler],
|
|
189
190
|
on_backoff: Iterable[_Handler],
|
|
190
191
|
on_giveup: Iterable[_Handler],
|
|
191
192
|
raise_on_giveup: bool,
|
|
192
193
|
wait_gen_kwargs: dict[str, Any],
|
|
193
|
-
) -> Callable[P, T]:
|
|
194
|
+
) -> Callable[P, Coroutine[object, object, T]]:
|
|
195
|
+
on_try = _ensure_coroutines(on_try)
|
|
194
196
|
on_success = _ensure_coroutines(on_success)
|
|
195
197
|
on_backoff = _ensure_coroutines(on_backoff)
|
|
196
198
|
on_giveup = _ensure_coroutines(on_giveup)
|
|
@@ -214,6 +216,7 @@ def retry_exception(
|
|
|
214
216
|
max_time=max_time,
|
|
215
217
|
jitter=jitter,
|
|
216
218
|
giveup=giveup,
|
|
219
|
+
on_try=_adapt_context_handlers(on_try, target, args, kwargs),
|
|
217
220
|
on_success=_adapt_context_handlers(on_success, target, args, kwargs),
|
|
218
221
|
on_backoff=_adapt_context_handlers(on_backoff, target, args, kwargs),
|
|
219
222
|
on_giveup=_adapt_context_handlers(on_giveup, target, args, kwargs),
|
|
@@ -221,11 +224,11 @@ def retry_exception(
|
|
|
221
224
|
wait_gen_kwargs=wait_gen_kwargs,
|
|
222
225
|
):
|
|
223
226
|
with attempt:
|
|
224
|
-
ret = await target(*args, **kwargs)
|
|
227
|
+
ret = await target(*args, **kwargs)
|
|
225
228
|
|
|
226
229
|
return ret
|
|
227
230
|
|
|
228
|
-
return retry
|
|
231
|
+
return retry
|
|
229
232
|
|
|
230
233
|
|
|
231
234
|
async def _dispatch_handlers(
|
|
@@ -243,12 +246,13 @@ async def aretry_context(
|
|
|
243
246
|
max_time: _MaybeCallable[float] | None,
|
|
244
247
|
jitter: _Jitterer | None,
|
|
245
248
|
giveup: _Predicate[BaseException],
|
|
249
|
+
on_try: Iterable[_ContextHandler],
|
|
246
250
|
on_success: Iterable[_ContextHandler],
|
|
247
251
|
on_backoff: Iterable[_ContextHandler],
|
|
248
252
|
on_giveup: Iterable[_ContextHandler],
|
|
249
253
|
raise_on_giveup: bool,
|
|
250
254
|
wait_gen_kwargs: dict[str, Any],
|
|
251
|
-
) -> AsyncGenerator[
|
|
255
|
+
) -> AsyncGenerator[Attempt, None]:
|
|
252
256
|
giveup = _ensure_coroutine(giveup)
|
|
253
257
|
|
|
254
258
|
state = _RetryState(
|
|
@@ -259,7 +263,12 @@ async def aretry_context(
|
|
|
259
263
|
)
|
|
260
264
|
while True:
|
|
261
265
|
state.start_attempt()
|
|
262
|
-
attempt =
|
|
266
|
+
attempt = Attempt(exception)
|
|
267
|
+
await _dispatch_handlers(
|
|
268
|
+
handlers=on_try,
|
|
269
|
+
tries=state.tries,
|
|
270
|
+
elapsed=state.elapsed,
|
|
271
|
+
)
|
|
263
272
|
yield attempt
|
|
264
273
|
elapsed = state.record_elapsed()
|
|
265
274
|
|
backoff/_common.py
CHANGED
|
@@ -4,16 +4,18 @@ import functools
|
|
|
4
4
|
import logging
|
|
5
5
|
import time
|
|
6
6
|
import traceback
|
|
7
|
-
import
|
|
8
|
-
from typing import TYPE_CHECKING, Any,
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from typing import TYPE_CHECKING, Any, TypeAlias, TypeVar
|
|
9
9
|
|
|
10
10
|
if TYPE_CHECKING:
|
|
11
11
|
import sys
|
|
12
|
-
from collections.abc import Generator, Iterable
|
|
12
|
+
from collections.abc import Callable, Generator, Iterable
|
|
13
13
|
|
|
14
14
|
from backoff._typing import (
|
|
15
15
|
ContextDetails,
|
|
16
16
|
Details,
|
|
17
|
+
_AnyLogger,
|
|
18
|
+
_AnyLoggerOrName,
|
|
17
19
|
_ContextHandler,
|
|
18
20
|
_Jitterer,
|
|
19
21
|
_MaybeCallable,
|
|
@@ -25,6 +27,8 @@ if TYPE_CHECKING:
|
|
|
25
27
|
else:
|
|
26
28
|
from typing_extensions import Self
|
|
27
29
|
|
|
30
|
+
ExceptionTypes: TypeAlias = type[Exception] | tuple[type[Exception], ...]
|
|
31
|
+
|
|
28
32
|
|
|
29
33
|
# Use module-specific logger with a default null handler.
|
|
30
34
|
_logger = logging.getLogger("backoff")
|
|
@@ -39,7 +43,7 @@ _HandlerT = TypeVar("_HandlerT")
|
|
|
39
43
|
def _maybe_call(f: _MaybeCallable[T] | None, *args: Any, **kwargs: Any) -> T | None:
|
|
40
44
|
if callable(f):
|
|
41
45
|
try:
|
|
42
|
-
return f(*args, **kwargs) # ty:ignore[
|
|
46
|
+
return f(*args, **kwargs) # ty:ignore[invalid-return-type]
|
|
43
47
|
except TypeError:
|
|
44
48
|
return f # type: ignore[return-value] # ty:ignore[invalid-return-type]
|
|
45
49
|
else:
|
|
@@ -64,18 +68,7 @@ def _next_wait(
|
|
|
64
68
|
max_time: float | None,
|
|
65
69
|
) -> float:
|
|
66
70
|
value = wait.send(send_value)
|
|
67
|
-
|
|
68
|
-
seconds = jitter(value) if jitter is not None else value
|
|
69
|
-
except TypeError:
|
|
70
|
-
warnings.warn(
|
|
71
|
-
"Nullary jitter function signature is deprecated. Use "
|
|
72
|
-
"unary signature accepting a wait value in seconds and "
|
|
73
|
-
"returning a jittered version of it.",
|
|
74
|
-
DeprecationWarning,
|
|
75
|
-
stacklevel=2,
|
|
76
|
-
)
|
|
77
|
-
|
|
78
|
-
seconds = value + jitter() # type: ignore[call-arg, misc] # ty:ignore[missing-argument, call-non-callable]
|
|
71
|
+
seconds = jitter(value) if jitter is not None else value
|
|
79
72
|
|
|
80
73
|
# don't sleep longer than remaining allotted max_time
|
|
81
74
|
if max_time is not None:
|
|
@@ -132,7 +125,8 @@ class _RetryState:
|
|
|
132
125
|
return _next_wait(self.wait, send_value, jitter, self.elapsed, self.max_time)
|
|
133
126
|
|
|
134
127
|
|
|
135
|
-
|
|
128
|
+
@dataclass(slots=True)
|
|
129
|
+
class Attempt:
|
|
136
130
|
"""A single attempt yielded by `retry_context`/`aretry_context`.
|
|
137
131
|
|
|
138
132
|
Used as `with attempt: ...`. Exceptions matching `exception_types` are
|
|
@@ -141,16 +135,10 @@ class _Attempt:
|
|
|
141
135
|
exception at all, is left for the `with` block's normal exit behavior.
|
|
142
136
|
"""
|
|
143
137
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
"exception_types",
|
|
147
|
-
)
|
|
138
|
+
exception_types: ExceptionTypes
|
|
139
|
+
exception: BaseException | None = field(init=False)
|
|
148
140
|
|
|
149
|
-
def
|
|
150
|
-
self,
|
|
151
|
-
exception_types: type[Exception] | tuple[type[Exception], ...],
|
|
152
|
-
) -> None:
|
|
153
|
-
self.exception_types = exception_types
|
|
141
|
+
def __post_init__(self) -> None:
|
|
154
142
|
self.exception: BaseException | None = None
|
|
155
143
|
|
|
156
144
|
def __enter__(self) -> Self:
|
|
@@ -163,7 +151,6 @@ class _Attempt:
|
|
|
163
151
|
tb: object,
|
|
164
152
|
) -> bool:
|
|
165
153
|
if exc_type is None:
|
|
166
|
-
# self.outcome = _Success()
|
|
167
154
|
return False
|
|
168
155
|
|
|
169
156
|
if not issubclass(exc_type, self.exception_types):
|
|
@@ -178,9 +165,7 @@ def _dispatch_handlers(handlers: Iterable[_ContextHandler], **details: Any) -> N
|
|
|
178
165
|
hdlr(details) # type: ignore[arg-type] # ty:ignore[invalid-argument-type]
|
|
179
166
|
|
|
180
167
|
|
|
181
|
-
def _prepare_logger(
|
|
182
|
-
logger: str | logging.Logger | logging.LoggerAdapter | None,
|
|
183
|
-
) -> logging.Logger | logging.LoggerAdapter | None:
|
|
168
|
+
def _prepare_logger(logger: _AnyLoggerOrName | None) -> _AnyLogger | None:
|
|
184
169
|
if isinstance(logger, str):
|
|
185
170
|
logger = logging.getLogger(logger)
|
|
186
171
|
return logger
|
|
@@ -192,7 +177,7 @@ def _config_handlers(
|
|
|
192
177
|
user_handlers: _HandlerT | Iterable[_HandlerT] | None,
|
|
193
178
|
*,
|
|
194
179
|
default_handler: Callable[..., None] | None = None,
|
|
195
|
-
logger:
|
|
180
|
+
logger: _AnyLogger | None = None,
|
|
196
181
|
log_level: int | None = None,
|
|
197
182
|
) -> list[_HandlerT]:
|
|
198
183
|
handlers: list[_HandlerT] = []
|
|
@@ -225,16 +210,11 @@ def _config_handlers(
|
|
|
225
210
|
|
|
226
211
|
|
|
227
212
|
# Default backoff handler
|
|
228
|
-
def _log_backoff(
|
|
229
|
-
details: Details,
|
|
230
|
-
logger: logging.Logger | logging.LoggerAdapter,
|
|
231
|
-
log_level: int,
|
|
232
|
-
) -> None:
|
|
213
|
+
def _log_backoff(details: Details, logger: _AnyLogger, log_level: int) -> None:
|
|
233
214
|
msg = "Backing off %s(...) for %.1fs (%s)"
|
|
234
|
-
log_args = [details["target"].__name__, details["wait"]] # ty:ignore[
|
|
215
|
+
log_args = [details["target"].__name__, details["wait"]] # ty:ignore[missing-attribute]
|
|
235
216
|
|
|
236
|
-
exc
|
|
237
|
-
if exc is not None:
|
|
217
|
+
if (exc := details.get("exception")) is not None:
|
|
238
218
|
exc_fmt = traceback.format_exception_only(type(exc), exc)[-1]
|
|
239
219
|
log_args.append(exc_fmt.rstrip("\n"))
|
|
240
220
|
else:
|
|
@@ -243,16 +223,11 @@ def _log_backoff(
|
|
|
243
223
|
|
|
244
224
|
|
|
245
225
|
# Default giveup handler
|
|
246
|
-
def _log_giveup(
|
|
247
|
-
details: Details,
|
|
248
|
-
logger: logging.Logger | logging.LoggerAdapter,
|
|
249
|
-
log_level: int,
|
|
250
|
-
) -> None:
|
|
226
|
+
def _log_giveup(details: Details, logger: _AnyLogger, log_level: int) -> None:
|
|
251
227
|
msg = "Giving up %s(...) after %d tries (%s)"
|
|
252
|
-
log_args = [details["target"].__name__, details["tries"]] # ty:ignore[
|
|
228
|
+
log_args = [details["target"].__name__, details["tries"]] # ty:ignore[missing-attribute]
|
|
253
229
|
|
|
254
|
-
exc
|
|
255
|
-
if exc is not None:
|
|
230
|
+
if (exc := details.get("exception")) is not None:
|
|
256
231
|
exc_fmt = traceback.format_exception_only(type(exc), exc)[-1]
|
|
257
232
|
log_args.append(exc_fmt.rstrip("\n"))
|
|
258
233
|
else:
|
|
@@ -266,7 +241,7 @@ def _log_giveup(
|
|
|
266
241
|
# directly since it's no longer the active exception by this point).
|
|
267
242
|
def _log_backoff_context(
|
|
268
243
|
details: ContextDetails,
|
|
269
|
-
logger:
|
|
244
|
+
logger: _AnyLogger,
|
|
270
245
|
log_level: int,
|
|
271
246
|
) -> None:
|
|
272
247
|
logger.log(
|
|
@@ -280,7 +255,7 @@ def _log_backoff_context(
|
|
|
280
255
|
# Default giveup handler for retry_context/aretry_context.
|
|
281
256
|
def _log_giveup_context(
|
|
282
257
|
details: ContextDetails,
|
|
283
|
-
logger: logging.Logger | logging.LoggerAdapter,
|
|
258
|
+
logger: logging.Logger | logging.LoggerAdapter[Any],
|
|
284
259
|
log_level: int,
|
|
285
260
|
) -> None:
|
|
286
261
|
logger.log(
|
backoff/_decorator.py
CHANGED
|
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
import inspect
|
|
4
4
|
import logging
|
|
5
5
|
import operator
|
|
6
|
-
from typing import TYPE_CHECKING, Any,
|
|
6
|
+
from typing import TYPE_CHECKING, Any, cast
|
|
7
7
|
|
|
8
8
|
from backoff import _async, _sync
|
|
9
9
|
from backoff._common import (
|
|
@@ -18,29 +18,21 @@ from backoff._jitter import full_jitter
|
|
|
18
18
|
from backoff._wait_gen import expo
|
|
19
19
|
|
|
20
20
|
if TYPE_CHECKING:
|
|
21
|
-
import
|
|
22
|
-
from collections.abc import AsyncGenerator, Generator, Iterable
|
|
21
|
+
from collections.abc import AsyncGenerator, Callable, Generator, Iterable
|
|
23
22
|
|
|
24
|
-
from backoff._common import
|
|
23
|
+
from backoff._common import Attempt
|
|
25
24
|
from backoff._typing import (
|
|
25
|
+
_AnyLoggerOrName,
|
|
26
|
+
_CallableT,
|
|
26
27
|
_ContextHandler,
|
|
27
28
|
_Handler,
|
|
28
29
|
_Jitterer,
|
|
29
30
|
_MaybeCallable,
|
|
30
|
-
_MaybeLogger,
|
|
31
31
|
_MaybeTuple,
|
|
32
32
|
_Predicate,
|
|
33
33
|
_WaitGenerator,
|
|
34
34
|
)
|
|
35
35
|
|
|
36
|
-
if sys.version_info >= (3, 10):
|
|
37
|
-
from typing import ParamSpec
|
|
38
|
-
else:
|
|
39
|
-
from typing_extensions import ParamSpec
|
|
40
|
-
|
|
41
|
-
T = TypeVar("T")
|
|
42
|
-
P = ParamSpec("P")
|
|
43
|
-
|
|
44
36
|
|
|
45
37
|
def on_predicate(
|
|
46
38
|
wait_gen: _WaitGenerator,
|
|
@@ -49,14 +41,15 @@ def on_predicate(
|
|
|
49
41
|
max_tries: _MaybeCallable[int] | None = None,
|
|
50
42
|
max_time: _MaybeCallable[float] | None = None,
|
|
51
43
|
jitter: _Jitterer | None = full_jitter,
|
|
44
|
+
on_try: _Handler | Iterable[_Handler] | None = None,
|
|
52
45
|
on_success: _Handler | Iterable[_Handler] | None = None,
|
|
53
46
|
on_backoff: _Handler | Iterable[_Handler] | None = None,
|
|
54
47
|
on_giveup: _Handler | Iterable[_Handler] | None = None,
|
|
55
|
-
logger:
|
|
48
|
+
logger: _AnyLoggerOrName | None = "backoff",
|
|
56
49
|
backoff_log_level: int = logging.INFO,
|
|
57
50
|
giveup_log_level: int = logging.ERROR,
|
|
58
51
|
**wait_gen_kwargs: Any,
|
|
59
|
-
) -> Callable[[
|
|
52
|
+
) -> Callable[[_CallableT], _CallableT]:
|
|
60
53
|
"""Returns decorator for backoff and retry triggered by predicate.
|
|
61
54
|
|
|
62
55
|
Args:
|
|
@@ -81,6 +74,9 @@ def on_predicate(
|
|
|
81
74
|
concurrent clients. Wait times are jittered by default
|
|
82
75
|
using the full_jitter function. Jittering may be disabled
|
|
83
76
|
altogether by passing jitter=None.
|
|
77
|
+
on_try: Callable (or iterable of callables) with a unary
|
|
78
|
+
signature to be called before each attempt. The parameter
|
|
79
|
+
is a dict containing details about the invocation.
|
|
84
80
|
on_success: Callable (or iterable of callables) with a unary
|
|
85
81
|
signature to be called in the event of success. The
|
|
86
82
|
parameter is a dict containing details about the invocation.
|
|
@@ -101,10 +97,11 @@ def on_predicate(
|
|
|
101
97
|
This is useful for runtime configuration.
|
|
102
98
|
"""
|
|
103
99
|
|
|
104
|
-
def decorate(target:
|
|
105
|
-
nonlocal logger, on_success, on_backoff, on_giveup
|
|
100
|
+
def decorate(target: _CallableT) -> _CallableT:
|
|
101
|
+
nonlocal logger, on_try, on_success, on_backoff, on_giveup
|
|
106
102
|
|
|
107
103
|
logger = _prepare_logger(logger)
|
|
104
|
+
on_try = _config_handlers(on_try)
|
|
108
105
|
on_success = _config_handlers(on_success)
|
|
109
106
|
on_backoff = _config_handlers(
|
|
110
107
|
on_backoff,
|
|
@@ -120,21 +117,38 @@ def on_predicate(
|
|
|
120
117
|
)
|
|
121
118
|
|
|
122
119
|
if inspect.iscoroutinefunction(target):
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
120
|
+
return cast(
|
|
121
|
+
"_CallableT",
|
|
122
|
+
_async.retry_predicate(
|
|
123
|
+
target,
|
|
124
|
+
wait_gen,
|
|
125
|
+
predicate,
|
|
126
|
+
max_tries=max_tries,
|
|
127
|
+
max_time=max_time,
|
|
128
|
+
jitter=jitter,
|
|
129
|
+
on_try=on_try,
|
|
130
|
+
on_success=on_success,
|
|
131
|
+
on_backoff=on_backoff,
|
|
132
|
+
on_giveup=on_giveup,
|
|
133
|
+
wait_gen_kwargs=wait_gen_kwargs,
|
|
134
|
+
),
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
return cast(
|
|
138
|
+
"_CallableT",
|
|
139
|
+
_sync.retry_predicate(
|
|
140
|
+
target,
|
|
141
|
+
wait_gen,
|
|
142
|
+
predicate,
|
|
143
|
+
max_tries=max_tries,
|
|
144
|
+
max_time=max_time,
|
|
145
|
+
jitter=jitter,
|
|
146
|
+
on_try=on_try,
|
|
147
|
+
on_success=on_success,
|
|
148
|
+
on_backoff=on_backoff,
|
|
149
|
+
on_giveup=on_giveup,
|
|
150
|
+
wait_gen_kwargs=wait_gen_kwargs,
|
|
151
|
+
),
|
|
138
152
|
)
|
|
139
153
|
|
|
140
154
|
# Return a function which decorates a target with a retry loop.
|
|
@@ -149,15 +163,16 @@ def on_exception(
|
|
|
149
163
|
max_time: _MaybeCallable[float] | None = None,
|
|
150
164
|
jitter: _Jitterer | None = full_jitter,
|
|
151
165
|
giveup: _Predicate[Exception] = lambda e: False,
|
|
166
|
+
on_try: _Handler | Iterable[_Handler] | None = None,
|
|
152
167
|
on_success: _Handler | Iterable[_Handler] | None = None,
|
|
153
168
|
on_backoff: _Handler | Iterable[_Handler] | None = None,
|
|
154
169
|
on_giveup: _Handler | Iterable[_Handler] | None = None,
|
|
155
170
|
raise_on_giveup: bool = True,
|
|
156
|
-
logger:
|
|
171
|
+
logger: _AnyLoggerOrName | None = "backoff",
|
|
157
172
|
backoff_log_level: int = logging.INFO,
|
|
158
173
|
giveup_log_level: int = logging.ERROR,
|
|
159
174
|
**wait_gen_kwargs: Any,
|
|
160
|
-
) -> Callable[[
|
|
175
|
+
) -> Callable[[_CallableT], _CallableT]:
|
|
161
176
|
"""Returns decorator for backoff and retry triggered by exception.
|
|
162
177
|
|
|
163
178
|
Args:
|
|
@@ -183,6 +198,9 @@ def on_exception(
|
|
|
183
198
|
giveup: Function accepting an exception instance and
|
|
184
199
|
returning whether or not to give up. Optional. The default
|
|
185
200
|
is to always continue.
|
|
201
|
+
on_try: Callable (or iterable of callables) with a unary
|
|
202
|
+
signature to be called before each attempt. The parameter
|
|
203
|
+
is a dict containing details about the invocation.
|
|
186
204
|
on_success: Callable (or iterable of callables) with a unary
|
|
187
205
|
signature to be called in the event of success. The
|
|
188
206
|
parameter is a dict containing details about the invocation.
|
|
@@ -204,10 +222,11 @@ def on_exception(
|
|
|
204
222
|
This is useful for runtime configuration.
|
|
205
223
|
"""
|
|
206
224
|
|
|
207
|
-
def decorate(target:
|
|
208
|
-
nonlocal logger, on_success, on_backoff, on_giveup
|
|
225
|
+
def decorate(target: _CallableT) -> _CallableT:
|
|
226
|
+
nonlocal logger, on_try, on_success, on_backoff, on_giveup
|
|
209
227
|
|
|
210
228
|
logger = _prepare_logger(logger)
|
|
229
|
+
on_try = _config_handlers(on_try)
|
|
211
230
|
on_success = _config_handlers(on_success)
|
|
212
231
|
on_backoff = _config_handlers(
|
|
213
232
|
on_backoff,
|
|
@@ -223,23 +242,42 @@ def on_exception(
|
|
|
223
242
|
)
|
|
224
243
|
|
|
225
244
|
if inspect.iscoroutinefunction(target):
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
245
|
+
return cast(
|
|
246
|
+
"_CallableT",
|
|
247
|
+
_async.retry_exception(
|
|
248
|
+
target,
|
|
249
|
+
wait_gen,
|
|
250
|
+
exception,
|
|
251
|
+
max_tries=max_tries,
|
|
252
|
+
max_time=max_time,
|
|
253
|
+
jitter=jitter,
|
|
254
|
+
giveup=giveup,
|
|
255
|
+
on_try=on_try,
|
|
256
|
+
on_success=on_success,
|
|
257
|
+
on_backoff=on_backoff,
|
|
258
|
+
on_giveup=on_giveup,
|
|
259
|
+
raise_on_giveup=raise_on_giveup,
|
|
260
|
+
wait_gen_kwargs=wait_gen_kwargs,
|
|
261
|
+
),
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
return cast(
|
|
265
|
+
"_CallableT",
|
|
266
|
+
_sync.retry_exception(
|
|
267
|
+
target,
|
|
268
|
+
wait_gen,
|
|
269
|
+
exception,
|
|
270
|
+
max_tries=max_tries,
|
|
271
|
+
max_time=max_time,
|
|
272
|
+
jitter=jitter,
|
|
273
|
+
giveup=giveup,
|
|
274
|
+
on_try=on_try,
|
|
275
|
+
on_success=on_success,
|
|
276
|
+
on_backoff=on_backoff,
|
|
277
|
+
on_giveup=on_giveup,
|
|
278
|
+
raise_on_giveup=raise_on_giveup,
|
|
279
|
+
wait_gen_kwargs=wait_gen_kwargs,
|
|
280
|
+
),
|
|
243
281
|
)
|
|
244
282
|
|
|
245
283
|
# Return a function which decorates a target with a retry loop.
|
|
@@ -254,23 +292,29 @@ def retry_context(
|
|
|
254
292
|
max_time: _MaybeCallable[float] | None = None,
|
|
255
293
|
jitter: _Jitterer | None = full_jitter,
|
|
256
294
|
giveup: _Predicate[BaseException] = lambda e: False,
|
|
295
|
+
on_try: _ContextHandler | Iterable[_ContextHandler] | None = None,
|
|
257
296
|
on_success: _ContextHandler | Iterable[_ContextHandler] | None = None,
|
|
258
297
|
on_backoff: _ContextHandler | Iterable[_ContextHandler] | None = None,
|
|
259
298
|
on_giveup: _ContextHandler | Iterable[_ContextHandler] | None = None,
|
|
260
299
|
raise_on_giveup: bool = True,
|
|
261
|
-
logger:
|
|
300
|
+
logger: _AnyLoggerOrName | None = "backoff",
|
|
262
301
|
backoff_log_level: int = logging.INFO,
|
|
263
302
|
giveup_log_level: int = logging.ERROR,
|
|
264
303
|
**wait_gen_kwargs: Any,
|
|
265
|
-
) -> Generator[
|
|
304
|
+
) -> Generator[Attempt, None, None]:
|
|
266
305
|
"""Returns a generator of retry attempts, for direct use with a `for` loop.
|
|
267
306
|
|
|
268
307
|
Unlike `on_exception`, this doesn't wrap a whole function; it lets a
|
|
269
308
|
caller retry an arbitrary block of code:
|
|
270
309
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
310
|
+
```python
|
|
311
|
+
for attempt in backoff.retry_context(
|
|
312
|
+
ValueError,
|
|
313
|
+
backoff.expo,
|
|
314
|
+
):
|
|
315
|
+
with attempt:
|
|
316
|
+
do_something()
|
|
317
|
+
```
|
|
274
318
|
|
|
275
319
|
Each `attempt` is a context manager: exceptions matching `exception`
|
|
276
320
|
are caught, and the loop either sleeps and retries or lets the
|
|
@@ -313,6 +357,7 @@ def retry_context(
|
|
|
313
357
|
passed to wait_gen when it is initialized.
|
|
314
358
|
"""
|
|
315
359
|
logger = _prepare_logger(logger)
|
|
360
|
+
on_try = _config_handlers(on_try)
|
|
316
361
|
on_success = _config_handlers(on_success)
|
|
317
362
|
on_backoff = _config_handlers(
|
|
318
363
|
on_backoff,
|
|
@@ -334,6 +379,7 @@ def retry_context(
|
|
|
334
379
|
max_time=max_time,
|
|
335
380
|
jitter=jitter,
|
|
336
381
|
giveup=giveup,
|
|
382
|
+
on_try=on_try,
|
|
337
383
|
on_success=on_success,
|
|
338
384
|
on_backoff=on_backoff,
|
|
339
385
|
on_giveup=on_giveup,
|
|
@@ -350,25 +396,32 @@ def aretry_context(
|
|
|
350
396
|
max_time: _MaybeCallable[float] | None = None,
|
|
351
397
|
jitter: _Jitterer | None = full_jitter,
|
|
352
398
|
giveup: _Predicate[BaseException] = lambda e: False,
|
|
399
|
+
on_try: _ContextHandler | Iterable[_ContextHandler] | None = None,
|
|
353
400
|
on_success: _ContextHandler | Iterable[_ContextHandler] | None = None,
|
|
354
401
|
on_backoff: _ContextHandler | Iterable[_ContextHandler] | None = None,
|
|
355
402
|
on_giveup: _ContextHandler | Iterable[_ContextHandler] | None = None,
|
|
356
403
|
raise_on_giveup: bool = True,
|
|
357
|
-
logger:
|
|
404
|
+
logger: _AnyLoggerOrName | None = "backoff",
|
|
358
405
|
backoff_log_level: int = logging.INFO,
|
|
359
406
|
giveup_log_level: int = logging.ERROR,
|
|
360
407
|
**wait_gen_kwargs: Any,
|
|
361
|
-
) -> AsyncGenerator[
|
|
408
|
+
) -> AsyncGenerator[Attempt, None]:
|
|
362
409
|
"""Async counterpart to `retry_context`, for use with `async for`.
|
|
363
410
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
411
|
+
```python
|
|
412
|
+
async for attempt in backoff.aretry_context(
|
|
413
|
+
ValueError,
|
|
414
|
+
backoff.expo,
|
|
415
|
+
):
|
|
416
|
+
with attempt:
|
|
417
|
+
await do_something()
|
|
418
|
+
```
|
|
367
419
|
|
|
368
420
|
`giveup` and the handlers may be sync or async callables. See
|
|
369
421
|
`retry_context` for the full argument reference.
|
|
370
422
|
"""
|
|
371
423
|
logger = _prepare_logger(logger)
|
|
424
|
+
on_try = _config_handlers(on_try)
|
|
372
425
|
on_success = _config_handlers(on_success)
|
|
373
426
|
on_backoff = _config_handlers(
|
|
374
427
|
on_backoff,
|
|
@@ -390,6 +443,7 @@ def aretry_context(
|
|
|
390
443
|
max_time=max_time,
|
|
391
444
|
jitter=jitter,
|
|
392
445
|
giveup=giveup,
|
|
446
|
+
on_try=on_try,
|
|
393
447
|
on_success=on_success,
|
|
394
448
|
on_backoff=on_backoff,
|
|
395
449
|
on_giveup=on_giveup,
|
backoff/_sync.py
CHANGED
|
@@ -2,13 +2,14 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import functools
|
|
4
4
|
import time
|
|
5
|
-
from typing import TYPE_CHECKING, Any,
|
|
5
|
+
from typing import TYPE_CHECKING, Any, TypeVar
|
|
6
6
|
|
|
7
|
-
from backoff._common import
|
|
7
|
+
from backoff._common import Attempt, _dispatch_handlers, _RetryState
|
|
8
8
|
|
|
9
9
|
if TYPE_CHECKING:
|
|
10
10
|
import sys
|
|
11
|
-
from collections.abc import Generator, Iterable
|
|
11
|
+
from collections.abc import Callable, Generator, Iterable
|
|
12
|
+
from typing import ParamSpec
|
|
12
13
|
|
|
13
14
|
from backoff._typing import (
|
|
14
15
|
ContextDetails,
|
|
@@ -24,11 +25,6 @@ if TYPE_CHECKING:
|
|
|
24
25
|
_WaitGenerator,
|
|
25
26
|
)
|
|
26
27
|
|
|
27
|
-
if sys.version_info >= (3, 10):
|
|
28
|
-
from typing import ParamSpec
|
|
29
|
-
else:
|
|
30
|
-
from typing_extensions import ParamSpec
|
|
31
|
-
|
|
32
28
|
if sys.version_info >= (3, 11):
|
|
33
29
|
from typing import Unpack
|
|
34
30
|
else:
|
|
@@ -53,9 +49,8 @@ def _call_handlers(
|
|
|
53
49
|
"kwargs": kwargs,
|
|
54
50
|
"tries": tries,
|
|
55
51
|
"elapsed": elapsed,
|
|
52
|
+
**extra,
|
|
56
53
|
}
|
|
57
|
-
# pyrefly: ignore [no-matching-overload]
|
|
58
|
-
details.update(extra)
|
|
59
54
|
for hdlr in hdlrs:
|
|
60
55
|
hdlr(details)
|
|
61
56
|
|
|
@@ -68,6 +63,7 @@ def retry_predicate(
|
|
|
68
63
|
max_tries: _MaybeCallable[int] | None,
|
|
69
64
|
max_time: _MaybeCallable[float] | None,
|
|
70
65
|
jitter: _Jitterer | None,
|
|
66
|
+
on_try: Iterable[_Handler],
|
|
71
67
|
on_success: Iterable[_Handler],
|
|
72
68
|
on_backoff: Iterable[_Handler],
|
|
73
69
|
on_giveup: Iterable[_Handler],
|
|
@@ -83,14 +79,16 @@ def retry_predicate(
|
|
|
83
79
|
)
|
|
84
80
|
while True:
|
|
85
81
|
state.start_attempt()
|
|
86
|
-
ret = target(*args, **kwargs)
|
|
87
82
|
details: _BaseDetails = {
|
|
88
83
|
"target": target,
|
|
89
84
|
"args": args,
|
|
90
85
|
"kwargs": kwargs,
|
|
91
86
|
"tries": state.tries,
|
|
92
|
-
"elapsed": state.
|
|
87
|
+
"elapsed": state.elapsed,
|
|
93
88
|
}
|
|
89
|
+
_call_handlers(on_try, **details)
|
|
90
|
+
ret = target(*args, **kwargs)
|
|
91
|
+
details["elapsed"] = state.record_elapsed()
|
|
94
92
|
|
|
95
93
|
if predicate(ret):
|
|
96
94
|
if state.exhausted():
|
|
@@ -143,6 +141,7 @@ def retry_exception(
|
|
|
143
141
|
max_time: _MaybeCallable[float] | None,
|
|
144
142
|
jitter: _Jitterer | None,
|
|
145
143
|
giveup: _Predicate[Exception],
|
|
144
|
+
on_try: Iterable[_Handler],
|
|
146
145
|
on_success: Iterable[_Handler],
|
|
147
146
|
on_backoff: Iterable[_Handler],
|
|
148
147
|
on_giveup: Iterable[_Handler],
|
|
@@ -160,6 +159,7 @@ def retry_exception(
|
|
|
160
159
|
max_time=max_time,
|
|
161
160
|
jitter=jitter,
|
|
162
161
|
giveup=giveup, # type: ignore[arg-type] # ty:ignore[invalid-argument-type]
|
|
162
|
+
on_try=_adapt_context_handlers(on_try, target, args, kwargs),
|
|
163
163
|
on_success=_adapt_context_handlers(on_success, target, args, kwargs),
|
|
164
164
|
on_backoff=_adapt_context_handlers(on_backoff, target, args, kwargs),
|
|
165
165
|
on_giveup=_adapt_context_handlers(on_giveup, target, args, kwargs),
|
|
@@ -182,12 +182,13 @@ def retry_context(
|
|
|
182
182
|
max_time: _MaybeCallable[float] | None,
|
|
183
183
|
jitter: _Jitterer | None,
|
|
184
184
|
giveup: _Predicate[BaseException],
|
|
185
|
+
on_try: Iterable[_ContextHandler],
|
|
185
186
|
on_success: Iterable[_ContextHandler],
|
|
186
187
|
on_backoff: Iterable[_ContextHandler],
|
|
187
188
|
on_giveup: Iterable[_ContextHandler],
|
|
188
189
|
raise_on_giveup: bool,
|
|
189
190
|
wait_gen_kwargs: dict[str, Any],
|
|
190
|
-
) -> Generator[
|
|
191
|
+
) -> Generator[Attempt, None, None]:
|
|
191
192
|
state = _RetryState(
|
|
192
193
|
wait_gen,
|
|
193
194
|
wait_gen_kwargs,
|
|
@@ -196,7 +197,8 @@ def retry_context(
|
|
|
196
197
|
)
|
|
197
198
|
while True:
|
|
198
199
|
state.start_attempt()
|
|
199
|
-
attempt =
|
|
200
|
+
attempt = Attempt(exception)
|
|
201
|
+
_dispatch_handlers(handlers=on_try, tries=state.tries, elapsed=state.elapsed)
|
|
200
202
|
yield attempt
|
|
201
203
|
elapsed = state.record_elapsed()
|
|
202
204
|
|
backoff/_typing.py
CHANGED
|
@@ -1,14 +1,8 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
|
-
from collections.abc import Coroutine, Generator
|
|
5
|
-
from typing import
|
|
6
|
-
Any,
|
|
7
|
-
Callable,
|
|
8
|
-
TypedDict,
|
|
9
|
-
TypeVar,
|
|
10
|
-
Union,
|
|
11
|
-
)
|
|
4
|
+
from collections.abc import Callable, Coroutine, Generator
|
|
5
|
+
from typing import Any, TypeAlias, TypedDict, TypeVar
|
|
12
6
|
|
|
13
7
|
|
|
14
8
|
class _BaseDetails(TypedDict):
|
|
@@ -36,6 +30,7 @@ class _BaseContextDetails(TypedDict):
|
|
|
36
30
|
|
|
37
31
|
class _ContextCallDetails(TypedDict, total=False):
|
|
38
32
|
wait: float # present in the on_backoff handler case
|
|
33
|
+
value: Any # present in the on_predicate decorator case
|
|
39
34
|
exception: Exception # present in the on_giveup handler case
|
|
40
35
|
|
|
41
36
|
|
|
@@ -48,21 +43,16 @@ class ContextDetails(_BaseContextDetails, _ContextCallDetails, total=False):
|
|
|
48
43
|
|
|
49
44
|
T = TypeVar("T")
|
|
50
45
|
|
|
46
|
+
_AnyLogger = logging.Logger | logging.LoggerAdapter # type: ignore[type-arg]
|
|
47
|
+
_AnyLoggerOrName = str | _AnyLogger
|
|
51
48
|
_CallableT = TypeVar("_CallableT", bound=Callable[..., Any]) # ruff:ignore[unused-private-type-var]
|
|
52
|
-
|
|
53
|
-
Callable[[
|
|
54
|
-
Callable[[
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
Callable[[ContextDetails], None],
|
|
58
|
-
Callable[[ContextDetails], Coroutine[Any, Any, None]],
|
|
59
|
-
]
|
|
49
|
+
_ContextHandler = (
|
|
50
|
+
Callable[[ContextDetails], None]
|
|
51
|
+
| Callable[[ContextDetails], Coroutine[Any, Any, None]]
|
|
52
|
+
)
|
|
53
|
+
_Handler = Callable[[Details], None] | Callable[[Details], Coroutine[Any, Any, None]]
|
|
60
54
|
_Jitterer = Callable[[float], float]
|
|
61
|
-
_MaybeCallable =
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
_Predicate = Union[
|
|
65
|
-
Callable[[T], bool],
|
|
66
|
-
Callable[[T], Coroutine[Any, Any, bool]],
|
|
67
|
-
]
|
|
55
|
+
_MaybeCallable: TypeAlias = T | Callable[[], T] # ruff: ignore[unused-private-type-alias]
|
|
56
|
+
_MaybeTuple: TypeAlias = T | tuple[T, ...] # ruff: ignore[unused-private-type-alias]
|
|
57
|
+
_Predicate = Callable[[T], bool] | Callable[[T], Coroutine[Any, Any, bool]]
|
|
68
58
|
_WaitGenerator = Callable[..., Generator[float, Any, None]]
|
backoff/_wait_gen.py
CHANGED
|
@@ -2,10 +2,12 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import itertools
|
|
4
4
|
import math
|
|
5
|
-
from typing import TYPE_CHECKING, Any,
|
|
5
|
+
from typing import TYPE_CHECKING, Any, ParamSpec
|
|
6
6
|
|
|
7
7
|
if TYPE_CHECKING:
|
|
8
|
-
from collections.abc import Generator, Iterable
|
|
8
|
+
from collections.abc import Callable, Generator, Iterable
|
|
9
|
+
|
|
10
|
+
P = ParamSpec("P")
|
|
9
11
|
|
|
10
12
|
|
|
11
13
|
def expo(
|
|
@@ -38,7 +40,7 @@ def decay(
|
|
|
38
40
|
decay_factor: float = 1,
|
|
39
41
|
min_value: float | None = None,
|
|
40
42
|
) -> Generator[float, Any, None]:
|
|
41
|
-
"""Generator for exponential decay
|
|
43
|
+
"""Generator for [exponential decay](https://en.wikipedia.org/wiki/Exponential_decay).
|
|
42
44
|
|
|
43
45
|
Args:
|
|
44
46
|
initial_value: initial quantity
|
|
@@ -46,8 +48,6 @@ def decay(
|
|
|
46
48
|
min_value: The minimum value to yield. Once the value in the
|
|
47
49
|
true exponential sequence is lower than this, the value
|
|
48
50
|
of min_value will forever after be yielded.
|
|
49
|
-
|
|
50
|
-
[1] https://en.wikipedia.org/wiki/Exponential_decay
|
|
51
51
|
"""
|
|
52
52
|
# Advance past initial .send() call
|
|
53
53
|
yield 0
|
|
@@ -61,12 +61,12 @@ def decay(
|
|
|
61
61
|
|
|
62
62
|
|
|
63
63
|
def fibo(max_value: int | None = None) -> Generator[int, Any, None]:
|
|
64
|
-
"""Generator for
|
|
64
|
+
"""Generator for Fibonacci decay.
|
|
65
65
|
|
|
66
66
|
Args:
|
|
67
67
|
max_value: The maximum value to yield. Once the value in the
|
|
68
|
-
true
|
|
69
|
-
of max_value will forever after be yielded.
|
|
68
|
+
true Fibonacci sequence exceeds this, the value
|
|
69
|
+
of `max_value` will forever after be yielded.
|
|
70
70
|
"""
|
|
71
71
|
# Advance past initial .send() call
|
|
72
72
|
yield 0
|
|
@@ -99,10 +99,84 @@ def constant(interval: float | Iterable[float] = 1) -> Generator[float, Any, Non
|
|
|
99
99
|
yield val
|
|
100
100
|
|
|
101
101
|
|
|
102
|
+
def capped(
|
|
103
|
+
wait_gen: Callable[P, Generator[float, Any, None]],
|
|
104
|
+
*,
|
|
105
|
+
min_value: float | None = None,
|
|
106
|
+
max_value: float | None = None,
|
|
107
|
+
) -> Callable[P, Generator[float, Any, None]]:
|
|
108
|
+
"""Wraps a wait generator, clamping each value it yields.
|
|
109
|
+
|
|
110
|
+
Useful for wait generators without their own bound, such as
|
|
111
|
+
[`constant`][backoff.constant] or [`runtime`][backoff.runtime] (e.g.
|
|
112
|
+
capping a server-provided `Retry-After` value so a misbehaving server
|
|
113
|
+
can't stall retries indefinitely):
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
backoff.on_predicate(
|
|
117
|
+
backoff.capped(backoff.runtime, max_value=60),
|
|
118
|
+
predicate=lambda r: r.status_code == 429,
|
|
119
|
+
value=lambda r: int(r.headers.get("Retry-After", 1)),
|
|
120
|
+
)
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
wait_gen: The wait generator to wrap.
|
|
125
|
+
min_value: The minimum value to yield. Values below this are
|
|
126
|
+
raised to min_value.
|
|
127
|
+
max_value: The maximum value to yield. Values above this are
|
|
128
|
+
lowered to max_value.
|
|
129
|
+
"""
|
|
130
|
+
if min_value is not None and max_value is not None and min_value > max_value:
|
|
131
|
+
msg = f"`min_value` {min_value} cannot be greater than `max_value` {max_value}"
|
|
132
|
+
raise ValueError(msg)
|
|
133
|
+
|
|
134
|
+
def generator(*args: P.args, **kwargs: P.kwargs) -> Generator[float, Any, None]:
|
|
135
|
+
gen = wait_gen(*args, **kwargs)
|
|
136
|
+
gen.send(None)
|
|
137
|
+
|
|
138
|
+
send_value = yield 0
|
|
139
|
+
while True:
|
|
140
|
+
try:
|
|
141
|
+
value = gen.send(send_value)
|
|
142
|
+
except StopIteration:
|
|
143
|
+
return
|
|
144
|
+
if max_value is not None:
|
|
145
|
+
value = min(value, max_value)
|
|
146
|
+
if min_value is not None:
|
|
147
|
+
value = max(value, min_value)
|
|
148
|
+
send_value = yield value
|
|
149
|
+
|
|
150
|
+
return generator
|
|
151
|
+
|
|
152
|
+
|
|
102
153
|
def runtime(*, value: Callable[[Any], float]) -> Generator[float, Any, None]:
|
|
103
154
|
"""Generator that is based on parsing the return value or thrown
|
|
104
155
|
exception of the decorated method
|
|
105
156
|
|
|
157
|
+
Useful for honoring a server-specified retry delay, e.g. an HTTP
|
|
158
|
+
`Retry-After` header, rather than a fixed wait sequence:
|
|
159
|
+
|
|
160
|
+
```python
|
|
161
|
+
# with on_predicate, `value` receives the return value
|
|
162
|
+
@backoff.on_predicate(
|
|
163
|
+
backoff.runtime,
|
|
164
|
+
predicate=lambda r: r.status_code == 429,
|
|
165
|
+
value=lambda r: int(r.headers.get("Retry-After", 1)),
|
|
166
|
+
)
|
|
167
|
+
def get_page():
|
|
168
|
+
return requests.get(url)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
# with on_exception, `value` receives the raised exception
|
|
172
|
+
@backoff.on_exception(
|
|
173
|
+
backoff.runtime,
|
|
174
|
+
RetryableError,
|
|
175
|
+
value=lambda e: e.wait_seconds,
|
|
176
|
+
)
|
|
177
|
+
def get_page(): ...
|
|
178
|
+
```
|
|
179
|
+
|
|
106
180
|
Args:
|
|
107
181
|
value: a callable which takes as input the decorated
|
|
108
182
|
function's return value or thrown exception and
|
backoff/types.py
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: python-backoff
|
|
3
|
-
Version:
|
|
4
|
-
|
|
3
|
+
Version: 3.1.0
|
|
4
|
+
Import-Name: backoff
|
|
5
|
+
Summary: Function decoration and context managers for backoff and retry
|
|
5
6
|
Project-URL: Documentation, https://python-backoff.github.io/backoff
|
|
6
7
|
Project-URL: Repository, https://github.com/python-backoff/backoff
|
|
7
8
|
Author-email: Bob Green <rgreen@aquent.com>
|
|
@@ -15,17 +16,17 @@ Classifier: Natural Language :: English
|
|
|
15
16
|
Classifier: Operating System :: OS Independent
|
|
16
17
|
Classifier: Programming Language :: Python
|
|
17
18
|
Classifier: Programming Language :: Python :: 3
|
|
18
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
19
19
|
Classifier: Programming Language :: Python :: 3.10
|
|
20
20
|
Classifier: Programming Language :: Python :: 3.11
|
|
21
21
|
Classifier: Programming Language :: Python :: 3.12
|
|
22
22
|
Classifier: Programming Language :: Python :: 3.13
|
|
23
23
|
Classifier: Programming Language :: Python :: 3.14
|
|
24
24
|
Classifier: Programming Language :: Python :: 3.15
|
|
25
|
+
Classifier: Programming Language :: Python :: 3.16
|
|
25
26
|
Classifier: Topic :: Internet :: WWW/HTTP
|
|
26
27
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
27
28
|
Classifier: Topic :: Utilities
|
|
28
|
-
Requires-Python: >=3.
|
|
29
|
+
Requires-Python: >=3.10
|
|
29
30
|
Description-Content-Type: text/x-rst
|
|
30
31
|
|
|
31
32
|
backoff
|
|
@@ -40,9 +41,11 @@ backoff
|
|
|
40
41
|
.. image:: https://img.shields.io/github/license/python-backoff/backoff
|
|
41
42
|
:target: https://github.com/python-backoff/backoff/blob/main/LICENSE
|
|
42
43
|
|
|
44
|
+
*Note: this is an actively maintained fork of https://github.com/litl/backoff.*
|
|
45
|
+
|
|
43
46
|
**Function decoration for backoff and retry**
|
|
44
47
|
|
|
45
|
-
This
|
|
48
|
+
This library provides function decorators which can be used to wrap a
|
|
46
49
|
function such that it will be retried until some condition is met. It
|
|
47
50
|
is meant to be of use when accessing unreliable resources with the
|
|
48
51
|
potential for intermittent failures i.e. network resources and external
|
|
@@ -53,6 +56,8 @@ Decorators support both regular functions for synchronous code and
|
|
|
53
56
|
`asyncio <https://docs.python.org/3/library/asyncio.html>`__'s coroutines
|
|
54
57
|
for asynchronous code.
|
|
55
58
|
|
|
59
|
+
Context managers are also available to enable retries of arbitrary code blocks.
|
|
60
|
+
|
|
56
61
|
Examples
|
|
57
62
|
========
|
|
58
63
|
|
|
@@ -183,7 +188,7 @@ The ``on_predicate`` decorator is used to retry when a particular
|
|
|
183
188
|
condition is true of the return value of the target function. This may
|
|
184
189
|
be useful when polling a resource for externally generated content.
|
|
185
190
|
|
|
186
|
-
Here's an example which uses a
|
|
191
|
+
Here's an example which uses a Fibonacci sequence backoff when the
|
|
187
192
|
return value of the target function is the empty list:
|
|
188
193
|
|
|
189
194
|
.. code-block:: python
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
backoff/__init__.py,sha256=pTD7ZdghoWQHaMhPwKe7S04yfLjCK7SZYkbUBa1ibGY,1080
|
|
2
|
+
backoff/_async.py,sha256=UMAls92zTDOc_W8nmaTIWAEznQgE-bnSCjfoYerVG9Q,9235
|
|
3
|
+
backoff/_common.py,sha256=d7D0crETK5Gstdb2qh1afPEEAJSwu1nqFt7qnw67HgY,8307
|
|
4
|
+
backoff/_decorator.py,sha256=RO_z4IEDifNS7R-2MiLKtVAeI6b2KYGxlJ0SKjx21AU,18176
|
|
5
|
+
backoff/_jitter.py,sha256=wzQHBDNuYg7owbc3fcZLOr5uhhRgqiuncU5rIYOB9lY,766
|
|
6
|
+
backoff/_sync.py,sha256=bUoAkIBUB1TEyvSvHQSulesSjuyvRxD6Lq2SbLcw96o,6710
|
|
7
|
+
backoff/_typing.py,sha256=ZMEhNamBlnXcOqzSCvGqZZ0uc0KVu9-mOKcxccCCPAs,1996
|
|
8
|
+
backoff/_wait_gen.py,sha256=lEAyqcFdSH8cgjNotBMhb0bWaK4fNxzlCZbylvGN5Qc,5475
|
|
9
|
+
backoff/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
|
+
backoff/types.py,sha256=obBefroeCaGlPbOQSw1KIKvYl4SKFr8_YvwogvorVnw,97
|
|
11
|
+
python_backoff-3.1.0.dist-info/METADATA,sha256=Yi6wAsS2d9G4UTtOMCxKUK5ifN4nt7dD46H-9i6UxhE,15016
|
|
12
|
+
python_backoff-3.1.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
|
|
13
|
+
python_backoff-3.1.0.dist-info/licenses/LICENSE,sha256=KmtNX4hNTXob8E6n3xlEzxKzLjWnmobQoHWi0_QPuaw,1077
|
|
14
|
+
python_backoff-3.1.0.dist-info/RECORD,,
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
backoff/__init__.py,sha256=BeZF5I4RDsBoy2C1xNvabGaJ5ez6m8JFteZYElAL_RM,988
|
|
2
|
-
backoff/_async.py,sha256=xx5z-gkSK484W09d2On9vTQvnMbK-KyzVuiZPR67zFA,8936
|
|
3
|
-
backoff/_common.py,sha256=2p0oj1toXysJ7jVxYr6fZ3Q3Pa7CYpje44k116C7il4,8939
|
|
4
|
-
backoff/_decorator.py,sha256=rMTzMgRz7tPI0sIy6tCI9Q0RBStRX7bcgYuiYg9TlKU,16114
|
|
5
|
-
backoff/_jitter.py,sha256=wzQHBDNuYg7owbc3fcZLOr5uhhRgqiuncU5rIYOB9lY,766
|
|
6
|
-
backoff/_sync.py,sha256=iL5JBScW-l93opMSs09lSHMBFnrDBWjpml_Ar12yJME,6510
|
|
7
|
-
backoff/_typing.py,sha256=RkIqYIJwjUlhwg5Z6TIF3HkCIv1oKDAtDGRDZJO0K48,1856
|
|
8
|
-
backoff/_wait_gen.py,sha256=uyDnfEcOzUpLeGqktkVZ2pBr-NKu4K03qYug-VR_KJo,3056
|
|
9
|
-
backoff/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
|
-
backoff/types.py,sha256=rOQAfuetqVwfEIuzVvK5D_w97A3wrZ9lQ8HwuJ_uq5s,59
|
|
11
|
-
python_backoff-2.4.0.dist-info/METADATA,sha256=Xek_me-7Sxh1vUE8Tb5NSdeSylTrBIUruOUJwLsQ7XQ,14809
|
|
12
|
-
python_backoff-2.4.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
|
|
13
|
-
python_backoff-2.4.0.dist-info/licenses/LICENSE,sha256=KmtNX4hNTXob8E6n3xlEzxKzLjWnmobQoHWi0_QPuaw,1077
|
|
14
|
-
python_backoff-2.4.0.dist-info/RECORD,,
|
|
File without changes
|