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/_sync.py CHANGED
@@ -1,130 +1,238 @@
1
- # coding:utf-8
1
+ from __future__ import annotations
2
+
2
3
  import functools
3
4
  import time
4
-
5
- from backoff._common import (_init_wait_gen, _maybe_call, _next_wait)
6
-
7
-
8
- def _call_handlers(hdlrs, target, args, kwargs, tries, elapsed, **extra):
9
- details = {
10
- 'target': target,
11
- 'args': args,
12
- 'kwargs': kwargs,
13
- 'tries': tries,
14
- 'elapsed': elapsed,
5
+ from typing import TYPE_CHECKING, Any, Callable, TypeVar
6
+
7
+ from backoff._common import _Attempt, _dispatch_handlers, _RetryState
8
+
9
+ if TYPE_CHECKING:
10
+ import sys
11
+ from collections.abc import Generator, Iterable
12
+
13
+ from backoff._typing import (
14
+ ContextDetails,
15
+ Details,
16
+ _BaseDetails,
17
+ _CallDetails,
18
+ _ContextHandler,
19
+ _Handler,
20
+ _Jitterer,
21
+ _MaybeCallable,
22
+ _MaybeTuple,
23
+ _Predicate,
24
+ _WaitGenerator,
25
+ )
26
+
27
+ if sys.version_info >= (3, 10):
28
+ from typing import ParamSpec
29
+ else:
30
+ from typing_extensions import ParamSpec
31
+
32
+ if sys.version_info >= (3, 11):
33
+ from typing import Unpack
34
+ else:
35
+ from typing_extensions import Unpack
36
+
37
+ T = TypeVar("T")
38
+ P = ParamSpec("P")
39
+
40
+
41
+ def _call_handlers(
42
+ hdlrs: Iterable[_Handler],
43
+ target: Callable[..., Any],
44
+ args: tuple[Any, ...],
45
+ kwargs: dict[str, Any],
46
+ tries: int,
47
+ elapsed: float,
48
+ **extra: Unpack[_CallDetails],
49
+ ) -> None:
50
+ details: Details = {
51
+ "target": target,
52
+ "args": args,
53
+ "kwargs": kwargs,
54
+ "tries": tries,
55
+ "elapsed": elapsed,
15
56
  }
57
+ # pyrefly: ignore [no-matching-overload]
16
58
  details.update(extra)
17
59
  for hdlr in hdlrs:
18
60
  hdlr(details)
19
61
 
20
62
 
21
- def retry_predicate(target, wait_gen, predicate,
22
- *,
23
- max_tries, max_time, jitter,
24
- on_success, on_backoff, on_giveup,
25
- wait_gen_kwargs):
26
-
63
+ def retry_predicate(
64
+ target: Callable[P, T],
65
+ wait_gen: _WaitGenerator,
66
+ predicate: _Predicate[T],
67
+ *,
68
+ max_tries: _MaybeCallable[int] | None,
69
+ max_time: _MaybeCallable[float] | None,
70
+ jitter: _Jitterer | None,
71
+ on_success: Iterable[_Handler],
72
+ on_backoff: Iterable[_Handler],
73
+ on_giveup: Iterable[_Handler],
74
+ wait_gen_kwargs: dict[str, Any],
75
+ ) -> Callable[P, T]:
27
76
  @functools.wraps(target)
28
- def retry(*args, **kwargs):
29
- max_tries_value = _maybe_call(max_tries)
30
- max_time_value = _maybe_call(max_time)
31
-
32
- tries = 0
33
- start = time.monotonic()
34
- wait = _init_wait_gen(wait_gen, wait_gen_kwargs)
77
+ def retry(*args: P.args, **kwargs: P.kwargs) -> T:
78
+ state = _RetryState(
79
+ wait_gen,
80
+ wait_gen_kwargs,
81
+ max_tries=max_tries,
82
+ max_time=max_time,
83
+ )
35
84
  while True:
36
- tries += 1
37
- elapsed = time.monotonic() - start
38
- details = {
85
+ state.start_attempt()
86
+ ret = target(*args, **kwargs)
87
+ details: _BaseDetails = {
39
88
  "target": target,
40
89
  "args": args,
41
90
  "kwargs": kwargs,
42
- "tries": tries,
43
- "elapsed": elapsed,
91
+ "tries": state.tries,
92
+ "elapsed": state.record_elapsed(),
44
93
  }
45
94
 
46
- ret = target(*args, **kwargs)
47
95
  if predicate(ret):
48
- max_tries_exceeded = (tries == max_tries_value)
49
- max_time_exceeded = (max_time_value is not None and
50
- elapsed >= max_time_value)
51
-
52
- if max_tries_exceeded or max_time_exceeded:
96
+ if state.exhausted():
53
97
  _call_handlers(on_giveup, **details, value=ret)
54
98
  break
55
99
 
56
100
  try:
57
- seconds = _next_wait(wait, ret, jitter, elapsed,
58
- max_time_value)
101
+ seconds = state.next_wait(ret, jitter)
59
102
  except StopIteration:
60
- _call_handlers(on_giveup, **details)
103
+ _call_handlers(on_giveup, **details, value=ret)
61
104
  break
62
105
 
63
- _call_handlers(on_backoff, **details,
64
- value=ret, wait=seconds)
106
+ _call_handlers(on_backoff, **details, value=ret, wait=seconds)
65
107
 
66
108
  time.sleep(seconds)
67
109
  continue
68
- else:
69
- _call_handlers(on_success, **details, value=ret)
70
- break
110
+ _call_handlers(on_success, **details, value=ret)
111
+ break
71
112
 
72
113
  return ret
73
114
 
74
115
  return retry
75
116
 
76
117
 
77
- def retry_exception(target, wait_gen, exception,
78
- *,
79
- max_tries, max_time, jitter, giveup,
80
- on_success, on_backoff, on_giveup, raise_on_giveup,
81
- wait_gen_kwargs):
82
-
118
+ def _adapt_context_handlers(
119
+ handlers: Iterable[_Handler],
120
+ target: Callable[..., Any],
121
+ args: tuple[Any, ...],
122
+ kwargs: dict[str, Any],
123
+ ) -> list[_ContextHandler]:
124
+ def adapted(details: ContextDetails) -> None:
125
+ full_details: Details = {
126
+ "target": target,
127
+ "args": args,
128
+ "kwargs": kwargs,
129
+ **details,
130
+ }
131
+ for hdlr in handlers:
132
+ hdlr(full_details)
133
+
134
+ return [adapted]
135
+
136
+
137
+ def retry_exception(
138
+ target: Callable[P, T],
139
+ wait_gen: _WaitGenerator,
140
+ exception: _MaybeTuple[type[Exception]],
141
+ *,
142
+ max_tries: _MaybeCallable[int] | None,
143
+ max_time: _MaybeCallable[float] | None,
144
+ jitter: _Jitterer | None,
145
+ giveup: _Predicate[Exception],
146
+ on_success: Iterable[_Handler],
147
+ on_backoff: Iterable[_Handler],
148
+ on_giveup: Iterable[_Handler],
149
+ raise_on_giveup: bool,
150
+ wait_gen_kwargs: dict[str, Any],
151
+ ) -> Callable[P, T]:
83
152
  @functools.wraps(target)
84
- def retry(*args, **kwargs):
85
- max_tries_value = _maybe_call(max_tries)
86
- max_time_value = _maybe_call(max_time)
87
-
88
- tries = 0
89
- start = time.monotonic()
90
- wait = _init_wait_gen(wait_gen, wait_gen_kwargs)
91
- while True:
92
- tries += 1
93
- elapsed = time.monotonic() - start
94
- details = {
95
- "target": target,
96
- "args": args,
97
- "kwargs": kwargs,
98
- "tries": tries,
99
- "elapsed": elapsed,
100
- }
101
-
102
- try:
153
+ def retry(*args: P.args, **kwargs: P.kwargs) -> T:
154
+ ret: T = None # type: ignore[assignment] # ty:ignore[invalid-assignment]
155
+
156
+ for attempt in retry_context(
157
+ exception,
158
+ wait_gen,
159
+ max_tries=max_tries,
160
+ max_time=max_time,
161
+ jitter=jitter,
162
+ giveup=giveup, # type: ignore[arg-type] # ty:ignore[invalid-argument-type]
163
+ on_success=_adapt_context_handlers(on_success, target, args, kwargs),
164
+ on_backoff=_adapt_context_handlers(on_backoff, target, args, kwargs),
165
+ on_giveup=_adapt_context_handlers(on_giveup, target, args, kwargs),
166
+ raise_on_giveup=raise_on_giveup,
167
+ wait_gen_kwargs=wait_gen_kwargs,
168
+ ):
169
+ with attempt:
103
170
  ret = target(*args, **kwargs)
104
- except exception as e:
105
- max_tries_exceeded = (tries == max_tries_value)
106
- max_time_exceeded = (max_time_value is not None and
107
- elapsed >= max_time_value)
108
-
109
- if giveup(e) or max_tries_exceeded or max_time_exceeded:
110
- _call_handlers(on_giveup, **details, exception=e)
111
- if raise_on_giveup:
112
- raise
113
- return None
114
171
 
115
- try:
116
- seconds = _next_wait(wait, e, jitter, elapsed,
117
- max_time_value)
118
- except StopIteration:
119
- _call_handlers(on_giveup, **details, exception=e)
120
- raise e
172
+ return ret
121
173
 
122
- _call_handlers(on_backoff, **details, wait=seconds,
123
- exception=e)
174
+ return retry
124
175
 
125
- time.sleep(seconds)
126
- else:
127
- _call_handlers(on_success, **details)
128
176
 
129
- return ret
130
- return retry
177
+ def retry_context(
178
+ exception: _MaybeTuple[type[Exception]],
179
+ wait_gen: _WaitGenerator,
180
+ *,
181
+ max_tries: _MaybeCallable[int] | None,
182
+ max_time: _MaybeCallable[float] | None,
183
+ jitter: _Jitterer | None,
184
+ giveup: _Predicate[BaseException],
185
+ on_success: Iterable[_ContextHandler],
186
+ on_backoff: Iterable[_ContextHandler],
187
+ on_giveup: Iterable[_ContextHandler],
188
+ raise_on_giveup: bool,
189
+ wait_gen_kwargs: dict[str, Any],
190
+ ) -> Generator[_Attempt, None, None]:
191
+ state = _RetryState(
192
+ wait_gen,
193
+ wait_gen_kwargs,
194
+ max_tries=max_tries,
195
+ max_time=max_time,
196
+ )
197
+ while True:
198
+ state.start_attempt()
199
+ attempt = _Attempt(exception)
200
+ yield attempt
201
+ elapsed = state.record_elapsed()
202
+
203
+ exc = attempt.exception
204
+ if exc is None:
205
+ _dispatch_handlers(on_success, tries=state.tries, elapsed=elapsed)
206
+ return
207
+
208
+ if giveup(exc) or state.exhausted():
209
+ _dispatch_handlers(
210
+ on_giveup,
211
+ tries=state.tries,
212
+ elapsed=elapsed,
213
+ exception=exc,
214
+ )
215
+ if raise_on_giveup:
216
+ raise exc
217
+ return
218
+
219
+ try:
220
+ seconds = state.next_wait(exc, jitter)
221
+ except StopIteration:
222
+ _dispatch_handlers(
223
+ on_giveup,
224
+ tries=state.tries,
225
+ elapsed=elapsed,
226
+ exception=exc,
227
+ )
228
+ raise exc from None
229
+
230
+ _dispatch_handlers(
231
+ on_backoff,
232
+ tries=state.tries,
233
+ elapsed=elapsed,
234
+ wait=seconds,
235
+ exception=exc,
236
+ )
237
+
238
+ time.sleep(seconds)
backoff/_typing.py CHANGED
@@ -1,45 +1,68 @@
1
- # coding:utf-8
1
+ from __future__ import annotations
2
+
2
3
  import logging
3
- import sys
4
- from typing import (Any, Callable, Coroutine, Dict, Generator, Sequence, Tuple,
5
- TypeVar, Union)
6
-
7
- if sys.version_info >= (3, 8): # pragma: no cover
8
- from typing import TypedDict
9
- else: # pragma: no cover
10
- # use typing_extensions if installed but don't require it
11
- try:
12
- from typing_extensions import TypedDict
13
- except ImportError:
14
- class TypedDict(dict):
15
- def __init_subclass__(cls, **kwargs: Any) -> None:
16
- return super().__init_subclass__()
17
-
18
-
19
- class _Details(TypedDict):
4
+ from collections.abc import Coroutine, Generator
5
+ from typing import (
6
+ Any,
7
+ Callable,
8
+ TypedDict,
9
+ TypeVar,
10
+ Union,
11
+ )
12
+
13
+
14
+ class _BaseDetails(TypedDict):
20
15
  target: Callable[..., Any]
21
- args: Tuple[Any, ...]
22
- kwargs: Dict[str, Any]
16
+ args: tuple[Any, ...]
17
+ kwargs: dict[str, Any]
23
18
  tries: int
24
19
  elapsed: float
25
20
 
26
21
 
27
- class Details(_Details, total=False):
22
+ class _CallDetails(TypedDict, total=False):
28
23
  wait: float # present in the on_backoff handler case for either decorator
29
24
  value: Any # present in the on_predicate decorator case
30
25
  exception: Exception # present in the on_exception decorator case
31
26
 
32
27
 
28
+ class Details(_BaseDetails, _CallDetails, total=False):
29
+ pass
30
+
31
+
32
+ class _BaseContextDetails(TypedDict):
33
+ tries: int
34
+ elapsed: float
35
+
36
+
37
+ class _ContextCallDetails(TypedDict, total=False):
38
+ wait: float # present in the on_backoff handler case
39
+ exception: Exception # present in the on_giveup handler case
40
+
41
+
42
+ class ContextDetails(_BaseContextDetails, _ContextCallDetails, total=False):
43
+ """Details passed to handlers registered with `retry_context`/`aretry_context`.
44
+
45
+ Unlike `Details`, there's no wrapped callable, so no `target`/`args`/`kwargs`.
46
+ """
47
+
48
+
33
49
  T = TypeVar("T")
34
50
 
35
- _CallableT = TypeVar('_CallableT', bound=Callable[..., Any])
51
+ _CallableT = TypeVar("_CallableT", bound=Callable[..., Any]) # ruff:ignore[unused-private-type-var]
36
52
  _Handler = Union[
37
53
  Callable[[Details], None],
38
54
  Callable[[Details], Coroutine[Any, Any, None]],
39
55
  ]
56
+ _ContextHandler = Union[
57
+ Callable[[ContextDetails], None],
58
+ Callable[[ContextDetails], Coroutine[Any, Any, None]],
59
+ ]
40
60
  _Jitterer = Callable[[float], float]
41
61
  _MaybeCallable = Union[T, Callable[[], T]]
42
62
  _MaybeLogger = Union[str, logging.Logger, logging.LoggerAdapter, None]
43
- _MaybeSequence = Union[T, Sequence[T]]
44
- _Predicate = Callable[[T], bool]
45
- _WaitGenerator = Callable[..., Generator[float, None, None]]
63
+ _MaybeTuple = Union[T, tuple[T, ...]]
64
+ _Predicate = Union[
65
+ Callable[[T], bool],
66
+ Callable[[T], Coroutine[Any, Any, bool]],
67
+ ]
68
+ _WaitGenerator = Callable[..., Generator[float, Any, None]]
backoff/_wait_gen.py CHANGED
@@ -1,16 +1,18 @@
1
- # coding:utf-8
1
+ from __future__ import annotations
2
2
 
3
3
  import itertools
4
4
  import math
5
- from typing import Any, Callable, Generator, Iterable, Optional, Union
5
+ from typing import TYPE_CHECKING, Any, Callable
6
+
7
+ if TYPE_CHECKING:
8
+ from collections.abc import Generator, Iterable
6
9
 
7
10
 
8
11
  def expo(
9
12
  base: float = 2,
10
13
  factor: float = 1,
11
- max_value: Optional[float] = None,
14
+ max_value: float | None = None,
12
15
  ) -> Generator[float, Any, None]:
13
-
14
16
  """Generator for exponential decay.
15
17
 
16
18
  Args:
@@ -21,23 +23,21 @@ def expo(
21
23
  of max_value will forever after be yielded.
22
24
  """
23
25
  # Advance past initial .send() call
24
- yield # type: ignore[misc]
25
- base_n: float = 1
26
+ yield 0
27
+
28
+ a = factor
29
+ while max_value is None or a < max_value:
30
+ yield a
31
+ a *= base
26
32
  while True:
27
- a = factor * base_n
28
- if max_value is None or a < max_value:
29
- yield a
30
- base_n *= base
31
- else:
32
- yield max_value
33
+ yield max_value
33
34
 
34
35
 
35
36
  def decay(
36
37
  initial_value: float = 1,
37
38
  decay_factor: float = 1,
38
- min_value: Optional[float] = None,
39
+ min_value: float | None = None,
39
40
  ) -> Generator[float, Any, None]:
40
-
41
41
  """Generator for exponential decay[1]:
42
42
 
43
43
  Args:
@@ -50,18 +50,17 @@ def decay(
50
50
  [1] https://en.wikipedia.org/wiki/Exponential_decay
51
51
  """
52
52
  # Advance past initial .send() call
53
- yield # type: ignore[misc]
54
- t = 0
53
+ yield 0
54
+ a = initial_value
55
+ min_value = min_value or 0.0
56
+ while a > min_value:
57
+ yield a
58
+ a *= math.exp(-decay_factor)
55
59
  while True:
56
- a = initial_value * math.e ** (-t * decay_factor)
57
- if min_value is None or a > min_value:
58
- yield a
59
- t += 1
60
- else:
61
- yield min_value
60
+ yield min_value
62
61
 
63
62
 
64
- def fibo(max_value: Optional[int] = None) -> Generator[int, None, None]:
63
+ def fibo(max_value: int | None = None) -> Generator[int, Any, None]:
65
64
  """Generator for fibonaccial decay.
66
65
 
67
66
  Args:
@@ -70,42 +69,37 @@ def fibo(max_value: Optional[int] = None) -> Generator[int, None, None]:
70
69
  of max_value will forever after be yielded.
71
70
  """
72
71
  # Advance past initial .send() call
73
- yield # type: ignore[misc]
72
+ yield 0
74
73
 
75
74
  a = 1
76
75
  b = 1
76
+ while max_value is None or a < max_value:
77
+ yield a
78
+ a, b = b, a + b
77
79
  while True:
78
- if max_value is None or a < max_value:
79
- yield a
80
- a, b = b, a + b
81
- else:
82
- yield max_value
80
+ yield max_value
83
81
 
84
82
 
85
- def constant(
86
- interval: Union[int, Iterable[float]] = 1,
87
- ) -> Generator[float, None, None]:
83
+ def constant(interval: float | Iterable[float] = 1) -> Generator[float, Any, None]:
88
84
  """Generator for constant intervals.
89
85
 
90
86
  Args:
91
87
  interval: A constant value to yield or an iterable of such values.
92
88
  """
93
89
  # Advance past initial .send() call
94
- yield # type: ignore[misc]
90
+ yield 0
95
91
 
96
- try:
97
- itr = iter(interval) # type: ignore
98
- except TypeError:
99
- itr = itertools.repeat(interval) # type: ignore
92
+ itr = (
93
+ itertools.repeat(interval)
94
+ if isinstance(interval, (int, float))
95
+ else iter(interval)
96
+ )
100
97
 
101
98
  for val in itr:
102
99
  yield val
103
100
 
104
101
 
105
- def runtime(
106
- *,
107
- value: Callable[[Any], float],
108
- ) -> Generator[float, None, None]:
102
+ def runtime(*, value: Callable[[Any], float]) -> Generator[float, Any, None]:
109
103
  """Generator that is based on parsing the return value or thrown
110
104
  exception of the decorated method
111
105
 
@@ -114,6 +108,6 @@ def runtime(
114
108
  function's return value or thrown exception and
115
109
  determines how long to wait
116
110
  """
117
- ret_or_exc = yield # type: ignore[misc]
111
+ ret_or_exc = yield 0
118
112
  while True:
119
113
  ret_or_exc = yield value(ret_or_exc)
backoff/types.py CHANGED
@@ -1,6 +1,5 @@
1
- # coding:utf-8
2
1
  from ._typing import Details
3
2
 
4
3
  __all__ = [
5
- 'Details',
4
+ "Details",
6
5
  ]