python-backoff 3.0.0__tar.gz → 3.1.0__tar.gz

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.
Files changed (47) hide show
  1. {python_backoff-3.0.0 → python_backoff-3.1.0}/PKG-INFO +10 -4
  2. {python_backoff-3.0.0 → python_backoff-3.1.0}/README.rst +6 -2
  3. {python_backoff-3.0.0 → python_backoff-3.1.0}/backoff/__init__.py +6 -5
  4. {python_backoff-3.0.0 → python_backoff-3.1.0}/backoff/_common.py +5 -7
  5. {python_backoff-3.0.0 → python_backoff-3.1.0}/backoff/_decorator.py +16 -6
  6. {python_backoff-3.0.0 → python_backoff-3.1.0}/backoff/_wait_gen.py +77 -24
  7. python_backoff-3.1.0/docs/advanced/combining-decorators.md +123 -0
  8. python_backoff-3.1.0/docs/advanced/custom-strategies.md +144 -0
  9. python_backoff-3.1.0/docs/advanced/runtime-config.md +102 -0
  10. python_backoff-3.1.0/docs/api/backoff/index.md +1 -0
  11. python_backoff-3.1.0/docs/api/summary.md +1 -0
  12. python_backoff-3.1.0/docs/changelog.md +1 -0
  13. python_backoff-3.1.0/docs/examples.md +492 -0
  14. python_backoff-3.1.0/docs/faq.md +454 -0
  15. python_backoff-3.1.0/docs/getting-started.md +206 -0
  16. python_backoff-3.1.0/docs/index.md +73 -0
  17. python_backoff-3.1.0/docs/user-guide/async.md +194 -0
  18. python_backoff-3.1.0/docs/user-guide/configuration.md +397 -0
  19. python_backoff-3.1.0/docs/user-guide/context-manager.md +63 -0
  20. python_backoff-3.1.0/docs/user-guide/decorators.md +248 -0
  21. python_backoff-3.1.0/docs/user-guide/event-handlers.md +435 -0
  22. python_backoff-3.1.0/docs/user-guide/logging.md +234 -0
  23. python_backoff-3.1.0/docs/user-guide/wait-strategies.md +333 -0
  24. {python_backoff-3.0.0 → python_backoff-3.1.0}/pyproject.toml +10 -7
  25. {python_backoff-3.0.0 → python_backoff-3.1.0}/tests/common.py +1 -1
  26. {python_backoff-3.0.0 → python_backoff-3.1.0}/tests/test_backoff.py +51 -32
  27. {python_backoff-3.0.0 → python_backoff-3.1.0}/tests/test_backoff_async.py +76 -55
  28. {python_backoff-3.0.0 → python_backoff-3.1.0}/tests/test_integration.py +12 -9
  29. {python_backoff-3.0.0 → python_backoff-3.1.0}/tests/test_retry_context.py +8 -8
  30. {python_backoff-3.0.0 → python_backoff-3.1.0}/tests/test_wait_gen.py +39 -0
  31. {python_backoff-3.0.0 → python_backoff-3.1.0}/.gitignore +0 -0
  32. {python_backoff-3.0.0 → python_backoff-3.1.0}/LICENSE +0 -0
  33. {python_backoff-3.0.0 → python_backoff-3.1.0}/backoff/_async.py +0 -0
  34. {python_backoff-3.0.0 → python_backoff-3.1.0}/backoff/_jitter.py +0 -0
  35. {python_backoff-3.0.0 → python_backoff-3.1.0}/backoff/_sync.py +0 -0
  36. {python_backoff-3.0.0 → python_backoff-3.1.0}/backoff/_typing.py +0 -0
  37. {python_backoff-3.0.0 → python_backoff-3.1.0}/backoff/py.typed +0 -0
  38. {python_backoff-3.0.0 → python_backoff-3.1.0}/backoff/types.py +0 -0
  39. {python_backoff-3.0.0 → python_backoff-3.1.0}/tests/__init__.py +0 -0
  40. {python_backoff-3.0.0 → python_backoff-3.1.0}/tests/conftest.py +0 -0
  41. {python_backoff-3.0.0 → python_backoff-3.1.0}/tests/test_jitter.py +0 -0
  42. {python_backoff-3.0.0 → python_backoff-3.1.0}/tests/test_package.py +0 -0
  43. {python_backoff-3.0.0 → python_backoff-3.1.0}/tests/test_types.py +0 -0
  44. {python_backoff-3.0.0 → python_backoff-3.1.0}/tests/typing/__init__.py +0 -0
  45. {python_backoff-3.0.0 → python_backoff-3.1.0}/tests/typing/basic.py +0 -0
  46. {python_backoff-3.0.0 → python_backoff-3.1.0}/tests/typing/decorators.py +0 -0
  47. {python_backoff-3.0.0 → python_backoff-3.1.0}/tests/typing/pyrefly.py +0 -0
