python-backoff 2.3.1__py3-none-any.whl → 3.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
backoff/__init__.py CHANGED
@@ -12,20 +12,24 @@ For examples and full documentation see the README at
12
12
  https://github.com/python-backoff/backoff
13
13
  """
14
14
 
15
- from backoff._decorator import on_exception, on_predicate
15
+ from backoff._common import Attempt
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, expo, fibo, runtime, decay
18
+ from backoff._wait_gen import constant, decay, expo, fibo, runtime
18
19
 
19
20
  __all__ = [
20
- "on_predicate",
21
- "on_exception",
21
+ "Attempt",
22
+ "aretry_context",
22
23
  "constant",
23
- "expo",
24
24
  "decay",
25
+ "expo",
25
26
  "fibo",
26
- "runtime",
27
27
  "full_jitter",
28
+ "on_exception",
29
+ "on_predicate",
28
30
  "random_jitter",
31
+ "retry_context",
32
+ "runtime",
29
33
  ]
30
34
 
31
- __version__ = "2.3.1"
35
+ __version__ = "3.0.0"
backoff/_async.py CHANGED
@@ -1,53 +1,99 @@
1
+ from __future__ import annotations
2
+
1
3
  import asyncio
2
4
  import functools
3
5
  import inspect
4
- import time
6
+ from collections.abc import Callable
7
+ from typing import TYPE_CHECKING, Any, TypeVar
8
+
9
+ from backoff._common import Attempt, _RetryState
10
+
11
+ if TYPE_CHECKING:
12
+ import sys
13
+ from collections.abc import AsyncGenerator, Coroutine, Iterable
14
+ from typing import ParamSpec
15
+
16
+ from backoff._typing import (
17
+ ContextDetails,
18
+ Details,
19
+ _BaseDetails,
20
+ _CallDetails,
21
+ _ContextHandler,
22
+ _Handler,
23
+ _Jitterer,
24
+ _MaybeCallable,
25
+ _MaybeTuple,
26
+ _Predicate,
27
+ _WaitGenerator,
28
+ )
29
+
30
+ if sys.version_info >= (3, 11):
31
+ from typing import Unpack
32
+ else:
33
+ from typing_extensions import Unpack
34
+
35
+ T = TypeVar("T")
36
+ P = ParamSpec("P")
5
37
 
6
- from backoff._common import _init_wait_gen, _maybe_call, _next_wait
38
+ _AsyncHandler = Callable[["Details"], "Coroutine[Any, Any, None]"]
7
39
 
8
40
 
9
- def _ensure_coroutine(coro_or_func):
41
+ def _ensure_coroutine(
42
+ coro_or_func: Callable[..., Any],
43
+ ) -> Callable[..., Coroutine[Any, Any, Any]]:
10
44
  if inspect.iscoroutinefunction(coro_or_func):
11
45
  return coro_or_func
12
- else:
13
46
 
14
- @functools.wraps(coro_or_func)
15
- async def f(*args, **kwargs):
16
- return coro_or_func(*args, **kwargs)
47
+ @functools.wraps(coro_or_func)
48
+ async def f(*args: Any, **kwargs: Any) -> Any: # ruff:ignore[unused-async]
49
+ return coro_or_func(*args, **kwargs)
17
50
 
18
- return f
51
+ return f
19
52
 
20
53
 
21
- def _ensure_coroutines(coros_or_funcs):
54
+ def _ensure_coroutines(
55
+ coros_or_funcs: Iterable[Callable[..., Any]],
56
+ ) -> list[Callable[..., Coroutine[Any, Any, Any]]]:
22
57
  return [_ensure_coroutine(f) for f in coros_or_funcs]
23
58
 
24
59
 
25
- async def _call_handlers(handlers, *, target, args, kwargs, tries, elapsed, **extra):
26
- details = {
60
+ async def _call_handlers(
61
+ handlers: Iterable[_AsyncHandler],
62
+ *,
63
+ target: Callable[..., Any],
64
+ args: tuple[Any, ...],
65
+ kwargs: dict[str, Any],
66
+ tries: int,
67
+ elapsed: float,
68
+ **extra: Unpack[_CallDetails],
69
+ ) -> None:
70
+ details: Details = {
27
71
  "target": target,
28
72
  "args": args,
29
73
  "kwargs": kwargs,
30
74
  "tries": tries,
31
75
  "elapsed": elapsed,
76
+ **extra,
32
77
  }
33
- details.update(extra)
34
78
  for handler in handlers:
35
79
  await handler(details)
36
80
 
37
81
 
38
82
  def retry_predicate(
39
- target,
40
- wait_gen,
41
- predicate,
83
+ target: Callable[P, Coroutine[object, object, T]],
84
+ wait_gen: _WaitGenerator,
85
+ predicate: _Predicate[T],
42
86
  *,
43
- max_tries,
44
- max_time,
45
- jitter,
46
- on_success,
47
- on_backoff,
48
- on_giveup,
49
- wait_gen_kwargs,
50
- ):
87
+ max_tries: _MaybeCallable[int] | None,
88
+ max_time: _MaybeCallable[float] | None,
89
+ jitter: _Jitterer | None,
90
+ on_try: Iterable[_Handler],
91
+ on_success: Iterable[_Handler],
92
+ on_backoff: Iterable[_Handler],
93
+ on_giveup: Iterable[_Handler],
94
+ wait_gen_kwargs: dict[str, Any],
95
+ ) -> Callable[P, Coroutine[object, object, T]]:
96
+ on_try = _ensure_coroutines(on_try)
51
97
  on_success = _ensure_coroutines(on_success)
52
98
  on_backoff = _ensure_coroutines(on_backoff)
53
99
  on_giveup = _ensure_coroutines(on_giveup)
@@ -59,38 +105,33 @@ def retry_predicate(
59
105
  assert inspect.iscoroutinefunction(target)
60
106
 
61
107
  @functools.wraps(target)
62
- async def retry(*args, **kwargs):
63
- # update variables from outer function args
64
- max_tries_value = _maybe_call(max_tries)
65
- max_time_value = _maybe_call(max_time)
66
-
67
- tries = 0
68
- start = time.monotonic()
69
- wait = _init_wait_gen(wait_gen, wait_gen_kwargs)
108
+ async def retry(*args: P.args, **kwargs: P.kwargs) -> T:
109
+ state = _RetryState(
110
+ wait_gen,
111
+ wait_gen_kwargs,
112
+ max_tries=max_tries,
113
+ max_time=max_time,
114
+ )
70
115
  while True:
71
- tries += 1
72
- elapsed = time.monotonic() - start
73
- details = {
116
+ state.start_attempt()
117
+ details: _BaseDetails = {
74
118
  "target": target,
75
119
  "args": args,
76
120
  "kwargs": kwargs,
77
- "tries": tries,
78
- "elapsed": elapsed,
121
+ "tries": state.tries,
122
+ "elapsed": state.elapsed,
79
123
  }
80
-
124
+ await _call_handlers(on_try, **details)
81
125
  ret = await target(*args, **kwargs)
82
- if predicate(ret):
83
- max_tries_exceeded = tries == max_tries_value
84
- max_time_exceeded = (
85
- max_time_value is not None and elapsed >= max_time_value
86
- )
126
+ details["elapsed"] = state.record_elapsed()
87
127
 
88
- if max_tries_exceeded or max_time_exceeded:
128
+ if predicate(ret):
129
+ if state.exhausted():
89
130
  await _call_handlers(on_giveup, **details, value=ret)
90
131
  break
91
132
 
92
133
  try:
93
- seconds = _next_wait(wait, ret, jitter, elapsed, max_time_value)
134
+ seconds = state.next_wait(ret, jitter)
94
135
  except StopIteration:
95
136
  await _call_handlers(on_giveup, **details, value=ret)
96
137
  break
@@ -108,30 +149,50 @@ def retry_predicate(
108
149
  # <https://bugs.python.org/issue28613>
109
150
  await asyncio.sleep(seconds)
110
151
  continue
111
- else:
112
- await _call_handlers(on_success, **details, value=ret)
113
- break
152
+ await _call_handlers(on_success, **details, value=ret)
153
+ break
114
154
 
115
155
  return ret
116
156
 
117
157
  return retry
118
158
 
119
159
 
160
+ def _adapt_context_handlers(
161
+ handlers: Iterable[_AsyncHandler],
162
+ target: Callable[..., Any],
163
+ args: tuple[Any, ...],
164
+ kwargs: dict[str, Any],
165
+ ) -> list[_ContextHandler]:
166
+ async def adapted(details: ContextDetails) -> None:
167
+ full_details: Details = {
168
+ "target": target,
169
+ "args": args,
170
+ "kwargs": kwargs,
171
+ **details,
172
+ }
173
+ for hdlr in handlers:
174
+ await hdlr(full_details)
175
+
176
+ return [adapted]
177
+
178
+
120
179
  def retry_exception(
121
- target,
122
- wait_gen,
123
- exception,
180
+ target: Callable[P, Coroutine[object, object, T]],
181
+ wait_gen: _WaitGenerator,
182
+ exception: _MaybeTuple[type[Exception]],
124
183
  *,
125
- max_tries,
126
- max_time,
127
- jitter,
128
- giveup,
129
- on_success,
130
- on_backoff,
131
- on_giveup,
132
- raise_on_giveup,
133
- wait_gen_kwargs,
134
- ):
184
+ max_tries: _MaybeCallable[int] | None,
185
+ max_time: _MaybeCallable[float] | None,
186
+ jitter: _Jitterer | None,
187
+ giveup: _Predicate[Exception],
188
+ on_try: Iterable[_Handler],
189
+ on_success: Iterable[_Handler],
190
+ on_backoff: Iterable[_Handler],
191
+ on_giveup: Iterable[_Handler],
192
+ raise_on_giveup: bool,
193
+ wait_gen_kwargs: dict[str, Any],
194
+ ) -> Callable[P, Coroutine[object, object, T]]:
195
+ on_try = _ensure_coroutines(on_try)
135
196
  on_success = _ensure_coroutines(on_success)
136
197
  on_backoff = _ensure_coroutines(on_backoff)
137
198
  on_giveup = _ensure_coroutines(on_giveup)
@@ -142,60 +203,108 @@ def retry_exception(
142
203
  assert not inspect.iscoroutinefunction(jitter)
143
204
 
144
205
  @functools.wraps(target)
145
- async def retry(*args, **kwargs):
146
- max_tries_value = _maybe_call(max_tries)
147
- max_time_value = _maybe_call(max_time)
206
+ async def retry(
207
+ *args: P.args,
208
+ **kwargs: P.kwargs,
209
+ ) -> T:
210
+ ret: T = None # type: ignore[assignment] # ty:ignore[invalid-assignment]
211
+
212
+ async for attempt in aretry_context(
213
+ exception,
214
+ wait_gen,
215
+ max_tries=max_tries,
216
+ max_time=max_time,
217
+ jitter=jitter,
218
+ giveup=giveup,
219
+ on_try=_adapt_context_handlers(on_try, target, args, kwargs),
220
+ on_success=_adapt_context_handlers(on_success, target, args, kwargs),
221
+ on_backoff=_adapt_context_handlers(on_backoff, target, args, kwargs),
222
+ on_giveup=_adapt_context_handlers(on_giveup, target, args, kwargs),
223
+ raise_on_giveup=raise_on_giveup,
224
+ wait_gen_kwargs=wait_gen_kwargs,
225
+ ):
226
+ with attempt:
227
+ ret = await target(*args, **kwargs)
148
228
 
149
- tries = 0
150
- start = time.monotonic()
151
- wait = _init_wait_gen(wait_gen, wait_gen_kwargs)
152
- while True:
153
- tries += 1
154
- elapsed = time.monotonic() - start
155
- details = {
156
- "target": target,
157
- "args": args,
158
- "kwargs": kwargs,
159
- "tries": tries,
160
- "elapsed": elapsed,
161
- }
229
+ return ret
162
230
 
163
- try:
164
- ret = await target(*args, **kwargs)
165
- except exception as e:
166
- giveup_result = await giveup(e)
167
- max_tries_exceeded = tries == max_tries_value
168
- max_time_exceeded = (
169
- max_time_value is not None and elapsed >= max_time_value
170
- )
171
-
172
- if giveup_result or max_tries_exceeded or max_time_exceeded:
173
- await _call_handlers(on_giveup, **details, exception=e)
174
- if raise_on_giveup:
175
- raise
176
- return None
231
+ return retry
177
232
 
178
- try:
179
- seconds = _next_wait(wait, e, jitter, elapsed, max_time_value)
180
- except StopIteration:
181
- await _call_handlers(on_giveup, **details, exception=e)
182
- raise e
183
233
 
184
- await _call_handlers(on_backoff, **details, wait=seconds, exception=e)
234
+ async def _dispatch_handlers(
235
+ handlers: Iterable[_ContextHandler], **details: Any
236
+ ) -> None:
237
+ for hdlr in _ensure_coroutines(handlers):
238
+ await hdlr(details)
185
239
 
186
- # Note: there is no convenient way to pass explicit event
187
- # loop to decorator, so here we assume that either default
188
- # thread event loop is set and correct (it mostly is
189
- # by default), or Python >= 3.5.3 or Python >= 3.6 is used
190
- # where loop.get_event_loop() in coroutine guaranteed to
191
- # return correct value.
192
- # See for details:
193
- # <https://groups.google.com/forum/#!topic/python-tulip/yF9C-rFpiKk>
194
- # <https://bugs.python.org/issue28613>
195
- await asyncio.sleep(seconds)
196
- else:
197
- await _call_handlers(on_success, **details)
198
240
 
199
- return ret
241
+ async def aretry_context(
242
+ exception: _MaybeTuple[type[Exception]],
243
+ wait_gen: _WaitGenerator,
244
+ *,
245
+ max_tries: _MaybeCallable[int] | None,
246
+ max_time: _MaybeCallable[float] | None,
247
+ jitter: _Jitterer | None,
248
+ giveup: _Predicate[BaseException],
249
+ on_try: Iterable[_ContextHandler],
250
+ on_success: Iterable[_ContextHandler],
251
+ on_backoff: Iterable[_ContextHandler],
252
+ on_giveup: Iterable[_ContextHandler],
253
+ raise_on_giveup: bool,
254
+ wait_gen_kwargs: dict[str, Any],
255
+ ) -> AsyncGenerator[Attempt, None]:
256
+ giveup = _ensure_coroutine(giveup)
200
257
 
201
- return retry
258
+ state = _RetryState(
259
+ wait_gen,
260
+ wait_gen_kwargs,
261
+ max_tries=max_tries,
262
+ max_time=max_time,
263
+ )
264
+ while True:
265
+ state.start_attempt()
266
+ attempt = Attempt(exception)
267
+ await _dispatch_handlers(
268
+ handlers=on_try,
269
+ tries=state.tries,
270
+ elapsed=state.elapsed,
271
+ )
272
+ yield attempt
273
+ elapsed = state.record_elapsed()
274
+
275
+ exc = attempt.exception
276
+ if exc is None:
277
+ await _dispatch_handlers(on_success, tries=state.tries, elapsed=elapsed)
278
+ return
279
+
280
+ if await giveup(exc) or state.exhausted():
281
+ await _dispatch_handlers(
282
+ on_giveup,
283
+ tries=state.tries,
284
+ elapsed=elapsed,
285
+ exception=exc,
286
+ )
287
+ if raise_on_giveup:
288
+ raise exc
289
+ return
290
+
291
+ try:
292
+ seconds = state.next_wait(exc, jitter)
293
+ except StopIteration:
294
+ await _dispatch_handlers(
295
+ on_giveup,
296
+ tries=state.tries,
297
+ elapsed=elapsed,
298
+ exception=exc,
299
+ )
300
+ raise exc from None
301
+
302
+ await _dispatch_handlers(
303
+ on_backoff,
304
+ tries=state.tries,
305
+ elapsed=elapsed,
306
+ wait=seconds,
307
+ exception=exc,
308
+ )
309
+
310
+ await asyncio.sleep(seconds)