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/_common.py
CHANGED
|
@@ -1,43 +1,71 @@
|
|
|
1
|
-
|
|
1
|
+
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import functools
|
|
4
4
|
import logging
|
|
5
|
-
import
|
|
5
|
+
import time
|
|
6
6
|
import traceback
|
|
7
7
|
import warnings
|
|
8
|
+
from typing import TYPE_CHECKING, Any, Callable, TypeVar
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
import sys
|
|
12
|
+
from collections.abc import Generator, Iterable
|
|
13
|
+
|
|
14
|
+
from backoff._typing import (
|
|
15
|
+
ContextDetails,
|
|
16
|
+
Details,
|
|
17
|
+
_ContextHandler,
|
|
18
|
+
_Jitterer,
|
|
19
|
+
_MaybeCallable,
|
|
20
|
+
_WaitGenerator,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
if sys.version_info >= (3, 11):
|
|
24
|
+
from typing import Self
|
|
25
|
+
else:
|
|
26
|
+
from typing_extensions import Self
|
|
8
27
|
|
|
9
28
|
|
|
10
29
|
# Use module-specific logger with a default null handler.
|
|
11
|
-
_logger = logging.getLogger(
|
|
30
|
+
_logger = logging.getLogger("backoff")
|
|
12
31
|
_logger.addHandler(logging.NullHandler()) # pragma: no cover
|
|
13
32
|
_logger.setLevel(logging.INFO)
|
|
14
33
|
|
|
34
|
+
T = TypeVar("T")
|
|
35
|
+
_HandlerT = TypeVar("_HandlerT")
|
|
36
|
+
|
|
15
37
|
|
|
16
38
|
# Evaluate arg that can be either a fixed value or a callable.
|
|
17
|
-
def _maybe_call(f, *args, **kwargs):
|
|
39
|
+
def _maybe_call(f: _MaybeCallable[T] | None, *args: Any, **kwargs: Any) -> T | None:
|
|
18
40
|
if callable(f):
|
|
19
41
|
try:
|
|
20
|
-
return f(*args, **kwargs)
|
|
42
|
+
return f(*args, **kwargs) # ty:ignore[call-top-callable, invalid-return-type]
|
|
21
43
|
except TypeError:
|
|
22
|
-
return f
|
|
44
|
+
return f # type: ignore[return-value] # ty:ignore[invalid-return-type]
|
|
23
45
|
else:
|
|
24
46
|
return f
|
|
25
47
|
|
|
26
48
|
|
|
27
|
-
def _init_wait_gen(
|
|
49
|
+
def _init_wait_gen(
|
|
50
|
+
wait_gen: _WaitGenerator,
|
|
51
|
+
wait_gen_kwargs: dict[str, Any],
|
|
52
|
+
) -> Generator[float, Any, None]:
|
|
28
53
|
kwargs = {k: _maybe_call(v) for k, v in wait_gen_kwargs.items()}
|
|
29
54
|
initialized = wait_gen(**kwargs)
|
|
30
55
|
initialized.send(None) # Initialize with an empty send
|
|
31
56
|
return initialized
|
|
32
57
|
|
|
33
58
|
|
|
34
|
-
def _next_wait(
|
|
59
|
+
def _next_wait(
|
|
60
|
+
wait: Generator[float, None, None],
|
|
61
|
+
send_value: Any,
|
|
62
|
+
jitter: _Jitterer | None,
|
|
63
|
+
elapsed: float,
|
|
64
|
+
max_time: float | None,
|
|
65
|
+
) -> float:
|
|
35
66
|
value = wait.send(send_value)
|
|
36
67
|
try:
|
|
37
|
-
if jitter is not None
|
|
38
|
-
seconds = jitter(value)
|
|
39
|
-
else:
|
|
40
|
-
seconds = value
|
|
68
|
+
seconds = jitter(value) if jitter is not None else value
|
|
41
69
|
except TypeError:
|
|
42
70
|
warnings.warn(
|
|
43
71
|
"Nullary jitter function signature is deprecated. Use "
|
|
@@ -47,7 +75,7 @@ def _next_wait(wait, send_value, jitter, elapsed, max_time):
|
|
|
47
75
|
stacklevel=2,
|
|
48
76
|
)
|
|
49
77
|
|
|
50
|
-
seconds = value + jitter()
|
|
78
|
+
seconds = value + jitter() # type: ignore[call-arg, misc] # ty:ignore[missing-argument, call-non-callable]
|
|
51
79
|
|
|
52
80
|
# don't sleep longer than remaining allotted max_time
|
|
53
81
|
if max_time is not None:
|
|
@@ -56,7 +84,103 @@ def _next_wait(wait, send_value, jitter, elapsed, max_time):
|
|
|
56
84
|
return seconds
|
|
57
85
|
|
|
58
86
|
|
|
59
|
-
|
|
87
|
+
class _RetryState:
|
|
88
|
+
"""Bookkeeping shared by the sync and async retry loops.
|
|
89
|
+
|
|
90
|
+
Tracks try count and elapsed time, and owns the initialized wait
|
|
91
|
+
generator, so `retry_predicate`/`retry_exception` only need to drive
|
|
92
|
+
the call/check/sleep sequence around it.
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
__slots__ = (
|
|
96
|
+
"elapsed",
|
|
97
|
+
"max_time",
|
|
98
|
+
"max_tries",
|
|
99
|
+
"start",
|
|
100
|
+
"tries",
|
|
101
|
+
"wait",
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
def __init__(
|
|
105
|
+
self,
|
|
106
|
+
wait_gen: _WaitGenerator,
|
|
107
|
+
wait_gen_kwargs: dict[str, Any],
|
|
108
|
+
*,
|
|
109
|
+
max_tries: _MaybeCallable[int] | None = None,
|
|
110
|
+
max_time: _MaybeCallable[float] | None = None,
|
|
111
|
+
) -> None:
|
|
112
|
+
self.tries = 0
|
|
113
|
+
self.elapsed: float = 0
|
|
114
|
+
self.start = time.monotonic()
|
|
115
|
+
self.max_tries = _maybe_call(max_tries)
|
|
116
|
+
self.max_time = _maybe_call(max_time)
|
|
117
|
+
self.wait = _init_wait_gen(wait_gen, wait_gen_kwargs)
|
|
118
|
+
|
|
119
|
+
def start_attempt(self) -> None:
|
|
120
|
+
self.tries += 1
|
|
121
|
+
|
|
122
|
+
def record_elapsed(self) -> float:
|
|
123
|
+
self.elapsed = time.monotonic() - self.start
|
|
124
|
+
return self.elapsed
|
|
125
|
+
|
|
126
|
+
def exhausted(self) -> bool:
|
|
127
|
+
max_tries_exceeded = self.tries == self.max_tries
|
|
128
|
+
max_time_exceeded = self.max_time is not None and self.elapsed >= self.max_time
|
|
129
|
+
return max_tries_exceeded or max_time_exceeded
|
|
130
|
+
|
|
131
|
+
def next_wait(self, send_value: Any, jitter: _Jitterer | None) -> float:
|
|
132
|
+
return _next_wait(self.wait, send_value, jitter, self.elapsed, self.max_time)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class _Attempt:
|
|
136
|
+
"""A single attempt yielded by `retry_context`/`aretry_context`.
|
|
137
|
+
|
|
138
|
+
Used as `with attempt: ...`. Exceptions matching `exception_types` are
|
|
139
|
+
caught here so the driving generator (not this object) decides whether
|
|
140
|
+
to retry, sleep, or let the exception propagate; anything else, or no
|
|
141
|
+
exception at all, is left for the `with` block's normal exit behavior.
|
|
142
|
+
"""
|
|
143
|
+
|
|
144
|
+
__slots__ = (
|
|
145
|
+
"exception",
|
|
146
|
+
"exception_types",
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
def __init__(
|
|
150
|
+
self,
|
|
151
|
+
exception_types: type[Exception] | tuple[type[Exception], ...],
|
|
152
|
+
) -> None:
|
|
153
|
+
self.exception_types = exception_types
|
|
154
|
+
self.exception: BaseException | None = None
|
|
155
|
+
|
|
156
|
+
def __enter__(self) -> Self:
|
|
157
|
+
return self
|
|
158
|
+
|
|
159
|
+
def __exit__(
|
|
160
|
+
self,
|
|
161
|
+
exc_type: type[BaseException] | None,
|
|
162
|
+
exc: BaseException | None,
|
|
163
|
+
tb: object,
|
|
164
|
+
) -> bool:
|
|
165
|
+
if exc_type is None:
|
|
166
|
+
# self.outcome = _Success()
|
|
167
|
+
return False
|
|
168
|
+
|
|
169
|
+
if not issubclass(exc_type, self.exception_types):
|
|
170
|
+
return False # not ours; propagate immediately
|
|
171
|
+
|
|
172
|
+
self.exception = exc
|
|
173
|
+
return True # suppress for now; the driving generator decides next
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _dispatch_handlers(handlers: Iterable[_ContextHandler], **details: Any) -> None:
|
|
177
|
+
for hdlr in handlers:
|
|
178
|
+
hdlr(details) # type: ignore[arg-type] # ty:ignore[invalid-argument-type]
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _prepare_logger(
|
|
182
|
+
logger: str | logging.Logger | logging.LoggerAdapter | None,
|
|
183
|
+
) -> logging.Logger | logging.LoggerAdapter | None:
|
|
60
184
|
if isinstance(logger, str):
|
|
61
185
|
logger = logging.getLogger(logger)
|
|
62
186
|
return logger
|
|
@@ -65,56 +189,103 @@ def _prepare_logger(logger):
|
|
|
65
189
|
# Configure handler list with user specified handler and optionally
|
|
66
190
|
# with a default handler bound to the specified logger.
|
|
67
191
|
def _config_handlers(
|
|
68
|
-
user_handlers
|
|
69
|
-
|
|
70
|
-
|
|
192
|
+
user_handlers: _HandlerT | Iterable[_HandlerT] | None,
|
|
193
|
+
*,
|
|
194
|
+
default_handler: Callable[..., None] | None = None,
|
|
195
|
+
logger: logging.Logger | logging.LoggerAdapter | None = None,
|
|
196
|
+
log_level: int | None = None,
|
|
197
|
+
) -> list[_HandlerT]:
|
|
198
|
+
handlers: list[_HandlerT] = []
|
|
71
199
|
if logger is not None:
|
|
72
200
|
assert log_level is not None, "Log level is not specified"
|
|
201
|
+
assert default_handler is not None, "Default handler is not specified"
|
|
73
202
|
# bind the specified logger to the default log handler
|
|
74
203
|
log_handler = functools.partial(
|
|
75
|
-
default_handler,
|
|
204
|
+
default_handler,
|
|
205
|
+
logger=logger,
|
|
206
|
+
log_level=log_level,
|
|
76
207
|
)
|
|
77
|
-
handlers.append(log_handler)
|
|
208
|
+
handlers.append(log_handler) # type: ignore[arg-type] # ty:ignore[invalid-argument-type]
|
|
78
209
|
|
|
79
210
|
if user_handlers is None:
|
|
80
211
|
return handlers
|
|
81
212
|
|
|
82
213
|
# user specified handlers can either be an iterable of handlers
|
|
83
214
|
# or a single handler. either way append them to the list.
|
|
84
|
-
if hasattr(user_handlers,
|
|
215
|
+
if hasattr(user_handlers, "__iter__"):
|
|
85
216
|
# add all handlers in the iterable
|
|
86
|
-
|
|
217
|
+
# pyrefly: ignore [bad-argument-type]
|
|
218
|
+
handlers += list(user_handlers) # ty:ignore[invalid-argument-type]
|
|
87
219
|
else:
|
|
88
220
|
# append a single handler
|
|
221
|
+
# pyrefly: ignore [bad-argument-type]
|
|
89
222
|
handlers.append(user_handlers)
|
|
90
223
|
|
|
91
224
|
return handlers
|
|
92
225
|
|
|
93
226
|
|
|
94
227
|
# Default backoff handler
|
|
95
|
-
def _log_backoff(
|
|
228
|
+
def _log_backoff(
|
|
229
|
+
details: Details,
|
|
230
|
+
logger: logging.Logger | logging.LoggerAdapter,
|
|
231
|
+
log_level: int,
|
|
232
|
+
) -> None:
|
|
96
233
|
msg = "Backing off %s(...) for %.1fs (%s)"
|
|
97
|
-
log_args = [details[
|
|
234
|
+
log_args = [details["target"].__name__, details["wait"]] # ty:ignore[unresolved-attribute]
|
|
98
235
|
|
|
99
|
-
|
|
236
|
+
exc = details.get("exception")
|
|
100
237
|
if exc is not None:
|
|
101
|
-
exc_fmt = traceback.format_exception_only(
|
|
238
|
+
exc_fmt = traceback.format_exception_only(type(exc), exc)[-1]
|
|
102
239
|
log_args.append(exc_fmt.rstrip("\n"))
|
|
103
240
|
else:
|
|
104
|
-
log_args.append(details[
|
|
241
|
+
log_args.append(details["value"])
|
|
105
242
|
logger.log(log_level, msg, *log_args)
|
|
106
243
|
|
|
107
244
|
|
|
108
245
|
# Default giveup handler
|
|
109
|
-
def _log_giveup(
|
|
246
|
+
def _log_giveup(
|
|
247
|
+
details: Details,
|
|
248
|
+
logger: logging.Logger | logging.LoggerAdapter,
|
|
249
|
+
log_level: int,
|
|
250
|
+
) -> None:
|
|
110
251
|
msg = "Giving up %s(...) after %d tries (%s)"
|
|
111
|
-
log_args = [details[
|
|
252
|
+
log_args = [details["target"].__name__, details["tries"]] # ty:ignore[unresolved-attribute]
|
|
112
253
|
|
|
113
|
-
|
|
254
|
+
exc = details.get("exception")
|
|
114
255
|
if exc is not None:
|
|
115
|
-
exc_fmt = traceback.format_exception_only(
|
|
256
|
+
exc_fmt = traceback.format_exception_only(type(exc), exc)[-1]
|
|
116
257
|
log_args.append(exc_fmt.rstrip("\n"))
|
|
117
258
|
else:
|
|
118
|
-
log_args.append(details[
|
|
259
|
+
log_args.append(details["value"])
|
|
119
260
|
|
|
120
261
|
logger.log(log_level, msg, *log_args)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
# Default backoff handler for retry_context/aretry_context (no wrapped
|
|
265
|
+
# callable, so no name to log; the exception is read from `details`
|
|
266
|
+
# directly since it's no longer the active exception by this point).
|
|
267
|
+
def _log_backoff_context(
|
|
268
|
+
details: ContextDetails,
|
|
269
|
+
logger: logging.Logger | logging.LoggerAdapter,
|
|
270
|
+
log_level: int,
|
|
271
|
+
) -> None:
|
|
272
|
+
logger.log(
|
|
273
|
+
log_level,
|
|
274
|
+
"Backing off retry_context(...) for %.1fs (%s)",
|
|
275
|
+
details["wait"],
|
|
276
|
+
details.get("exception"),
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
# Default giveup handler for retry_context/aretry_context.
|
|
281
|
+
def _log_giveup_context(
|
|
282
|
+
details: ContextDetails,
|
|
283
|
+
logger: logging.Logger | logging.LoggerAdapter,
|
|
284
|
+
log_level: int,
|
|
285
|
+
) -> None:
|
|
286
|
+
logger.log(
|
|
287
|
+
log_level,
|
|
288
|
+
"Giving up retry_context(...) after %d tries (%s)",
|
|
289
|
+
details["tries"],
|
|
290
|
+
details.get("exception"),
|
|
291
|
+
)
|
backoff/_decorator.py
CHANGED
|
@@ -1,42 +1,62 @@
|
|
|
1
|
-
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
2
3
|
import inspect
|
|
3
4
|
import logging
|
|
4
5
|
import operator
|
|
5
|
-
from typing import Any, Callable,
|
|
6
|
+
from typing import TYPE_CHECKING, Any, Callable, TypeVar
|
|
6
7
|
|
|
8
|
+
from backoff import _async, _sync
|
|
7
9
|
from backoff._common import (
|
|
8
|
-
_prepare_logger,
|
|
9
10
|
_config_handlers,
|
|
10
11
|
_log_backoff,
|
|
12
|
+
_log_backoff_context,
|
|
11
13
|
_log_giveup,
|
|
14
|
+
_log_giveup_context,
|
|
15
|
+
_prepare_logger,
|
|
12
16
|
)
|
|
13
17
|
from backoff._jitter import full_jitter
|
|
14
|
-
from backoff import
|
|
15
|
-
from backoff._typing import (
|
|
16
|
-
_CallableT,
|
|
17
|
-
_Handler,
|
|
18
|
-
_Jitterer,
|
|
19
|
-
_MaybeCallable,
|
|
20
|
-
_MaybeLogger,
|
|
21
|
-
_MaybeSequence,
|
|
22
|
-
_Predicate,
|
|
23
|
-
_WaitGenerator,
|
|
24
|
-
)
|
|
18
|
+
from backoff._wait_gen import expo
|
|
25
19
|
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
import sys
|
|
22
|
+
from collections.abc import AsyncGenerator, Generator, Iterable
|
|
26
23
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
24
|
+
from backoff._common import _Attempt
|
|
25
|
+
from backoff._typing import (
|
|
26
|
+
_ContextHandler,
|
|
27
|
+
_Handler,
|
|
28
|
+
_Jitterer,
|
|
29
|
+
_MaybeCallable,
|
|
30
|
+
_MaybeLogger,
|
|
31
|
+
_MaybeTuple,
|
|
32
|
+
_Predicate,
|
|
33
|
+
_WaitGenerator,
|
|
34
|
+
)
|
|
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
|
+
|
|
45
|
+
def on_predicate(
|
|
46
|
+
wait_gen: _WaitGenerator,
|
|
47
|
+
predicate: _Predicate[Any] = operator.not_,
|
|
48
|
+
*,
|
|
49
|
+
max_tries: _MaybeCallable[int] | None = None,
|
|
50
|
+
max_time: _MaybeCallable[float] | None = None,
|
|
51
|
+
jitter: _Jitterer | None = full_jitter,
|
|
52
|
+
on_success: _Handler | Iterable[_Handler] | None = None,
|
|
53
|
+
on_backoff: _Handler | Iterable[_Handler] | None = None,
|
|
54
|
+
on_giveup: _Handler | Iterable[_Handler] | None = None,
|
|
55
|
+
logger: _MaybeLogger = "backoff",
|
|
56
|
+
backoff_log_level: int = logging.INFO,
|
|
57
|
+
giveup_log_level: int = logging.ERROR,
|
|
58
|
+
**wait_gen_kwargs: Any,
|
|
59
|
+
) -> Callable[[Callable[P, T]], Callable[P, T]]:
|
|
40
60
|
"""Returns decorator for backoff and retry triggered by predicate.
|
|
41
61
|
|
|
42
62
|
Args:
|
|
@@ -80,7 +100,8 @@ def on_predicate(wait_gen: _WaitGenerator,
|
|
|
80
100
|
args will first be evaluated and their return values passed.
|
|
81
101
|
This is useful for runtime configuration.
|
|
82
102
|
"""
|
|
83
|
-
|
|
103
|
+
|
|
104
|
+
def decorate(target: Callable[P, T]) -> Callable[P, T]:
|
|
84
105
|
nonlocal logger, on_success, on_backoff, on_giveup
|
|
85
106
|
|
|
86
107
|
logger = _prepare_logger(logger)
|
|
@@ -120,21 +141,23 @@ def on_predicate(wait_gen: _WaitGenerator,
|
|
|
120
141
|
return decorate
|
|
121
142
|
|
|
122
143
|
|
|
123
|
-
def on_exception(
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
144
|
+
def on_exception(
|
|
145
|
+
wait_gen: _WaitGenerator,
|
|
146
|
+
exception: _MaybeTuple[type[Exception]],
|
|
147
|
+
*,
|
|
148
|
+
max_tries: _MaybeCallable[int] | None = None,
|
|
149
|
+
max_time: _MaybeCallable[float] | None = None,
|
|
150
|
+
jitter: _Jitterer | None = full_jitter,
|
|
151
|
+
giveup: _Predicate[Exception] = lambda e: False,
|
|
152
|
+
on_success: _Handler | Iterable[_Handler] | None = None,
|
|
153
|
+
on_backoff: _Handler | Iterable[_Handler] | None = None,
|
|
154
|
+
on_giveup: _Handler | Iterable[_Handler] | None = None,
|
|
155
|
+
raise_on_giveup: bool = True,
|
|
156
|
+
logger: _MaybeLogger = "backoff",
|
|
157
|
+
backoff_log_level: int = logging.INFO,
|
|
158
|
+
giveup_log_level: int = logging.ERROR,
|
|
159
|
+
**wait_gen_kwargs: Any,
|
|
160
|
+
) -> Callable[[Callable[P, T]], Callable[P, T]]:
|
|
138
161
|
"""Returns decorator for backoff and retry triggered by exception.
|
|
139
162
|
|
|
140
163
|
Args:
|
|
@@ -180,7 +203,8 @@ def on_exception(wait_gen: _WaitGenerator,
|
|
|
180
203
|
args will first be evaluated and their return values passed.
|
|
181
204
|
This is useful for runtime configuration.
|
|
182
205
|
"""
|
|
183
|
-
|
|
206
|
+
|
|
207
|
+
def decorate(target: Callable[P, T]) -> Callable[P, T]:
|
|
184
208
|
nonlocal logger, on_success, on_backoff, on_giveup
|
|
185
209
|
|
|
186
210
|
logger = _prepare_logger(logger)
|
|
@@ -220,3 +244,155 @@ def on_exception(wait_gen: _WaitGenerator,
|
|
|
220
244
|
|
|
221
245
|
# Return a function which decorates a target with a retry loop.
|
|
222
246
|
return decorate
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def retry_context(
|
|
250
|
+
exception: _MaybeTuple[type[Exception]] = Exception,
|
|
251
|
+
wait_gen: _WaitGenerator = expo,
|
|
252
|
+
*,
|
|
253
|
+
max_tries: _MaybeCallable[int] | None = None,
|
|
254
|
+
max_time: _MaybeCallable[float] | None = None,
|
|
255
|
+
jitter: _Jitterer | None = full_jitter,
|
|
256
|
+
giveup: _Predicate[BaseException] = lambda e: False,
|
|
257
|
+
on_success: _ContextHandler | Iterable[_ContextHandler] | None = None,
|
|
258
|
+
on_backoff: _ContextHandler | Iterable[_ContextHandler] | None = None,
|
|
259
|
+
on_giveup: _ContextHandler | Iterable[_ContextHandler] | None = None,
|
|
260
|
+
raise_on_giveup: bool = True,
|
|
261
|
+
logger: _MaybeLogger = "backoff",
|
|
262
|
+
backoff_log_level: int = logging.INFO,
|
|
263
|
+
giveup_log_level: int = logging.ERROR,
|
|
264
|
+
**wait_gen_kwargs: Any,
|
|
265
|
+
) -> Generator[_Attempt, None, None]:
|
|
266
|
+
"""Returns a generator of retry attempts, for direct use with a `for` loop.
|
|
267
|
+
|
|
268
|
+
Unlike `on_exception`, this doesn't wrap a whole function; it lets a
|
|
269
|
+
caller retry an arbitrary block of code:
|
|
270
|
+
|
|
271
|
+
for attempt in backoff.retry_context(ValueError, backoff.expo):
|
|
272
|
+
with attempt:
|
|
273
|
+
do_something()
|
|
274
|
+
|
|
275
|
+
Each `attempt` is a context manager: exceptions matching `exception`
|
|
276
|
+
are caught, and the loop either sleeps and retries or lets the
|
|
277
|
+
exception (or a different one) propagate once retries are exhausted.
|
|
278
|
+
Succeeding (no exception raised in the `with` block) ends the loop.
|
|
279
|
+
|
|
280
|
+
Args:
|
|
281
|
+
exception: An exception type (or tuple of types) which triggers
|
|
282
|
+
backoff.
|
|
283
|
+
wait_gen: A generator yielding successive wait times in seconds.
|
|
284
|
+
max_tries: The maximum number of attempts to make before giving
|
|
285
|
+
up. Once exhausted, the exception will be allowed to escape.
|
|
286
|
+
The default value of None means there is no limit to the
|
|
287
|
+
number of tries. If a callable is passed, it will be
|
|
288
|
+
evaluated at runtime and its return value used.
|
|
289
|
+
max_time: The maximum total amount of time to try for before
|
|
290
|
+
giving up. Once expired, the exception will be allowed to
|
|
291
|
+
escape. If a callable is passed, it will be evaluated at
|
|
292
|
+
runtime and its return value used.
|
|
293
|
+
jitter: A function of the value yielded by wait_gen returning
|
|
294
|
+
the actual time to wait. Jittered by default using
|
|
295
|
+
full_jitter; disable with jitter=None.
|
|
296
|
+
giveup: Function accepting an exception instance and returning
|
|
297
|
+
whether or not to give up. Optional. The default is to
|
|
298
|
+
always continue.
|
|
299
|
+
on_success: Callable (or iterable of callables) with a unary
|
|
300
|
+
signature called on success. The parameter is a dict with
|
|
301
|
+
`tries` and `elapsed`.
|
|
302
|
+
on_backoff: Callable (or iterable of callables) called on
|
|
303
|
+
backoff. The parameter dict additionally has `wait` and
|
|
304
|
+
`exception`.
|
|
305
|
+
on_giveup: Callable (or iterable of callables) called when
|
|
306
|
+
giving up. The parameter dict additionally has `exception`.
|
|
307
|
+
raise_on_giveup: Boolean indicating whether the registered
|
|
308
|
+
exception should be raised on giveup. Defaults to `True`.
|
|
309
|
+
logger: Name or Logger object to log to. Defaults to 'backoff'.
|
|
310
|
+
backoff_log_level: log level for the backoff event. Defaults to "INFO"
|
|
311
|
+
giveup_log_level: log level for the give up event. Defaults to "ERROR"
|
|
312
|
+
**wait_gen_kwargs: Any additional keyword args specified will be
|
|
313
|
+
passed to wait_gen when it is initialized.
|
|
314
|
+
"""
|
|
315
|
+
logger = _prepare_logger(logger)
|
|
316
|
+
on_success = _config_handlers(on_success)
|
|
317
|
+
on_backoff = _config_handlers(
|
|
318
|
+
on_backoff,
|
|
319
|
+
default_handler=_log_backoff_context,
|
|
320
|
+
logger=logger,
|
|
321
|
+
log_level=backoff_log_level,
|
|
322
|
+
)
|
|
323
|
+
on_giveup = _config_handlers(
|
|
324
|
+
on_giveup,
|
|
325
|
+
default_handler=_log_giveup_context,
|
|
326
|
+
logger=logger,
|
|
327
|
+
log_level=giveup_log_level,
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
return _sync.retry_context(
|
|
331
|
+
exception,
|
|
332
|
+
wait_gen,
|
|
333
|
+
max_tries=max_tries,
|
|
334
|
+
max_time=max_time,
|
|
335
|
+
jitter=jitter,
|
|
336
|
+
giveup=giveup,
|
|
337
|
+
on_success=on_success,
|
|
338
|
+
on_backoff=on_backoff,
|
|
339
|
+
on_giveup=on_giveup,
|
|
340
|
+
raise_on_giveup=raise_on_giveup,
|
|
341
|
+
wait_gen_kwargs=wait_gen_kwargs,
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def aretry_context(
|
|
346
|
+
exception: _MaybeTuple[type[Exception]] = Exception,
|
|
347
|
+
wait_gen: _WaitGenerator = expo,
|
|
348
|
+
*,
|
|
349
|
+
max_tries: _MaybeCallable[int] | None = None,
|
|
350
|
+
max_time: _MaybeCallable[float] | None = None,
|
|
351
|
+
jitter: _Jitterer | None = full_jitter,
|
|
352
|
+
giveup: _Predicate[BaseException] = lambda e: False,
|
|
353
|
+
on_success: _ContextHandler | Iterable[_ContextHandler] | None = None,
|
|
354
|
+
on_backoff: _ContextHandler | Iterable[_ContextHandler] | None = None,
|
|
355
|
+
on_giveup: _ContextHandler | Iterable[_ContextHandler] | None = None,
|
|
356
|
+
raise_on_giveup: bool = True,
|
|
357
|
+
logger: _MaybeLogger = "backoff",
|
|
358
|
+
backoff_log_level: int = logging.INFO,
|
|
359
|
+
giveup_log_level: int = logging.ERROR,
|
|
360
|
+
**wait_gen_kwargs: Any,
|
|
361
|
+
) -> AsyncGenerator[_Attempt, None]:
|
|
362
|
+
"""Async counterpart to `retry_context`, for use with `async for`.
|
|
363
|
+
|
|
364
|
+
async for attempt in backoff.aretry_context(ValueError, backoff.expo):
|
|
365
|
+
with attempt:
|
|
366
|
+
await do_something()
|
|
367
|
+
|
|
368
|
+
`giveup` and the handlers may be sync or async callables. See
|
|
369
|
+
`retry_context` for the full argument reference.
|
|
370
|
+
"""
|
|
371
|
+
logger = _prepare_logger(logger)
|
|
372
|
+
on_success = _config_handlers(on_success)
|
|
373
|
+
on_backoff = _config_handlers(
|
|
374
|
+
on_backoff,
|
|
375
|
+
default_handler=_log_backoff_context,
|
|
376
|
+
logger=logger,
|
|
377
|
+
log_level=backoff_log_level,
|
|
378
|
+
)
|
|
379
|
+
on_giveup = _config_handlers(
|
|
380
|
+
on_giveup,
|
|
381
|
+
default_handler=_log_giveup_context,
|
|
382
|
+
logger=logger,
|
|
383
|
+
log_level=giveup_log_level,
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
return _async.aretry_context(
|
|
387
|
+
exception,
|
|
388
|
+
wait_gen,
|
|
389
|
+
max_tries=max_tries,
|
|
390
|
+
max_time=max_time,
|
|
391
|
+
jitter=jitter,
|
|
392
|
+
giveup=giveup,
|
|
393
|
+
on_success=on_success,
|
|
394
|
+
on_backoff=on_backoff,
|
|
395
|
+
on_giveup=on_giveup,
|
|
396
|
+
raise_on_giveup=raise_on_giveup,
|
|
397
|
+
wait_gen_kwargs=wait_gen_kwargs,
|
|
398
|
+
)
|