@@ -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
@@ -10,9 +10,11 @@ backoff
10
10
  .. image:: https://img.shields.io/github/license/python-backoff/backoff
11
11
  :target: https://github.com/python-backoff/backoff/blob/main/LICENSE
12
12
 
13
+ *Note: this is an actively maintained fork of https://github.com/litl/backoff.*
14
+
13
15
  **Function decoration for backoff and retry**
14
16
 
15
- This module provides function decorators which can be used to wrap a
17
+ This library provides function decorators which can be used to wrap a
16
18
  function such that it will be retried until some condition is met. It
17
19
  is meant to be of use when accessing unreliable resources with the
18
20
  potential for intermittent failures i.e. network resources and external
@@ -23,6 +25,8 @@ Decorators support both regular functions for synchronous code and
23
25
  `asyncio <https://docs.python.org/3/library/asyncio.html>`__'s coroutines
24
26
  for asynchronous code.
25
27
 
28
+ Context managers are also available to enable retries of arbitrary code blocks.
29
+
26
30
  Examples
27
31
  ========
28
32
 
@@ -153,7 +157,7 @@ The ``on_predicate`` decorator is used to retry when a particular
153
157
  condition is true of the return value of the target function. This may
154
158
  be useful when polling a resource for externally generated content.
155
159
 
156
- Here's an example which uses a fibonacci sequence backoff when the
160
+ Here's an example which uses a Fibonacci sequence backoff when the
157
161
  return value of the target function is the empty list:
158
162
 
159
163
  .. code-block:: python
