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