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 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 the README at
12
- https://github.com/python-backoff/backoff
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__ = "2.4.0"
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 typing import TYPE_CHECKING, Any, Callable, TypeVar
6
+ from collections.abc import Callable
7
+ from typing import TYPE_CHECKING, Any, TypeVar
7
8
 
8
- from backoff._common import _Attempt, _RetryState
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.record_elapsed(),
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 # type: ignore[return-value] # ty:ignore[invalid-return-type]
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) # type: ignore[misc] # ty:ignore[invalid-await]
227
+ ret = await target(*args, **kwargs)
225
228
 
226
229
  return ret
227
230
 
228
- return retry # type: ignore[return-value] # ty:ignore[invalid-return-type]
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[_Attempt, None]:
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 = _Attempt(exception)
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 warnings
8
- from typing import TYPE_CHECKING, Any, Callable, TypeVar
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[call-top-callable, invalid-return-type]
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
- try:
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
- class _Attempt:
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
- __slots__ = (
145
- "exception",
146
- "exception_types",
147
- )
138
+ exception_types: ExceptionTypes
139
+ exception: BaseException | None = field(init=False)
148
140
 
149
- def __init__(
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: logging.Logger | logging.LoggerAdapter | None = None,
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[unresolved-attribute]
215
+ log_args = [details["target"].__name__, details["wait"]] # ty:ignore[missing-attribute]
235
216
 
236
- exc = details.get("exception")
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[unresolved-attribute]
228
+ log_args = [details["target"].__name__, details["tries"]] # ty:ignore[missing-attribute]
253
229
 
254
- exc = details.get("exception")
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: logging.Logger | logging.LoggerAdapter,
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, Callable, TypeVar
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 sys
22
- from collections.abc import AsyncGenerator, Generator, Iterable
21
+ from collections.abc import AsyncGenerator, Callable, Generator, Iterable
23
22
 
24
- from backoff._common import _Attempt
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: _MaybeLogger = "backoff",
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[[Callable[P, T]], Callable[P, T]]:
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: Callable[P, T]) -> Callable[P, T]:
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
- retry = _async.retry_predicate
124
- else:
125
- retry = _sync.retry_predicate
126
-
127
- return retry(
128
- target,
129
- wait_gen,
130
- predicate,
131
- max_tries=max_tries,
132
- max_time=max_time,
133
- jitter=jitter,
134
- on_success=on_success,
135
- on_backoff=on_backoff,
136
- on_giveup=on_giveup,
137
- wait_gen_kwargs=wait_gen_kwargs,
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: _MaybeLogger = "backoff",
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[[Callable[P, T]], Callable[P, T]]:
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: Callable[P, T]) -> Callable[P, T]:
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
- retry = _async.retry_exception
227
- else:
228
- retry = _sync.retry_exception
229
-
230
- return retry(
231
- target,
232
- wait_gen,
233
- exception,
234
- max_tries=max_tries,
235
- max_time=max_time,
236
- jitter=jitter,
237
- giveup=giveup,
238
- on_success=on_success,
239
- on_backoff=on_backoff,
240
- on_giveup=on_giveup,
241
- raise_on_giveup=raise_on_giveup,
242
- wait_gen_kwargs=wait_gen_kwargs,
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: _MaybeLogger = "backoff",
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[_Attempt, None, None]:
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
- for attempt in backoff.retry_context(ValueError, backoff.expo):
272
- with attempt:
273
- do_something()
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: _MaybeLogger = "backoff",
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[_Attempt, None]:
408
+ ) -> AsyncGenerator[Attempt, None]:
362
409
  """Async counterpart to `retry_context`, for use with `async for`.
363
410
 
364
- async for attempt in backoff.aretry_context(ValueError, backoff.expo):
365
- with attempt:
366
- await do_something()
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, Callable, TypeVar
5
+ from typing import TYPE_CHECKING, Any, TypeVar
6
6
 
7
- from backoff._common import _Attempt, _dispatch_handlers, _RetryState
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.record_elapsed(),
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[_Attempt, None, None]:
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 = _Attempt(exception)
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
- _Handler = Union[
53
- Callable[[Details], None],
54
- Callable[[Details], Coroutine[Any, Any, None]],
55
- ]
56
- _ContextHandler = Union[
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 = Union[T, Callable[[], T]]
62
- _MaybeLogger = Union[str, logging.Logger, logging.LoggerAdapter, None]
63
- _MaybeTuple = Union[T, tuple[T, ...]]
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, Callable
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[1]:
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 fibonaccial decay.
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 fibonacci sequence exceeds this, the value
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,5 +1,6 @@
1
- from ._typing import Details
1
+ from ._typing import ContextDetails, Details
2
2
 
3
3
  __all__ = [
4
+ "ContextDetails",
4
5
  "Details",
5
6
  ]
@@ -1,7 +1,8 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: python-backoff
3
- Version: 2.4.0
4
- Summary: Function decoration for backoff and retry
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.9
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 module provides function decorators which can be used to wrap a
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 fibonacci sequence backoff when the
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,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.31.0
2
+ Generator: hatchling 1.33.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -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,,