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/_common.py
CHANGED
|
@@ -1,8 +1,29 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import functools
|
|
2
4
|
import logging
|
|
3
|
-
import
|
|
5
|
+
import time
|
|
4
6
|
import traceback
|
|
5
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
|
|
6
27
|
|
|
7
28
|
|
|
8
29
|
# Use module-specific logger with a default null handler.
|
|
@@ -10,26 +31,38 @@ _logger = logging.getLogger("backoff")
|
|
|
10
31
|
_logger.addHandler(logging.NullHandler()) # pragma: no cover
|
|
11
32
|
_logger.setLevel(logging.INFO)
|
|
12
33
|
|
|
34
|
+
T = TypeVar("T")
|
|
35
|
+
_HandlerT = TypeVar("_HandlerT")
|
|
36
|
+
|
|
13
37
|
|
|
14
38
|
# Evaluate arg that can be either a fixed value or a callable.
|
|
15
|
-
def _maybe_call(f, *args, **kwargs):
|
|
39
|
+
def _maybe_call(f: _MaybeCallable[T] | None, *args: Any, **kwargs: Any) -> T | None:
|
|
16
40
|
if callable(f):
|
|
17
41
|
try:
|
|
18
|
-
return f(*args, **kwargs)
|
|
42
|
+
return f(*args, **kwargs) # ty:ignore[call-top-callable, invalid-return-type]
|
|
19
43
|
except TypeError:
|
|
20
|
-
return f
|
|
44
|
+
return f # type: ignore[return-value] # ty:ignore[invalid-return-type]
|
|
21
45
|
else:
|
|
22
46
|
return f
|
|
23
47
|
|
|
24
48
|
|
|
25
|
-
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]:
|
|
26
53
|
kwargs = {k: _maybe_call(v) for k, v in wait_gen_kwargs.items()}
|
|
27
54
|
initialized = wait_gen(**kwargs)
|
|
28
55
|
initialized.send(None) # Initialize with an empty send
|
|
29
56
|
return initialized
|
|
30
57
|
|
|
31
58
|
|
|
32
|
-
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:
|
|
33
66
|
value = wait.send(send_value)
|
|
34
67
|
try:
|
|
35
68
|
seconds = jitter(value) if jitter is not None else value
|
|
@@ -42,7 +75,7 @@ def _next_wait(wait, send_value, jitter, elapsed, max_time):
|
|
|
42
75
|
stacklevel=2,
|
|
43
76
|
)
|
|
44
77
|
|
|
45
|
-
seconds = value + jitter()
|
|
78
|
+
seconds = value + jitter() # type: ignore[call-arg, misc] # ty:ignore[missing-argument, call-non-callable]
|
|
46
79
|
|
|
47
80
|
# don't sleep longer than remaining allotted max_time
|
|
48
81
|
if max_time is not None:
|
|
@@ -51,7 +84,103 @@ def _next_wait(wait, send_value, jitter, elapsed, max_time):
|
|
|
51
84
|
return seconds
|
|
52
85
|
|
|
53
86
|
|
|
54
|
-
|
|
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:
|
|
55
184
|
if isinstance(logger, str):
|
|
56
185
|
logger = logging.getLogger(logger)
|
|
57
186
|
return logger
|
|
@@ -60,22 +189,23 @@ def _prepare_logger(logger):
|
|
|
60
189
|
# Configure handler list with user specified handler and optionally
|
|
61
190
|
# with a default handler bound to the specified logger.
|
|
62
191
|
def _config_handlers(
|
|
63
|
-
user_handlers,
|
|
192
|
+
user_handlers: _HandlerT | Iterable[_HandlerT] | None,
|
|
64
193
|
*,
|
|
65
|
-
default_handler=None,
|
|
66
|
-
logger=None,
|
|
67
|
-
log_level=None,
|
|
68
|
-
):
|
|
69
|
-
handlers = []
|
|
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] = []
|
|
70
199
|
if logger is not None:
|
|
71
200
|
assert log_level is not None, "Log level is not specified"
|
|
201
|
+
assert default_handler is not None, "Default handler is not specified"
|
|
72
202
|
# bind the specified logger to the default log handler
|
|
73
203
|
log_handler = functools.partial(
|
|
74
204
|
default_handler,
|
|
75
205
|
logger=logger,
|
|
76
206
|
log_level=log_level,
|
|
77
207
|
)
|
|
78
|
-
handlers.append(log_handler)
|
|
208
|
+
handlers.append(log_handler) # type: ignore[arg-type] # ty:ignore[invalid-argument-type]
|
|
79
209
|
|
|
80
210
|
if user_handlers is None:
|
|
81
211
|
return handlers
|
|
@@ -84,22 +214,28 @@ def _config_handlers(
|
|
|
84
214
|
# or a single handler. either way append them to the list.
|
|
85
215
|
if hasattr(user_handlers, "__iter__"):
|
|
86
216
|
# add all handlers in the iterable
|
|
87
|
-
|
|
217
|
+
# pyrefly: ignore [bad-argument-type]
|
|
218
|
+
handlers += list(user_handlers) # ty:ignore[invalid-argument-type]
|
|
88
219
|
else:
|
|
89
220
|
# append a single handler
|
|
221
|
+
# pyrefly: ignore [bad-argument-type]
|
|
90
222
|
handlers.append(user_handlers)
|
|
91
223
|
|
|
92
224
|
return handlers
|
|
93
225
|
|
|
94
226
|
|
|
95
227
|
# Default backoff handler
|
|
96
|
-
def _log_backoff(
|
|
228
|
+
def _log_backoff(
|
|
229
|
+
details: Details,
|
|
230
|
+
logger: logging.Logger | logging.LoggerAdapter,
|
|
231
|
+
log_level: int,
|
|
232
|
+
) -> None:
|
|
97
233
|
msg = "Backing off %s(...) for %.1fs (%s)"
|
|
98
|
-
log_args = [details["target"].__name__, details["wait"]]
|
|
234
|
+
log_args = [details["target"].__name__, details["wait"]] # ty:ignore[unresolved-attribute]
|
|
99
235
|
|
|
100
|
-
|
|
236
|
+
exc = details.get("exception")
|
|
101
237
|
if exc is not None:
|
|
102
|
-
exc_fmt = traceback.format_exception_only(
|
|
238
|
+
exc_fmt = traceback.format_exception_only(type(exc), exc)[-1]
|
|
103
239
|
log_args.append(exc_fmt.rstrip("\n"))
|
|
104
240
|
else:
|
|
105
241
|
log_args.append(details["value"])
|
|
@@ -107,15 +243,49 @@ def _log_backoff(details, logger, log_level):
|
|
|
107
243
|
|
|
108
244
|
|
|
109
245
|
# Default giveup handler
|
|
110
|
-
def _log_giveup(
|
|
246
|
+
def _log_giveup(
|
|
247
|
+
details: Details,
|
|
248
|
+
logger: logging.Logger | logging.LoggerAdapter,
|
|
249
|
+
log_level: int,
|
|
250
|
+
) -> None:
|
|
111
251
|
msg = "Giving up %s(...) after %d tries (%s)"
|
|
112
|
-
log_args = [details["target"].__name__, details["tries"]]
|
|
252
|
+
log_args = [details["target"].__name__, details["tries"]] # ty:ignore[unresolved-attribute]
|
|
113
253
|
|
|
114
|
-
|
|
254
|
+
exc = details.get("exception")
|
|
115
255
|
if exc is not None:
|
|
116
|
-
exc_fmt = traceback.format_exception_only(
|
|
256
|
+
exc_fmt = traceback.format_exception_only(type(exc), exc)[-1]
|
|
117
257
|
log_args.append(exc_fmt.rstrip("\n"))
|
|
118
258
|
else:
|
|
119
259
|
log_args.append(details["value"])
|
|
120
260
|
|
|
121
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,43 +1,62 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import inspect
|
|
2
4
|
import logging
|
|
3
5
|
import operator
|
|
4
|
-
from typing import Any, Callable,
|
|
6
|
+
from typing import TYPE_CHECKING, Any, Callable, TypeVar
|
|
5
7
|
|
|
8
|
+
from backoff import _async, _sync
|
|
6
9
|
from backoff._common import (
|
|
7
|
-
_prepare_logger,
|
|
8
10
|
_config_handlers,
|
|
9
11
|
_log_backoff,
|
|
12
|
+
_log_backoff_context,
|
|
10
13
|
_log_giveup,
|
|
14
|
+
_log_giveup_context,
|
|
15
|
+
_prepare_logger,
|
|
11
16
|
)
|
|
12
17
|
from backoff._jitter import full_jitter
|
|
13
|
-
from backoff import
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
18
|
+
from backoff._wait_gen import expo
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
import sys
|
|
22
|
+
from collections.abc import AsyncGenerator, Generator, Iterable
|
|
23
|
+
|
|
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")
|
|
24
43
|
|
|
25
44
|
|
|
26
45
|
def on_predicate(
|
|
27
46
|
wait_gen: _WaitGenerator,
|
|
28
47
|
predicate: _Predicate[Any] = operator.not_,
|
|
29
48
|
*,
|
|
30
|
-
max_tries:
|
|
31
|
-
max_time:
|
|
32
|
-
jitter:
|
|
33
|
-
on_success:
|
|
34
|
-
on_backoff:
|
|
35
|
-
on_giveup:
|
|
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,
|
|
36
55
|
logger: _MaybeLogger = "backoff",
|
|
37
56
|
backoff_log_level: int = logging.INFO,
|
|
38
57
|
giveup_log_level: int = logging.ERROR,
|
|
39
58
|
**wait_gen_kwargs: Any,
|
|
40
|
-
) -> Callable[[
|
|
59
|
+
) -> Callable[[Callable[P, T]], Callable[P, T]]:
|
|
41
60
|
"""Returns decorator for backoff and retry triggered by predicate.
|
|
42
61
|
|
|
43
62
|
Args:
|
|
@@ -82,7 +101,7 @@ def on_predicate(
|
|
|
82
101
|
This is useful for runtime configuration.
|
|
83
102
|
"""
|
|
84
103
|
|
|
85
|
-
def decorate(target):
|
|
104
|
+
def decorate(target: Callable[P, T]) -> Callable[P, T]:
|
|
86
105
|
nonlocal logger, on_success, on_backoff, on_giveup
|
|
87
106
|
|
|
88
107
|
logger = _prepare_logger(logger)
|
|
@@ -124,21 +143,21 @@ def on_predicate(
|
|
|
124
143
|
|
|
125
144
|
def on_exception(
|
|
126
145
|
wait_gen: _WaitGenerator,
|
|
127
|
-
exception:
|
|
146
|
+
exception: _MaybeTuple[type[Exception]],
|
|
128
147
|
*,
|
|
129
|
-
max_tries:
|
|
130
|
-
max_time:
|
|
131
|
-
jitter:
|
|
148
|
+
max_tries: _MaybeCallable[int] | None = None,
|
|
149
|
+
max_time: _MaybeCallable[float] | None = None,
|
|
150
|
+
jitter: _Jitterer | None = full_jitter,
|
|
132
151
|
giveup: _Predicate[Exception] = lambda e: False,
|
|
133
|
-
on_success:
|
|
134
|
-
on_backoff:
|
|
135
|
-
on_giveup:
|
|
152
|
+
on_success: _Handler | Iterable[_Handler] | None = None,
|
|
153
|
+
on_backoff: _Handler | Iterable[_Handler] | None = None,
|
|
154
|
+
on_giveup: _Handler | Iterable[_Handler] | None = None,
|
|
136
155
|
raise_on_giveup: bool = True,
|
|
137
156
|
logger: _MaybeLogger = "backoff",
|
|
138
157
|
backoff_log_level: int = logging.INFO,
|
|
139
158
|
giveup_log_level: int = logging.ERROR,
|
|
140
159
|
**wait_gen_kwargs: Any,
|
|
141
|
-
) -> Callable[[
|
|
160
|
+
) -> Callable[[Callable[P, T]], Callable[P, T]]:
|
|
142
161
|
"""Returns decorator for backoff and retry triggered by exception.
|
|
143
162
|
|
|
144
163
|
Args:
|
|
@@ -185,7 +204,7 @@ def on_exception(
|
|
|
185
204
|
This is useful for runtime configuration.
|
|
186
205
|
"""
|
|
187
206
|
|
|
188
|
-
def decorate(target):
|
|
207
|
+
def decorate(target: Callable[P, T]) -> Callable[P, T]:
|
|
189
208
|
nonlocal logger, on_success, on_backoff, on_giveup
|
|
190
209
|
|
|
191
210
|
logger = _prepare_logger(logger)
|
|
@@ -225,3 +244,155 @@ def on_exception(
|
|
|
225
244
|
|
|
226
245
|
# Return a function which decorates a target with a retry loop.
|
|
227
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
|
+
)
|