python-backoff 2.4.0__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 CHANGED
@@ -12,11 +12,13 @@ For examples and full documentation see the README at
12
12
  https://github.com/python-backoff/backoff
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
18
  from backoff._wait_gen import constant, decay, expo, fibo, runtime
18
19
 
19
20
  __all__ = [
21
+ "Attempt",
20
22
  "aretry_context",
21
23
  "constant",
22
24
  "decay",
@@ -30,4 +32,4 @@ __all__ = [
30
32
  "runtime",
31
33
  ]
32
34
 
33
- __version__ = "2.4.0"
35
+ __version__ = "3.0.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")
@@ -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,11 +210,7 @@ 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
215
  log_args = [details["target"].__name__, details["wait"]] # ty:ignore[unresolved-attribute]
235
216
 
@@ -243,11 +224,7 @@ def _log_backoff(
243
224
 
244
225
 
245
226
  # Default giveup handler
246
- def _log_giveup(
247
- details: Details,
248
- logger: logging.Logger | logging.LoggerAdapter,
249
- log_level: int,
250
- ) -> None:
227
+ def _log_giveup(details: Details, logger: _AnyLogger, log_level: int) -> None:
251
228
  msg = "Giving up %s(...) after %d tries (%s)"
252
229
  log_args = [details["target"].__name__, details["tries"]] # ty:ignore[unresolved-attribute]
253
230
 
@@ -266,7 +243,7 @@ def _log_giveup(
266
243
  # directly since it's no longer the active exception by this point).
267
244
  def _log_backoff_context(
268
245
  details: ContextDetails,
269
- logger: logging.Logger | logging.LoggerAdapter,
246
+ logger: _AnyLogger,
270
247
  log_level: int,
271
248
  ) -> None:
272
249
  logger.log(
@@ -280,7 +257,7 @@ def _log_backoff_context(
280
257
  # Default giveup handler for retry_context/aretry_context.
281
258
  def _log_giveup_context(
282
259
  details: ContextDetails,
283
- logger: logging.Logger | logging.LoggerAdapter,
260
+ logger: logging.Logger | logging.LoggerAdapter[Any],
284
261
  log_level: int,
285
262
  ) -> None:
286
263
  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,15 +292,16 @@ 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
@@ -313,6 +352,7 @@ def retry_context(
313
352
  passed to wait_gen when it is initialized.
314
353
  """
315
354
  logger = _prepare_logger(logger)
355
+ on_try = _config_handlers(on_try)
316
356
  on_success = _config_handlers(on_success)
317
357
  on_backoff = _config_handlers(
318
358
  on_backoff,
@@ -334,6 +374,7 @@ def retry_context(
334
374
  max_time=max_time,
335
375
  jitter=jitter,
336
376
  giveup=giveup,
377
+ on_try=on_try,
337
378
  on_success=on_success,
338
379
  on_backoff=on_backoff,
339
380
  on_giveup=on_giveup,
@@ -350,15 +391,16 @@ def aretry_context(
350
391
  max_time: _MaybeCallable[float] | None = None,
351
392
  jitter: _Jitterer | None = full_jitter,
352
393
  giveup: _Predicate[BaseException] = lambda e: False,
394
+ on_try: _ContextHandler | Iterable[_ContextHandler] | None = None,
353
395
  on_success: _ContextHandler | Iterable[_ContextHandler] | None = None,
354
396
  on_backoff: _ContextHandler | Iterable[_ContextHandler] | None = None,
355
397
  on_giveup: _ContextHandler | Iterable[_ContextHandler] | None = None,
356
398
  raise_on_giveup: bool = True,
357
- logger: _MaybeLogger = "backoff",
399
+ logger: _AnyLoggerOrName | None = "backoff",
358
400
  backoff_log_level: int = logging.INFO,
359
401
  giveup_log_level: int = logging.ERROR,
360
402
  **wait_gen_kwargs: Any,
361
- ) -> AsyncGenerator[_Attempt, None]:
403
+ ) -> AsyncGenerator[Attempt, None]:
362
404
  """Async counterpart to `retry_context`, for use with `async for`.
363
405
 
364
406
  async for attempt in backoff.aretry_context(ValueError, backoff.expo):
@@ -369,6 +411,7 @@ def aretry_context(
369
411
  `retry_context` for the full argument reference.
370
412
  """
371
413
  logger = _prepare_logger(logger)
414
+ on_try = _config_handlers(on_try)
372
415
  on_success = _config_handlers(on_success)
373
416
  on_backoff = _config_handlers(
374
417
  on_backoff,
@@ -390,6 +433,7 @@ def aretry_context(
390
433
  max_time=max_time,
391
434
  jitter=jitter,
392
435
  giveup=giveup,
436
+ on_try=on_try,
393
437
  on_success=on_success,
394
438
  on_backoff=on_backoff,
395
439
  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,10 @@ 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
6
6
 
7
7
  if TYPE_CHECKING:
8
- from collections.abc import Generator, Iterable
8
+ from collections.abc import Callable, Generator, Iterable
9
9
 
10
10
 
11
11
  def expo(
@@ -103,6 +103,27 @@ def runtime(*, value: Callable[[Any], float]) -> Generator[float, Any, None]:
103
103
  """Generator that is based on parsing the return value or thrown
104
104
  exception of the decorated method
105
105
 
106
+ Useful for honoring a server-specified retry delay, e.g. an HTTP
107
+ `Retry-After` header, rather than a fixed wait sequence:
108
+
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
+ ...
126
+
106
127
  Args:
107
128
  value: a callable which takes as input the decorated
108
129
  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,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: python-backoff
3
- Version: 2.4.0
3
+ Version: 3.0.0
4
4
  Summary: Function decoration for backoff and retry
5
5
  Project-URL: Documentation, https://python-backoff.github.io/backoff
6
6
  Project-URL: Repository, https://github.com/python-backoff/backoff
@@ -15,7 +15,6 @@ Classifier: Natural Language :: English
15
15
  Classifier: Operating System :: OS Independent
16
16
  Classifier: Programming Language :: Python
17
17
  Classifier: Programming Language :: Python :: 3
18
- Classifier: Programming Language :: Python :: 3.9
19
18
  Classifier: Programming Language :: Python :: 3.10
20
19
  Classifier: Programming Language :: Python :: 3.11
21
20
  Classifier: Programming Language :: Python :: 3.12
@@ -25,7 +24,7 @@ Classifier: Programming Language :: Python :: 3.15
25
24
  Classifier: Topic :: Internet :: WWW/HTTP
26
25
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
27
26
  Classifier: Topic :: Utilities
28
- Requires-Python: >=3.9
27
+ Requires-Python: >=3.10
29
28
  Description-Content-Type: text/x-rst
30
29
 
31
30
  backoff
@@ -0,0 +1,14 @@
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,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.31.0
2
+ Generator: hatchling 1.32.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,,