python-backoff 3.0.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,18 +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
15
  from backoff._common import Attempt
16
16
  from backoff._decorator import aretry_context, on_exception, on_predicate, retry_context
17
17
  from backoff._jitter import full_jitter, random_jitter
18
- from backoff._wait_gen import constant, decay, expo, fibo, runtime
18
+ from backoff._wait_gen import capped, constant, decay, expo, fibo, runtime
19
19
 
20
20
  __all__ = [
21
21
  "Attempt",
22
22
  "aretry_context",
23
+ "capped",
23
24
  "constant",
24
25
  "decay",
25
26
  "expo",
@@ -32,4 +33,4 @@ __all__ = [
32
33
  "runtime",
33
34
  ]
34
35
 
35
- __version__ = "3.0.0"
36
+ __version__ = "3.1.0"
backoff/_common.py CHANGED
@@ -43,7 +43,7 @@ _HandlerT = TypeVar("_HandlerT")
43
43
  def _maybe_call(f: _MaybeCallable[T] | None, *args: Any, **kwargs: Any) -> T | None:
44
44
  if callable(f):
45
45
  try:
46
- return f(*args, **kwargs) # ty:ignore[call-top-callable, invalid-return-type]
46
+ return f(*args, **kwargs) # ty:ignore[invalid-return-type]
47
47
  except TypeError:
48
48
  return f # type: ignore[return-value] # ty:ignore[invalid-return-type]
49
49
  else:
@@ -212,10 +212,9 @@ def _config_handlers(
212
212
  # Default backoff handler
213
213
  def _log_backoff(details: Details, logger: _AnyLogger, log_level: int) -> None:
214
214
  msg = "Backing off %s(...) for %.1fs (%s)"
215
- log_args = [details["target"].__name__, details["wait"]] # ty:ignore[unresolved-attribute]
215
+ log_args = [details["target"].__name__, details["wait"]] # ty:ignore[missing-attribute]
216
216
 
217
- exc = details.get("exception")
218
- if exc is not None:
217
+ if (exc := details.get("exception")) is not None:
219
218
  exc_fmt = traceback.format_exception_only(type(exc), exc)[-1]
220
219
  log_args.append(exc_fmt.rstrip("\n"))
221
220
  else:
@@ -226,10 +225,9 @@ def _log_backoff(details: Details, logger: _AnyLogger, log_level: int) -> None:
226
225
  # Default giveup handler
227
226
  def _log_giveup(details: Details, logger: _AnyLogger, log_level: int) -> None:
228
227
  msg = "Giving up %s(...) after %d tries (%s)"
229
- log_args = [details["target"].__name__, details["tries"]] # ty:ignore[unresolved-attribute]
228
+ log_args = [details["target"].__name__, details["tries"]] # ty:ignore[missing-attribute]
230
229
 
231
- exc = details.get("exception")
232
- if exc is not None:
230
+ if (exc := details.get("exception")) is not None:
233
231
  exc_fmt = traceback.format_exception_only(type(exc), exc)[-1]
234
232
  log_args.append(exc_fmt.rstrip("\n"))
235
233
  else:
backoff/_decorator.py CHANGED
@@ -307,9 +307,14 @@ def retry_context(
307
307
  Unlike `on_exception`, this doesn't wrap a whole function; it lets a
308
308
  caller retry an arbitrary block of code:
309
309
 
310
- for attempt in backoff.retry_context(ValueError, backoff.expo):
311
- with attempt:
312
- do_something()
310
+ ```python
311
+ for attempt in backoff.retry_context(
312
+ ValueError,
313
+ backoff.expo,
314
+ ):
315
+ with attempt:
316
+ do_something()
317
+ ```
313
318
 
314
319
  Each `attempt` is a context manager: exceptions matching `exception`
315
320
  are caught, and the loop either sleeps and retries or lets the
@@ -403,9 +408,14 @@ def aretry_context(
403
408
  ) -> AsyncGenerator[Attempt, None]:
404
409
  """Async counterpart to `retry_context`, for use with `async for`.
405
410
 
406
- async for attempt in backoff.aretry_context(ValueError, backoff.expo):
407
- with attempt:
408
- 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
+ ```
409
419
 
410
420
  `giveup` and the handlers may be sync or async callables. See
411
421
  `retry_context` for the full argument reference.
backoff/_wait_gen.py CHANGED
@@ -2,11 +2,13 @@ from __future__ import annotations
2
2
 
3
3
  import itertools
4
4
  import math
5
- from typing import TYPE_CHECKING, Any
5
+ from typing import TYPE_CHECKING, Any, ParamSpec
6
6
 
7
7
  if TYPE_CHECKING:
8
8
  from collections.abc import Callable, Generator, Iterable
9
9
 
10
+ P = ParamSpec("P")
11
+
10
12
 
11
13
  def expo(
12
14
  base: float = 2,
@@ -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,6 +99,57 @@ 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
@@ -106,23 +157,25 @@ def runtime(*, value: Callable[[Any], float]) -> Generator[float, Any, None]:
106
157
  Useful for honoring a server-specified retry delay, e.g. an HTTP
107
158
  `Retry-After` header, rather than a fixed wait sequence:
108
159
 
109
- # with on_predicate, `value` receives the return value
110
- @backoff.on_predicate(
111
- backoff.runtime,
112
- predicate=lambda r: r.status_code == 429,
113
- value=lambda r: int(r.headers.get("Retry-After", 1)),
114
- )
115
- def get_page():
116
- return requests.get(url)
117
-
118
- # with on_exception, `value` receives the raised exception
119
- @backoff.on_exception(
120
- backoff.runtime,
121
- RetryableError,
122
- value=lambda e: e.wait_seconds,
123
- )
124
- def get_page():
125
- ...
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
+ ```
126
179
 
127
180
  Args:
128
181
  value: a callable which takes as input the decorated
@@ -1,7 +1,8 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: python-backoff
3
- Version: 3.0.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>
@@ -21,6 +22,7 @@ Classifier: Programming Language :: Python :: 3.12
21
22
  Classifier: Programming Language :: Python :: 3.13
22
23
  Classifier: Programming Language :: Python :: 3.14
23
24
  Classifier: Programming Language :: Python :: 3.15
25
+ Classifier: Programming Language :: Python :: 3.16
24
26
  Classifier: Topic :: Internet :: WWW/HTTP
25
27
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
26
28
  Classifier: Topic :: Utilities
@@ -39,9 +41,11 @@ backoff
39
41
  .. image:: https://img.shields.io/github/license/python-backoff/backoff
40
42
  :target: https://github.com/python-backoff/backoff/blob/main/LICENSE
41
43
 
44
+ *Note: this is an actively maintained fork of https://github.com/litl/backoff.*
45
+
42
46
  **Function decoration for backoff and retry**
43
47
 
44
- 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
45
49
  function such that it will be retried until some condition is met. It
46
50
  is meant to be of use when accessing unreliable resources with the
47
51
  potential for intermittent failures i.e. network resources and external
@@ -52,6 +56,8 @@ Decorators support both regular functions for synchronous code and
52
56
  `asyncio <https://docs.python.org/3/library/asyncio.html>`__'s coroutines
53
57
  for asynchronous code.
54
58
 
59
+ Context managers are also available to enable retries of arbitrary code blocks.
60
+
55
61
  Examples
56
62
  ========
57
63
 
@@ -182,7 +188,7 @@ The ``on_predicate`` decorator is used to retry when a particular
182
188
  condition is true of the return value of the target function. This may
183
189
  be useful when polling a resource for externally generated content.
184
190
 
185
- 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
186
192
  return value of the target function is the empty list:
187
193
 
188
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.32.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=_AGhwnTFSCdB_oeBA1-X5HPaivH4At8qucG8WQ8kZjI,1039
2
- backoff/_async.py,sha256=UMAls92zTDOc_W8nmaTIWAEznQgE-bnSCjfoYerVG9Q,9235
3
- backoff/_common.py,sha256=0ONXWZBiAMSAXw4KN_Cp7mLvhlUPOGV826NHYn6E760,8342
4
- backoff/_decorator.py,sha256=loMJ2ep7O0KOeXL8vvyMFU9VGK2fKlSsUaZoVCT5Tfs,18110
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=J7-UEqBjxdxvj7LKb5D1ILfPzMrUmCIVX0YsvS107UQ,3752
9
- backoff/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
- backoff/types.py,sha256=obBefroeCaGlPbOQSw1KIKvYl4SKFr8_YvwogvorVnw,97
11
- python_backoff-3.0.0.dist-info/METADATA,sha256=n9RmkgXbEsR9yRYThMaw0tk7KFaPG6eNIKO_TX5WRBI,14760
12
- python_backoff-3.0.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
13
- python_backoff-3.0.0.dist-info/licenses/LICENSE,sha256=KmtNX4hNTXob8E6n3xlEzxKzLjWnmobQoHWi0_QPuaw,1077
14
- python_backoff-3.0.0.dist-info/RECORD,,