@@ -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"
@@ -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:
@@ -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.
@@ -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
@@ -0,0 +1,123 @@
1
+ # Combining Decorators
2
+
3
+ Stack multiple backoff decorators for complex retry logic.
4
+
5
+ ## Basics
6
+
7
+ Decorators are applied from bottom to top (inside out):
8
+
9
+ ```python
10
+ @backoff.on_predicate(backoff.fibo, lambda x: x is None) # Applied last
11
+ @backoff.on_exception(backoff.expo, HTTPError) # Applied second
12
+ @backoff.on_exception(backoff.expo, Timeout) # Applied first
13
+ def complex_operation():
14
+ pass
15
+ ```
16
+
17
+ ## Common Patterns
18
+
19
+ ### Different Exceptions, Different Strategies
20
+
21
+ ```python
22
+ @backoff.on_exception(
23
+ backoff.expo,
24
+ requests.exceptions.Timeout,
25
+ max_time=300, # Generous timeout for network issues
26
+ )
27
+ @backoff.on_exception(
28
+ backoff.expo,
29
+ requests.exceptions.HTTPError,
30
+ max_time=60, # Shorter timeout for HTTP errors
31
+ giveup=lambda e: 400 <= e.response.status_code < 500,
32
+ )
33
+ def api_call(url):
34
+ response = requests.get(url)
35
+ response.raise_for_status()
36
+ return response.json()
37
+ ```
38
+
39
+ ### Exception + Predicate
40
+
41
+ ```python
42
+ @backoff.on_predicate(
43
+ backoff.constant,
44
+ lambda result: result.get("status") == "pending",
45
+ interval=5,
46
+ max_time=600,
47
+ )
48
+ @backoff.on_exception(
49
+ backoff.expo,
50
+ requests.exceptions.RequestException,
51
+ max_time=60,
52
+ )
53
+ def poll_until_ready(job_id):
54
+ response = requests.get(f"/api/jobs/{job_id}")
55
+ response.raise_for_status()
56
+ return response.json()
57
+ ```
58
+
59
+ ## Execution Order
60
+
61
+ Inner decorators execute first:
62
+
63
+ ```python
64
+ calls = []
65
+
66
+
67
+ def track_call(func_name):
68
+ def handler(details):
69
+ calls.append(func_name)
70
+
71
+ return handler
72
+
73
+
74
+ @backoff.on_exception(
75
+ backoff.constant,
76
+ ValueError,
77
+ on_backoff=track_call("outer"),
78
+ max_tries=2,
79
+ interval=0.01,
80
+ )
81
+ @backoff.on_exception(
82
+ backoff.constant,
83
+ TypeError,
84
+ on_backoff=track_call("inner"),
85
+ max_tries=2,
86
+ interval=0.01,
87
+ )
88
+ def failing_function(error_type):
89
+ raise error_type("Test")
90
+ ```
91
+
92
+ - If `TypeError` raised: inner decorator retries
93
+ - If `ValueError` raised: outer decorator retries
94
+ - Both errors: inner handles TypeError, then outer handles ValueError
95
+
96
+ ## Best Practices
97
+
98
+ ### Specific Before General
99
+
100
+ ```python
101
+ @backoff.on_exception(backoff.expo, Exception) # Catch-all
102
+ @backoff.on_exception(backoff.fibo, ConnectionError) # Specific
103
+ def network_operation():
104
+ pass
105
+ ```
106
+
107
+ ### Short Timeouts Inside, Long Outside
108
+
109
+ ```python
110
+ @backoff.on_exception(
111
+ backoff.expo,
112
+ Exception,
113
+ max_time=600, # Overall 10-minute limit
114
+ )
115
+ @backoff.on_exception(
116
+ backoff.constant,
117
+ Timeout,
118
+ interval=1,
119
+ max_tries=3, # Quick retries for timeouts
120
+ )
121
+ def layered_retry():
122
+ pass
123
+ ```
@@ -0,0 +1,144 @@
1
+ # Custom Wait Strategies
2
+
3
+ Create custom wait generators for specialized retry patterns.
4
+
5
+ ## Wait Generator Interface
6
+
7
+ A wait generator is a function that yields wait times in seconds:
8
+
9
+ ```python
10
+ def my_wait_gen():
11
+ """Yields: 1, 2, 3, 4, 5, 5, 5, ..."""
12
+ # Note: Using `yield from` can be unsafe in backoff wait generators.
13
+ for i in range(1, 6):
14
+ yield i
15
+ while True:
16
+ yield 5
17
+
18
+
19
+ @backoff.on_exception(my_wait_gen, Exception)
20
+ def my_function():
21
+ pass
22
+ ```
23
+
24
+ ## Parameters
25
+
26
+ Accept parameters to customize behavior:
27
+
28
+ ```python
29
+ def linear_backoff(start=1, increment=1, max_value=None):
30
+ """Linear backoff: start, start+increment, start+2*increment, ..."""
31
+ value = start
32
+ while True:
33
+ if max_value and value > max_value:
34
+ yield max_value
35
+ else:
36
+ yield value
37
+ value += increment
38
+
39
+
40
+ @backoff.on_exception(
41
+ linear_backoff,
42
+ Exception,
43
+ start=2,
44
+ increment=3,
45
+ max_value=30,
46
+ )
47
+ def my_function():
48
+ pass
49
+ ```
50
+
51
+ ## Examples
52
+
53
+ ### Polynomial Backoff
54
+
55
+ ```python
56
+ def polynomial_backoff(base=2, exponent=2, max_value=None):
57
+ """Polynomial: base^(tries^exponent)"""
58
+ n = 1
59
+ while True:
60
+ value = base ** (n**exponent)
61
+ if max_value and value > max_value:
62
+ yield max_value
63
+ else:
64
+ yield value
65
+ n += 1
66
+
67
+
68
+ @backoff.on_exception(
69
+ polynomial_backoff,
70
+ Exception,
71
+ base=2,
72
+ exponent=1.5,
73
+ )
74
+ def my_function():
75
+ pass
76
+ ```
77
+
78
+ ### Stepped Backoff
79
+
80
+ ```python
81
+ def stepped_backoff(steps):
82
+ """Different wait times for different ranges
83
+ steps = [(3, 1), (5, 5), (None, 10)] # 3 tries at 1s, next 5 at 5s, rest at 10s
84
+ """
85
+ for max_tries, wait_time in steps:
86
+ if max_tries is None:
87
+ while True:
88
+ yield wait_time
89
+ else:
90
+ for _ in range(max_tries):
91
+ yield wait_time
92
+
93
+
94
+ @backoff.on_exception(
95
+ stepped_backoff,
96
+ Exception,
97
+ steps=[(3, 1), (3, 5), (None, 30)],
98
+ )
99
+ def my_function():
100
+ pass
101
+ ```
102
+
103
+ ### Random Backoff
104
+
105
+ ```python
106
+ import random
107
+
108
+
109
+ def random_backoff(min_wait=1, max_wait=60):
110
+ """Random wait between min and max"""
111
+ while True:
112
+ yield random.uniform(min_wait, max_wait)
113
+
114
+
115
+ @backoff.on_exception(
116
+ random_backoff,
117
+ Exception,
118
+ min_wait=1,
119
+ max_wait=10,
120
+ )
121
+ def my_function():
122
+ pass
123
+ ```
124
+
125
+ ### Time-of-Day Aware
126
+
127
+ ```python
128
+ from datetime import datetime, timezone
129
+
130
+
131
+ def business_hours_backoff():
132
+ """Shorter waits during business hours"""
133
+ while True:
134
+ hour = datetime.now(tz=timezone.utc).hour
135
+ if 9 <= hour < 17:
136
+ yield 5 # 5 seconds during business hours
137
+ else:
138
+ yield 60 # 1 minute otherwise
139
+
140
+
141
+ @backoff.on_exception(business_hours_backoff, Exception)
142
+ def my_function():
143
+ pass
144
+ ```