python-backoff 2.3.1__py3-none-any.whl → 3.0.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 +11 -7
- backoff/_async.py +219 -110
- backoff/_common.py +183 -36
- backoff/_decorator.py +279 -64
- backoff/_sync.py +193 -97
- backoff/_typing.py +43 -28
- backoff/_wait_gen.py +58 -39
- backoff/types.py +2 -1
- {python_backoff-2.3.1.dist-info → python_backoff-3.0.0.dist-info}/METADATA +4 -7
- python_backoff-3.0.0.dist-info/RECORD +14 -0
- {python_backoff-2.3.1.dist-info → python_backoff-3.0.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-3.0.0.dist-info}/licenses/LICENSE +0 -0
backoff/_common.py
CHANGED
|
@@ -1,8 +1,33 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import functools
|
|
2
4
|
import logging
|
|
3
|
-
import
|
|
5
|
+
import time
|
|
4
6
|
import traceback
|
|
5
|
-
import
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from typing import TYPE_CHECKING, Any, TypeAlias, TypeVar
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
import sys
|
|
12
|
+
from collections.abc import Callable, Generator, Iterable
|
|
13
|
+
|
|
14
|
+
from backoff._typing import (
|
|
15
|
+
ContextDetails,
|
|
16
|
+
Details,
|
|
17
|
+
_AnyLogger,
|
|
18
|
+
_AnyLoggerOrName,
|
|
19
|
+
_ContextHandler,
|
|
20
|
+
_Jitterer,
|
|
21
|
+
_MaybeCallable,
|
|
22
|
+
_WaitGenerator,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
if sys.version_info >= (3, 11):
|
|
26
|
+
from typing import Self
|
|
27
|
+
else:
|
|
28
|
+
from typing_extensions import Self
|
|
29
|
+
|
|
30
|
+
ExceptionTypes: TypeAlias = type[Exception] | tuple[type[Exception], ...]
|
|
6
31
|
|
|
7
32
|
|
|
8
33
|
# Use module-specific logger with a default null handler.
|
|
@@ -10,39 +35,40 @@ _logger = logging.getLogger("backoff")
|
|
|
10
35
|
_logger.addHandler(logging.NullHandler()) # pragma: no cover
|
|
11
36
|
_logger.setLevel(logging.INFO)
|
|
12
37
|
|
|
38
|
+
T = TypeVar("T")
|
|
39
|
+
_HandlerT = TypeVar("_HandlerT")
|
|
40
|
+
|
|
13
41
|
|
|
14
42
|
# Evaluate arg that can be either a fixed value or a callable.
|
|
15
|
-
def _maybe_call(f, *args, **kwargs):
|
|
43
|
+
def _maybe_call(f: _MaybeCallable[T] | None, *args: Any, **kwargs: Any) -> T | None:
|
|
16
44
|
if callable(f):
|
|
17
45
|
try:
|
|
18
|
-
return f(*args, **kwargs)
|
|
46
|
+
return f(*args, **kwargs) # ty:ignore[call-top-callable, invalid-return-type]
|
|
19
47
|
except TypeError:
|
|
20
|
-
return f
|
|
48
|
+
return f # type: ignore[return-value] # ty:ignore[invalid-return-type]
|
|
21
49
|
else:
|
|
22
50
|
return f
|
|
23
51
|
|
|
24
52
|
|
|
25
|
-
def _init_wait_gen(
|
|
53
|
+
def _init_wait_gen(
|
|
54
|
+
wait_gen: _WaitGenerator,
|
|
55
|
+
wait_gen_kwargs: dict[str, Any],
|
|
56
|
+
) -> Generator[float, Any, None]:
|
|
26
57
|
kwargs = {k: _maybe_call(v) for k, v in wait_gen_kwargs.items()}
|
|
27
58
|
initialized = wait_gen(**kwargs)
|
|
28
59
|
initialized.send(None) # Initialize with an empty send
|
|
29
60
|
return initialized
|
|
30
61
|
|
|
31
62
|
|
|
32
|
-
def _next_wait(
|
|
63
|
+
def _next_wait(
|
|
64
|
+
wait: Generator[float, None, None],
|
|
65
|
+
send_value: Any,
|
|
66
|
+
jitter: _Jitterer | None,
|
|
67
|
+
elapsed: float,
|
|
68
|
+
max_time: float | None,
|
|
69
|
+
) -> float:
|
|
33
70
|
value = wait.send(send_value)
|
|
34
|
-
|
|
35
|
-
seconds = jitter(value) if jitter is not None else value
|
|
36
|
-
except TypeError:
|
|
37
|
-
warnings.warn(
|
|
38
|
-
"Nullary jitter function signature is deprecated. Use "
|
|
39
|
-
"unary signature accepting a wait value in seconds and "
|
|
40
|
-
"returning a jittered version of it.",
|
|
41
|
-
DeprecationWarning,
|
|
42
|
-
stacklevel=2,
|
|
43
|
-
)
|
|
44
|
-
|
|
45
|
-
seconds = value + jitter()
|
|
71
|
+
seconds = jitter(value) if jitter is not None else value
|
|
46
72
|
|
|
47
73
|
# don't sleep longer than remaining allotted max_time
|
|
48
74
|
if max_time is not None:
|
|
@@ -51,7 +77,95 @@ def _next_wait(wait, send_value, jitter, elapsed, max_time):
|
|
|
51
77
|
return seconds
|
|
52
78
|
|
|
53
79
|
|
|
54
|
-
|
|
80
|
+
class _RetryState:
|
|
81
|
+
"""Bookkeeping shared by the sync and async retry loops.
|
|
82
|
+
|
|
83
|
+
Tracks try count and elapsed time, and owns the initialized wait
|
|
84
|
+
generator, so `retry_predicate`/`retry_exception` only need to drive
|
|
85
|
+
the call/check/sleep sequence around it.
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
__slots__ = (
|
|
89
|
+
"elapsed",
|
|
90
|
+
"max_time",
|
|
91
|
+
"max_tries",
|
|
92
|
+
"start",
|
|
93
|
+
"tries",
|
|
94
|
+
"wait",
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
def __init__(
|
|
98
|
+
self,
|
|
99
|
+
wait_gen: _WaitGenerator,
|
|
100
|
+
wait_gen_kwargs: dict[str, Any],
|
|
101
|
+
*,
|
|
102
|
+
max_tries: _MaybeCallable[int] | None = None,
|
|
103
|
+
max_time: _MaybeCallable[float] | None = None,
|
|
104
|
+
) -> None:
|
|
105
|
+
self.tries = 0
|
|
106
|
+
self.elapsed: float = 0
|
|
107
|
+
self.start = time.monotonic()
|
|
108
|
+
self.max_tries = _maybe_call(max_tries)
|
|
109
|
+
self.max_time = _maybe_call(max_time)
|
|
110
|
+
self.wait = _init_wait_gen(wait_gen, wait_gen_kwargs)
|
|
111
|
+
|
|
112
|
+
def start_attempt(self) -> None:
|
|
113
|
+
self.tries += 1
|
|
114
|
+
|
|
115
|
+
def record_elapsed(self) -> float:
|
|
116
|
+
self.elapsed = time.monotonic() - self.start
|
|
117
|
+
return self.elapsed
|
|
118
|
+
|
|
119
|
+
def exhausted(self) -> bool:
|
|
120
|
+
max_tries_exceeded = self.tries == self.max_tries
|
|
121
|
+
max_time_exceeded = self.max_time is not None and self.elapsed >= self.max_time
|
|
122
|
+
return max_tries_exceeded or max_time_exceeded
|
|
123
|
+
|
|
124
|
+
def next_wait(self, send_value: Any, jitter: _Jitterer | None) -> float:
|
|
125
|
+
return _next_wait(self.wait, send_value, jitter, self.elapsed, self.max_time)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@dataclass(slots=True)
|
|
129
|
+
class Attempt:
|
|
130
|
+
"""A single attempt yielded by `retry_context`/`aretry_context`.
|
|
131
|
+
|
|
132
|
+
Used as `with attempt: ...`. Exceptions matching `exception_types` are
|
|
133
|
+
caught here so the driving generator (not this object) decides whether
|
|
134
|
+
to retry, sleep, or let the exception propagate; anything else, or no
|
|
135
|
+
exception at all, is left for the `with` block's normal exit behavior.
|
|
136
|
+
"""
|
|
137
|
+
|
|
138
|
+
exception_types: ExceptionTypes
|
|
139
|
+
exception: BaseException | None = field(init=False)
|
|
140
|
+
|
|
141
|
+
def __post_init__(self) -> None:
|
|
142
|
+
self.exception: BaseException | None = None
|
|
143
|
+
|
|
144
|
+
def __enter__(self) -> Self:
|
|
145
|
+
return self
|
|
146
|
+
|
|
147
|
+
def __exit__(
|
|
148
|
+
self,
|
|
149
|
+
exc_type: type[BaseException] | None,
|
|
150
|
+
exc: BaseException | None,
|
|
151
|
+
tb: object,
|
|
152
|
+
) -> bool:
|
|
153
|
+
if exc_type is None:
|
|
154
|
+
return False
|
|
155
|
+
|
|
156
|
+
if not issubclass(exc_type, self.exception_types):
|
|
157
|
+
return False # not ours; propagate immediately
|
|
158
|
+
|
|
159
|
+
self.exception = exc
|
|
160
|
+
return True # suppress for now; the driving generator decides next
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _dispatch_handlers(handlers: Iterable[_ContextHandler], **details: Any) -> None:
|
|
164
|
+
for hdlr in handlers:
|
|
165
|
+
hdlr(details) # type: ignore[arg-type] # ty:ignore[invalid-argument-type]
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _prepare_logger(logger: _AnyLoggerOrName | None) -> _AnyLogger | None:
|
|
55
169
|
if isinstance(logger, str):
|
|
56
170
|
logger = logging.getLogger(logger)
|
|
57
171
|
return logger
|
|
@@ -60,22 +174,23 @@ def _prepare_logger(logger):
|
|
|
60
174
|
# Configure handler list with user specified handler and optionally
|
|
61
175
|
# with a default handler bound to the specified logger.
|
|
62
176
|
def _config_handlers(
|
|
63
|
-
user_handlers,
|
|
177
|
+
user_handlers: _HandlerT | Iterable[_HandlerT] | None,
|
|
64
178
|
*,
|
|
65
|
-
default_handler=None,
|
|
66
|
-
logger=None,
|
|
67
|
-
log_level=None,
|
|
68
|
-
):
|
|
69
|
-
handlers = []
|
|
179
|
+
default_handler: Callable[..., None] | None = None,
|
|
180
|
+
logger: _AnyLogger | None = None,
|
|
181
|
+
log_level: int | None = None,
|
|
182
|
+
) -> list[_HandlerT]:
|
|
183
|
+
handlers: list[_HandlerT] = []
|
|
70
184
|
if logger is not None:
|
|
71
185
|
assert log_level is not None, "Log level is not specified"
|
|
186
|
+
assert default_handler is not None, "Default handler is not specified"
|
|
72
187
|
# bind the specified logger to the default log handler
|
|
73
188
|
log_handler = functools.partial(
|
|
74
189
|
default_handler,
|
|
75
190
|
logger=logger,
|
|
76
191
|
log_level=log_level,
|
|
77
192
|
)
|
|
78
|
-
handlers.append(log_handler)
|
|
193
|
+
handlers.append(log_handler) # type: ignore[arg-type] # ty:ignore[invalid-argument-type]
|
|
79
194
|
|
|
80
195
|
if user_handlers is None:
|
|
81
196
|
return handlers
|
|
@@ -84,22 +199,24 @@ def _config_handlers(
|
|
|
84
199
|
# or a single handler. either way append them to the list.
|
|
85
200
|
if hasattr(user_handlers, "__iter__"):
|
|
86
201
|
# add all handlers in the iterable
|
|
87
|
-
|
|
202
|
+
# pyrefly: ignore [bad-argument-type]
|
|
203
|
+
handlers += list(user_handlers) # ty:ignore[invalid-argument-type]
|
|
88
204
|
else:
|
|
89
205
|
# append a single handler
|
|
206
|
+
# pyrefly: ignore [bad-argument-type]
|
|
90
207
|
handlers.append(user_handlers)
|
|
91
208
|
|
|
92
209
|
return handlers
|
|
93
210
|
|
|
94
211
|
|
|
95
212
|
# Default backoff handler
|
|
96
|
-
def _log_backoff(details, logger, log_level):
|
|
213
|
+
def _log_backoff(details: Details, logger: _AnyLogger, log_level: int) -> None:
|
|
97
214
|
msg = "Backing off %s(...) for %.1fs (%s)"
|
|
98
|
-
log_args = [details["target"].__name__, details["wait"]]
|
|
215
|
+
log_args = [details["target"].__name__, details["wait"]] # ty:ignore[unresolved-attribute]
|
|
99
216
|
|
|
100
|
-
|
|
217
|
+
exc = details.get("exception")
|
|
101
218
|
if exc is not None:
|
|
102
|
-
exc_fmt = traceback.format_exception_only(
|
|
219
|
+
exc_fmt = traceback.format_exception_only(type(exc), exc)[-1]
|
|
103
220
|
log_args.append(exc_fmt.rstrip("\n"))
|
|
104
221
|
else:
|
|
105
222
|
log_args.append(details["value"])
|
|
@@ -107,15 +224,45 @@ def _log_backoff(details, logger, log_level):
|
|
|
107
224
|
|
|
108
225
|
|
|
109
226
|
# Default giveup handler
|
|
110
|
-
def _log_giveup(details, logger, log_level):
|
|
227
|
+
def _log_giveup(details: Details, logger: _AnyLogger, log_level: int) -> None:
|
|
111
228
|
msg = "Giving up %s(...) after %d tries (%s)"
|
|
112
|
-
log_args = [details["target"].__name__, details["tries"]]
|
|
229
|
+
log_args = [details["target"].__name__, details["tries"]] # ty:ignore[unresolved-attribute]
|
|
113
230
|
|
|
114
|
-
|
|
231
|
+
exc = details.get("exception")
|
|
115
232
|
if exc is not None:
|
|
116
|
-
exc_fmt = traceback.format_exception_only(
|
|
233
|
+
exc_fmt = traceback.format_exception_only(type(exc), exc)[-1]
|
|
117
234
|
log_args.append(exc_fmt.rstrip("\n"))
|
|
118
235
|
else:
|
|
119
236
|
log_args.append(details["value"])
|
|
120
237
|
|
|
121
238
|
logger.log(log_level, msg, *log_args)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
# Default backoff handler for retry_context/aretry_context (no wrapped
|
|
242
|
+
# callable, so no name to log; the exception is read from `details`
|
|
243
|
+
# directly since it's no longer the active exception by this point).
|
|
244
|
+
def _log_backoff_context(
|
|
245
|
+
details: ContextDetails,
|
|
246
|
+
logger: _AnyLogger,
|
|
247
|
+
log_level: int,
|
|
248
|
+
) -> None:
|
|
249
|
+
logger.log(
|
|
250
|
+
log_level,
|
|
251
|
+
"Backing off retry_context(...) for %.1fs (%s)",
|
|
252
|
+
details["wait"],
|
|
253
|
+
details.get("exception"),
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
# Default giveup handler for retry_context/aretry_context.
|
|
258
|
+
def _log_giveup_context(
|
|
259
|
+
details: ContextDetails,
|
|
260
|
+
logger: logging.Logger | logging.LoggerAdapter[Any],
|
|
261
|
+
log_level: int,
|
|
262
|
+
) -> None:
|
|
263
|
+
logger.log(
|
|
264
|
+
log_level,
|
|
265
|
+
"Giving up retry_context(...) after %d tries (%s)",
|
|
266
|
+
details["tries"],
|
|
267
|
+
details.get("exception"),
|
|
268
|
+
)
|