tenacity 8.2.3__py3-none-any.whl → 8.3.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.
- tenacity/__init__.py +136 -38
- tenacity/_asyncio.py +59 -5
- tenacity/_utils.py +15 -2
- tenacity/before.py +3 -1
- tenacity/before_sleep.py +2 -1
- tenacity/retry.py +6 -2
- tenacity/stop.py +28 -1
- tenacity/tornadoweb.py +5 -1
- tenacity/wait.py +9 -3
- {tenacity-8.2.3.dist-info → tenacity-8.3.0.dist-info}/METADATA +7 -4
- tenacity-8.3.0.dist-info/RECORD +17 -0
- {tenacity-8.2.3.dist-info → tenacity-8.3.0.dist-info}/WHEEL +1 -1
- tenacity-8.2.3.dist-info/RECORD +0 -17
- {tenacity-8.2.3.dist-info → tenacity-8.3.0.dist-info}/LICENSE +0 -0
- {tenacity-8.2.3.dist-info → tenacity-8.3.0.dist-info}/top_level.txt +0 -0
tenacity/__init__.py
CHANGED
|
@@ -15,8 +15,7 @@
|
|
|
15
15
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
16
16
|
# See the License for the specific language governing permissions and
|
|
17
17
|
# limitations under the License.
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
import dataclasses
|
|
20
19
|
import functools
|
|
21
20
|
import sys
|
|
22
21
|
import threading
|
|
@@ -50,6 +49,7 @@ from .nap import sleep_using_event # noqa
|
|
|
50
49
|
# Import all built-in stop strategies for easier usage.
|
|
51
50
|
from .stop import stop_after_attempt # noqa
|
|
52
51
|
from .stop import stop_after_delay # noqa
|
|
52
|
+
from .stop import stop_before_delay # noqa
|
|
53
53
|
from .stop import stop_all # noqa
|
|
54
54
|
from .stop import stop_any # noqa
|
|
55
55
|
from .stop import stop_never # noqa
|
|
@@ -96,6 +96,29 @@ WrappedFnReturnT = t.TypeVar("WrappedFnReturnT")
|
|
|
96
96
|
WrappedFn = t.TypeVar("WrappedFn", bound=t.Callable[..., t.Any])
|
|
97
97
|
|
|
98
98
|
|
|
99
|
+
dataclass_kwargs = {}
|
|
100
|
+
if sys.version_info >= (3, 10):
|
|
101
|
+
dataclass_kwargs.update({"slots": True})
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@dataclasses.dataclass(**dataclass_kwargs)
|
|
105
|
+
class IterState:
|
|
106
|
+
actions: t.List[t.Callable[["RetryCallState"], t.Any]] = dataclasses.field(
|
|
107
|
+
default_factory=list
|
|
108
|
+
)
|
|
109
|
+
retry_run_result: bool = False
|
|
110
|
+
delay_since_first_attempt: int = 0
|
|
111
|
+
stop_run_result: bool = False
|
|
112
|
+
is_explicit_retry: bool = False
|
|
113
|
+
|
|
114
|
+
def reset(self) -> None:
|
|
115
|
+
self.actions = []
|
|
116
|
+
self.retry_run_result = False
|
|
117
|
+
self.delay_since_first_attempt = 0
|
|
118
|
+
self.stop_run_result = False
|
|
119
|
+
self.is_explicit_retry = False
|
|
120
|
+
|
|
121
|
+
|
|
99
122
|
class TryAgain(Exception):
|
|
100
123
|
"""Always retry the executed function when raised."""
|
|
101
124
|
|
|
@@ -124,7 +147,9 @@ class BaseAction:
|
|
|
124
147
|
NAME: t.Optional[str] = None
|
|
125
148
|
|
|
126
149
|
def __repr__(self) -> str:
|
|
127
|
-
state_str = ", ".join(
|
|
150
|
+
state_str = ", ".join(
|
|
151
|
+
f"{field}={getattr(self, field)!r}" for field in self.REPR_FIELDS
|
|
152
|
+
)
|
|
128
153
|
return f"{self.__class__.__name__}({state_str})"
|
|
129
154
|
|
|
130
155
|
def __str__(self) -> str:
|
|
@@ -220,10 +245,14 @@ class BaseRetrying(ABC):
|
|
|
220
245
|
retry: t.Union[retry_base, object] = _unset,
|
|
221
246
|
before: t.Union[t.Callable[["RetryCallState"], None], object] = _unset,
|
|
222
247
|
after: t.Union[t.Callable[["RetryCallState"], None], object] = _unset,
|
|
223
|
-
before_sleep: t.Union[
|
|
248
|
+
before_sleep: t.Union[
|
|
249
|
+
t.Optional[t.Callable[["RetryCallState"], None]], object
|
|
250
|
+
] = _unset,
|
|
224
251
|
reraise: t.Union[bool, object] = _unset,
|
|
225
252
|
retry_error_cls: t.Union[t.Type[RetryError], object] = _unset,
|
|
226
|
-
retry_error_callback: t.Union[
|
|
253
|
+
retry_error_callback: t.Union[
|
|
254
|
+
t.Optional[t.Callable[["RetryCallState"], t.Any]], object
|
|
255
|
+
] = _unset,
|
|
227
256
|
) -> "BaseRetrying":
|
|
228
257
|
"""Copy this object with some parameters changed if needed."""
|
|
229
258
|
return self.__class__(
|
|
@@ -236,7 +265,9 @@ class BaseRetrying(ABC):
|
|
|
236
265
|
before_sleep=_first_set(before_sleep, self.before_sleep),
|
|
237
266
|
reraise=_first_set(reraise, self.reraise),
|
|
238
267
|
retry_error_cls=_first_set(retry_error_cls, self.retry_error_cls),
|
|
239
|
-
retry_error_callback=_first_set(
|
|
268
|
+
retry_error_callback=_first_set(
|
|
269
|
+
retry_error_callback, self.retry_error_callback
|
|
270
|
+
),
|
|
240
271
|
)
|
|
241
272
|
|
|
242
273
|
def __repr__(self) -> str:
|
|
@@ -278,13 +309,23 @@ class BaseRetrying(ABC):
|
|
|
278
309
|
self._local.statistics = t.cast(t.Dict[str, t.Any], {})
|
|
279
310
|
return self._local.statistics
|
|
280
311
|
|
|
312
|
+
@property
|
|
313
|
+
def iter_state(self) -> IterState:
|
|
314
|
+
try:
|
|
315
|
+
return self._local.iter_state # type: ignore[no-any-return]
|
|
316
|
+
except AttributeError:
|
|
317
|
+
self._local.iter_state = IterState()
|
|
318
|
+
return self._local.iter_state
|
|
319
|
+
|
|
281
320
|
def wraps(self, f: WrappedFn) -> WrappedFn:
|
|
282
321
|
"""Wrap a function for retrying.
|
|
283
322
|
|
|
284
323
|
:param f: A function to wraps for retrying.
|
|
285
324
|
"""
|
|
286
325
|
|
|
287
|
-
@functools.wraps(
|
|
326
|
+
@functools.wraps(
|
|
327
|
+
f, functools.WRAPPER_ASSIGNMENTS + ("__defaults__", "__kwdefaults__")
|
|
328
|
+
)
|
|
288
329
|
def wrapped_f(*args: t.Any, **kw: t.Any) -> t.Any:
|
|
289
330
|
return self(f, *args, **kw)
|
|
290
331
|
|
|
@@ -302,42 +343,89 @@ class BaseRetrying(ABC):
|
|
|
302
343
|
self.statistics["attempt_number"] = 1
|
|
303
344
|
self.statistics["idle_for"] = 0
|
|
304
345
|
|
|
346
|
+
def _add_action_func(self, fn: t.Callable[..., t.Any]) -> None:
|
|
347
|
+
self.iter_state.actions.append(fn)
|
|
348
|
+
|
|
349
|
+
def _run_retry(self, retry_state: "RetryCallState") -> None:
|
|
350
|
+
self.iter_state.retry_run_result = self.retry(retry_state)
|
|
351
|
+
|
|
352
|
+
def _run_wait(self, retry_state: "RetryCallState") -> None:
|
|
353
|
+
if self.wait:
|
|
354
|
+
sleep = self.wait(retry_state)
|
|
355
|
+
else:
|
|
356
|
+
sleep = 0.0
|
|
357
|
+
|
|
358
|
+
retry_state.upcoming_sleep = sleep
|
|
359
|
+
|
|
360
|
+
def _run_stop(self, retry_state: "RetryCallState") -> None:
|
|
361
|
+
self.statistics["delay_since_first_attempt"] = retry_state.seconds_since_start
|
|
362
|
+
self.iter_state.stop_run_result = self.stop(retry_state)
|
|
363
|
+
|
|
305
364
|
def iter(self, retry_state: "RetryCallState") -> t.Union[DoAttempt, DoSleep, t.Any]: # noqa
|
|
365
|
+
self._begin_iter(retry_state)
|
|
366
|
+
result = None
|
|
367
|
+
for action in self.iter_state.actions:
|
|
368
|
+
result = action(retry_state)
|
|
369
|
+
return result
|
|
370
|
+
|
|
371
|
+
def _begin_iter(self, retry_state: "RetryCallState") -> None: # noqa
|
|
372
|
+
self.iter_state.reset()
|
|
373
|
+
|
|
306
374
|
fut = retry_state.outcome
|
|
307
375
|
if fut is None:
|
|
308
376
|
if self.before is not None:
|
|
309
|
-
self.before
|
|
310
|
-
|
|
377
|
+
self._add_action_func(self.before)
|
|
378
|
+
self._add_action_func(lambda rs: DoAttempt())
|
|
379
|
+
return
|
|
380
|
+
|
|
381
|
+
self.iter_state.is_explicit_retry = fut.failed and isinstance(
|
|
382
|
+
fut.exception(), TryAgain
|
|
383
|
+
)
|
|
384
|
+
if not self.iter_state.is_explicit_retry:
|
|
385
|
+
self._add_action_func(self._run_retry)
|
|
386
|
+
self._add_action_func(self._post_retry_check_actions)
|
|
311
387
|
|
|
312
|
-
|
|
313
|
-
if not (is_explicit_retry or self.
|
|
314
|
-
|
|
388
|
+
def _post_retry_check_actions(self, retry_state: "RetryCallState") -> None:
|
|
389
|
+
if not (self.iter_state.is_explicit_retry or self.iter_state.retry_run_result):
|
|
390
|
+
self._add_action_func(lambda rs: rs.outcome.result())
|
|
391
|
+
return
|
|
315
392
|
|
|
316
393
|
if self.after is not None:
|
|
317
|
-
self.after
|
|
394
|
+
self._add_action_func(self.after)
|
|
318
395
|
|
|
319
|
-
self.
|
|
320
|
-
|
|
396
|
+
self._add_action_func(self._run_wait)
|
|
397
|
+
self._add_action_func(self._run_stop)
|
|
398
|
+
self._add_action_func(self._post_stop_check_actions)
|
|
399
|
+
|
|
400
|
+
def _post_stop_check_actions(self, retry_state: "RetryCallState") -> None:
|
|
401
|
+
if self.iter_state.stop_run_result:
|
|
321
402
|
if self.retry_error_callback:
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
if self.reraise:
|
|
325
|
-
raise retry_exc.reraise()
|
|
326
|
-
raise retry_exc from fut.exception()
|
|
403
|
+
self._add_action_func(self.retry_error_callback)
|
|
404
|
+
return
|
|
327
405
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
406
|
+
def exc_check(rs: "RetryCallState") -> None:
|
|
407
|
+
fut = t.cast(Future, rs.outcome)
|
|
408
|
+
retry_exc = self.retry_error_cls(fut)
|
|
409
|
+
if self.reraise:
|
|
410
|
+
raise retry_exc.reraise()
|
|
411
|
+
raise retry_exc from fut.exception()
|
|
412
|
+
|
|
413
|
+
self._add_action_func(exc_check)
|
|
414
|
+
return
|
|
415
|
+
|
|
416
|
+
def next_action(rs: "RetryCallState") -> None:
|
|
417
|
+
sleep = rs.upcoming_sleep
|
|
418
|
+
rs.next_action = RetryAction(sleep)
|
|
419
|
+
rs.idle_for += sleep
|
|
420
|
+
self.statistics["idle_for"] += sleep
|
|
421
|
+
self.statistics["attempt_number"] += 1
|
|
422
|
+
|
|
423
|
+
self._add_action_func(next_action)
|
|
336
424
|
|
|
337
425
|
if self.before_sleep is not None:
|
|
338
|
-
self.before_sleep
|
|
426
|
+
self._add_action_func(self.before_sleep)
|
|
339
427
|
|
|
340
|
-
|
|
428
|
+
self._add_action_func(lambda rs: DoSleep(rs.upcoming_sleep))
|
|
341
429
|
|
|
342
430
|
def __iter__(self) -> t.Generator[AttemptManager, None, None]:
|
|
343
431
|
self.begin()
|
|
@@ -391,7 +479,7 @@ class Retrying(BaseRetrying):
|
|
|
391
479
|
return do # type: ignore[no-any-return]
|
|
392
480
|
|
|
393
481
|
|
|
394
|
-
if sys.version_info
|
|
482
|
+
if sys.version_info >= (3, 9):
|
|
395
483
|
FutureGenericT = futures.Future[t.Any]
|
|
396
484
|
else:
|
|
397
485
|
FutureGenericT = futures.Future
|
|
@@ -410,7 +498,9 @@ class Future(FutureGenericT):
|
|
|
410
498
|
return self.exception() is not None
|
|
411
499
|
|
|
412
500
|
@classmethod
|
|
413
|
-
def construct(
|
|
501
|
+
def construct(
|
|
502
|
+
cls, attempt_number: int, value: t.Any, has_exception: bool
|
|
503
|
+
) -> "Future":
|
|
414
504
|
"""Construct a new Future object."""
|
|
415
505
|
fut = cls(attempt_number)
|
|
416
506
|
if has_exception:
|
|
@@ -451,6 +541,8 @@ class RetryCallState:
|
|
|
451
541
|
self.idle_for: float = 0.0
|
|
452
542
|
#: Next action as decided by the retry manager
|
|
453
543
|
self.next_action: t.Optional[RetryAction] = None
|
|
544
|
+
#: Next sleep time as decided by the retry manager.
|
|
545
|
+
self.upcoming_sleep: float = 0.0
|
|
454
546
|
|
|
455
547
|
@property
|
|
456
548
|
def seconds_since_start(self) -> t.Optional[float]:
|
|
@@ -471,7 +563,10 @@ class RetryCallState:
|
|
|
471
563
|
self.outcome, self.outcome_timestamp = fut, ts
|
|
472
564
|
|
|
473
565
|
def set_exception(
|
|
474
|
-
self,
|
|
566
|
+
self,
|
|
567
|
+
exc_info: t.Tuple[
|
|
568
|
+
t.Type[BaseException], BaseException, "types.TracebackType| None"
|
|
569
|
+
],
|
|
475
570
|
) -> None:
|
|
476
571
|
ts = time.monotonic()
|
|
477
572
|
fut = Future(self.attempt_number)
|
|
@@ -493,8 +588,7 @@ class RetryCallState:
|
|
|
493
588
|
|
|
494
589
|
|
|
495
590
|
@t.overload
|
|
496
|
-
def retry(func: WrappedFn) -> WrappedFn:
|
|
497
|
-
...
|
|
591
|
+
def retry(func: WrappedFn) -> WrappedFn: ...
|
|
498
592
|
|
|
499
593
|
|
|
500
594
|
@t.overload
|
|
@@ -509,8 +603,7 @@ def retry(
|
|
|
509
603
|
reraise: bool = False,
|
|
510
604
|
retry_error_cls: t.Type["RetryError"] = RetryError,
|
|
511
605
|
retry_error_callback: t.Optional[t.Callable[["RetryCallState"], t.Any]] = None,
|
|
512
|
-
) -> t.Callable[[WrappedFn], WrappedFn]:
|
|
513
|
-
...
|
|
606
|
+
) -> t.Callable[[WrappedFn], WrappedFn]: ...
|
|
514
607
|
|
|
515
608
|
|
|
516
609
|
def retry(*dargs: t.Any, **dkw: t.Any) -> t.Any:
|
|
@@ -533,7 +626,11 @@ def retry(*dargs: t.Any, **dkw: t.Any) -> t.Any:
|
|
|
533
626
|
r: "BaseRetrying"
|
|
534
627
|
if iscoroutinefunction(f):
|
|
535
628
|
r = AsyncRetrying(*dargs, **dkw)
|
|
536
|
-
elif
|
|
629
|
+
elif (
|
|
630
|
+
tornado
|
|
631
|
+
and hasattr(tornado.gen, "is_coroutine_function")
|
|
632
|
+
and tornado.gen.is_coroutine_function(f)
|
|
633
|
+
):
|
|
537
634
|
r = TornadoRetrying(*dargs, **dkw)
|
|
538
635
|
else:
|
|
539
636
|
r = Retrying(*dargs, **dkw)
|
|
@@ -568,6 +665,7 @@ __all__ = [
|
|
|
568
665
|
"sleep_using_event",
|
|
569
666
|
"stop_after_attempt",
|
|
570
667
|
"stop_after_delay",
|
|
668
|
+
"stop_before_delay",
|
|
571
669
|
"stop_all",
|
|
572
670
|
"stop_any",
|
|
573
671
|
"stop_never",
|
tenacity/_asyncio.py
CHANGED
|
@@ -18,22 +18,33 @@
|
|
|
18
18
|
import functools
|
|
19
19
|
import sys
|
|
20
20
|
import typing as t
|
|
21
|
-
from asyncio import sleep
|
|
22
21
|
|
|
23
22
|
from tenacity import AttemptManager
|
|
24
23
|
from tenacity import BaseRetrying
|
|
25
24
|
from tenacity import DoAttempt
|
|
26
25
|
from tenacity import DoSleep
|
|
27
26
|
from tenacity import RetryCallState
|
|
27
|
+
from tenacity import _utils
|
|
28
28
|
|
|
29
29
|
WrappedFnReturnT = t.TypeVar("WrappedFnReturnT")
|
|
30
30
|
WrappedFn = t.TypeVar("WrappedFn", bound=t.Callable[..., t.Awaitable[t.Any]])
|
|
31
31
|
|
|
32
32
|
|
|
33
|
+
def asyncio_sleep(duration: float) -> t.Awaitable[None]:
|
|
34
|
+
# Lazy import asyncio as it's expensive (responsible for 25-50% of total import overhead).
|
|
35
|
+
import asyncio
|
|
36
|
+
|
|
37
|
+
return asyncio.sleep(duration)
|
|
38
|
+
|
|
39
|
+
|
|
33
40
|
class AsyncRetrying(BaseRetrying):
|
|
34
41
|
sleep: t.Callable[[float], t.Awaitable[t.Any]]
|
|
35
42
|
|
|
36
|
-
def __init__(
|
|
43
|
+
def __init__(
|
|
44
|
+
self,
|
|
45
|
+
sleep: t.Callable[[float], t.Awaitable[t.Any]] = asyncio_sleep,
|
|
46
|
+
**kwargs: t.Any,
|
|
47
|
+
) -> None:
|
|
37
48
|
super().__init__(**kwargs)
|
|
38
49
|
self.sleep = sleep
|
|
39
50
|
|
|
@@ -44,7 +55,7 @@ class AsyncRetrying(BaseRetrying):
|
|
|
44
55
|
|
|
45
56
|
retry_state = RetryCallState(retry_object=self, fn=fn, args=args, kwargs=kwargs)
|
|
46
57
|
while True:
|
|
47
|
-
do = self.iter(retry_state=retry_state)
|
|
58
|
+
do = await self.iter(retry_state=retry_state)
|
|
48
59
|
if isinstance(do, DoAttempt):
|
|
49
60
|
try:
|
|
50
61
|
result = await fn(*args, **kwargs)
|
|
@@ -58,6 +69,47 @@ class AsyncRetrying(BaseRetrying):
|
|
|
58
69
|
else:
|
|
59
70
|
return do # type: ignore[no-any-return]
|
|
60
71
|
|
|
72
|
+
@classmethod
|
|
73
|
+
def _wrap_action_func(cls, fn: t.Callable[..., t.Any]) -> t.Callable[..., t.Any]:
|
|
74
|
+
if _utils.is_coroutine_callable(fn):
|
|
75
|
+
return fn
|
|
76
|
+
|
|
77
|
+
async def inner(*args: t.Any, **kwargs: t.Any) -> t.Any:
|
|
78
|
+
return fn(*args, **kwargs)
|
|
79
|
+
|
|
80
|
+
return inner
|
|
81
|
+
|
|
82
|
+
def _add_action_func(self, fn: t.Callable[..., t.Any]) -> None:
|
|
83
|
+
self.iter_state.actions.append(self._wrap_action_func(fn))
|
|
84
|
+
|
|
85
|
+
async def _run_retry(self, retry_state: "RetryCallState") -> None: # type: ignore[override]
|
|
86
|
+
self.iter_state.retry_run_result = await self._wrap_action_func(self.retry)(
|
|
87
|
+
retry_state
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
async def _run_wait(self, retry_state: "RetryCallState") -> None: # type: ignore[override]
|
|
91
|
+
if self.wait:
|
|
92
|
+
sleep = await self._wrap_action_func(self.wait)(retry_state)
|
|
93
|
+
else:
|
|
94
|
+
sleep = 0.0
|
|
95
|
+
|
|
96
|
+
retry_state.upcoming_sleep = sleep
|
|
97
|
+
|
|
98
|
+
async def _run_stop(self, retry_state: "RetryCallState") -> None: # type: ignore[override]
|
|
99
|
+
self.statistics["delay_since_first_attempt"] = retry_state.seconds_since_start
|
|
100
|
+
self.iter_state.stop_run_result = await self._wrap_action_func(self.stop)(
|
|
101
|
+
retry_state
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
async def iter(
|
|
105
|
+
self, retry_state: "RetryCallState"
|
|
106
|
+
) -> t.Union[DoAttempt, DoSleep, t.Any]: # noqa: A003
|
|
107
|
+
self._begin_iter(retry_state)
|
|
108
|
+
result = None
|
|
109
|
+
for action in self.iter_state.actions:
|
|
110
|
+
result = await action(retry_state)
|
|
111
|
+
return result
|
|
112
|
+
|
|
61
113
|
def __iter__(self) -> t.Generator[AttemptManager, None, None]:
|
|
62
114
|
raise TypeError("AsyncRetrying object is not iterable")
|
|
63
115
|
|
|
@@ -68,7 +120,7 @@ class AsyncRetrying(BaseRetrying):
|
|
|
68
120
|
|
|
69
121
|
async def __anext__(self) -> AttemptManager:
|
|
70
122
|
while True:
|
|
71
|
-
do = self.iter(retry_state=self._retry_state)
|
|
123
|
+
do = await self.iter(retry_state=self._retry_state)
|
|
72
124
|
if do is None:
|
|
73
125
|
raise StopAsyncIteration
|
|
74
126
|
elif isinstance(do, DoAttempt):
|
|
@@ -83,7 +135,9 @@ class AsyncRetrying(BaseRetrying):
|
|
|
83
135
|
fn = super().wraps(fn)
|
|
84
136
|
# Ensure wrapper is recognized as a coroutine function.
|
|
85
137
|
|
|
86
|
-
@functools.wraps(
|
|
138
|
+
@functools.wraps(
|
|
139
|
+
fn, functools.WRAPPER_ASSIGNMENTS + ("__defaults__", "__kwdefaults__")
|
|
140
|
+
)
|
|
87
141
|
async def async_wrapped(*args: t.Any, **kwargs: t.Any) -> t.Any:
|
|
88
142
|
return await fn(*args, **kwargs)
|
|
89
143
|
|
tenacity/_utils.py
CHANGED
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
14
|
# See the License for the specific language governing permissions and
|
|
15
15
|
# limitations under the License.
|
|
16
|
-
|
|
16
|
+
import functools
|
|
17
|
+
import inspect
|
|
17
18
|
import sys
|
|
18
19
|
import typing
|
|
19
20
|
from datetime import timedelta
|
|
@@ -73,4 +74,16 @@ time_unit_type = typing.Union[int, float, timedelta]
|
|
|
73
74
|
|
|
74
75
|
|
|
75
76
|
def to_seconds(time_unit: time_unit_type) -> float:
|
|
76
|
-
return float(
|
|
77
|
+
return float(
|
|
78
|
+
time_unit.total_seconds() if isinstance(time_unit, timedelta) else time_unit
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def is_coroutine_callable(call: typing.Callable[..., typing.Any]) -> bool:
|
|
83
|
+
if inspect.isclass(call):
|
|
84
|
+
return False
|
|
85
|
+
if inspect.iscoroutinefunction(call):
|
|
86
|
+
return True
|
|
87
|
+
partial_call = isinstance(call, functools.partial) and call.func
|
|
88
|
+
dunder_call = partial_call or getattr(call, "__call__", None)
|
|
89
|
+
return inspect.iscoroutinefunction(dunder_call)
|
tenacity/before.py
CHANGED
|
@@ -28,7 +28,9 @@ def before_nothing(retry_state: "RetryCallState") -> None:
|
|
|
28
28
|
"""Before call strategy that does nothing."""
|
|
29
29
|
|
|
30
30
|
|
|
31
|
-
def before_log(
|
|
31
|
+
def before_log(
|
|
32
|
+
logger: "logging.Logger", log_level: int
|
|
33
|
+
) -> typing.Callable[["RetryCallState"], None]:
|
|
32
34
|
"""Before call strategy that logs to some logger the attempt."""
|
|
33
35
|
|
|
34
36
|
def log_it(retry_state: "RetryCallState") -> None:
|
tenacity/before_sleep.py
CHANGED
|
@@ -64,7 +64,8 @@ def before_sleep_log(
|
|
|
64
64
|
|
|
65
65
|
logger.log(
|
|
66
66
|
log_level,
|
|
67
|
-
f"Retrying {fn_name} "
|
|
67
|
+
f"Retrying {fn_name} "
|
|
68
|
+
f"in {retry_state.next_action.sleep} seconds as it {verb} {value}.",
|
|
68
69
|
exc_info=local_exc_info,
|
|
69
70
|
)
|
|
70
71
|
|
tenacity/retry.py
CHANGED
|
@@ -204,7 +204,9 @@ class retry_if_exception_message(retry_if_exception):
|
|
|
204
204
|
match: typing.Optional[str] = None,
|
|
205
205
|
) -> None:
|
|
206
206
|
if message and match:
|
|
207
|
-
raise TypeError(
|
|
207
|
+
raise TypeError(
|
|
208
|
+
f"{self.__class__.__name__}() takes either 'message' or 'match', not both"
|
|
209
|
+
)
|
|
208
210
|
|
|
209
211
|
# set predicate
|
|
210
212
|
if message:
|
|
@@ -221,7 +223,9 @@ class retry_if_exception_message(retry_if_exception):
|
|
|
221
223
|
|
|
222
224
|
predicate = match_fnc
|
|
223
225
|
else:
|
|
224
|
-
raise TypeError(
|
|
226
|
+
raise TypeError(
|
|
227
|
+
f"{self.__class__.__name__}() missing 1 required argument 'message' or 'match'"
|
|
228
|
+
)
|
|
225
229
|
|
|
226
230
|
super().__init__(predicate)
|
|
227
231
|
|
tenacity/stop.py
CHANGED
|
@@ -92,7 +92,14 @@ class stop_after_attempt(stop_base):
|
|
|
92
92
|
|
|
93
93
|
|
|
94
94
|
class stop_after_delay(stop_base):
|
|
95
|
-
"""
|
|
95
|
+
"""
|
|
96
|
+
Stop when the time from the first attempt >= limit.
|
|
97
|
+
|
|
98
|
+
Note: `max_delay` will be exceeded, so when used with a `wait`, the actual total delay will be greater
|
|
99
|
+
than `max_delay` by some of the final sleep period before `max_delay` is exceeded.
|
|
100
|
+
|
|
101
|
+
If you need stricter timing with waits, consider `stop_before_delay` instead.
|
|
102
|
+
"""
|
|
96
103
|
|
|
97
104
|
def __init__(self, max_delay: _utils.time_unit_type) -> None:
|
|
98
105
|
self.max_delay = _utils.to_seconds(max_delay)
|
|
@@ -101,3 +108,23 @@ class stop_after_delay(stop_base):
|
|
|
101
108
|
if retry_state.seconds_since_start is None:
|
|
102
109
|
raise RuntimeError("__call__() called but seconds_since_start is not set")
|
|
103
110
|
return retry_state.seconds_since_start >= self.max_delay
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class stop_before_delay(stop_base):
|
|
114
|
+
"""
|
|
115
|
+
Stop right before the next attempt would take place after the time from the first attempt >= limit.
|
|
116
|
+
|
|
117
|
+
Most useful when you are using with a `wait` function like wait_random_exponential, but need to make
|
|
118
|
+
sure that the max_delay is not exceeded.
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
def __init__(self, max_delay: _utils.time_unit_type) -> None:
|
|
122
|
+
self.max_delay = _utils.to_seconds(max_delay)
|
|
123
|
+
|
|
124
|
+
def __call__(self, retry_state: "RetryCallState") -> bool:
|
|
125
|
+
if retry_state.seconds_since_start is None:
|
|
126
|
+
raise RuntimeError("__call__() called but seconds_since_start is not set")
|
|
127
|
+
return (
|
|
128
|
+
retry_state.seconds_since_start + retry_state.upcoming_sleep
|
|
129
|
+
>= self.max_delay
|
|
130
|
+
)
|
tenacity/tornadoweb.py
CHANGED
|
@@ -29,7 +29,11 @@ _RetValT = typing.TypeVar("_RetValT")
|
|
|
29
29
|
|
|
30
30
|
|
|
31
31
|
class TornadoRetrying(BaseRetrying):
|
|
32
|
-
def __init__(
|
|
32
|
+
def __init__(
|
|
33
|
+
self,
|
|
34
|
+
sleep: "typing.Callable[[float], Future[None]]" = gen.sleep,
|
|
35
|
+
**kwargs: typing.Any,
|
|
36
|
+
) -> None:
|
|
33
37
|
super().__init__(**kwargs)
|
|
34
38
|
self.sleep = sleep
|
|
35
39
|
|
tenacity/wait.py
CHANGED
|
@@ -41,7 +41,9 @@ class wait_base(abc.ABC):
|
|
|
41
41
|
return self.__add__(other)
|
|
42
42
|
|
|
43
43
|
|
|
44
|
-
WaitBaseT = typing.Union[
|
|
44
|
+
WaitBaseT = typing.Union[
|
|
45
|
+
wait_base, typing.Callable[["RetryCallState"], typing.Union[float, int]]
|
|
46
|
+
]
|
|
45
47
|
|
|
46
48
|
|
|
47
49
|
class wait_fixed(wait_base):
|
|
@@ -64,12 +66,16 @@ class wait_none(wait_fixed):
|
|
|
64
66
|
class wait_random(wait_base):
|
|
65
67
|
"""Wait strategy that waits a random amount of time between min/max."""
|
|
66
68
|
|
|
67
|
-
def __init__(
|
|
69
|
+
def __init__(
|
|
70
|
+
self, min: _utils.time_unit_type = 0, max: _utils.time_unit_type = 1
|
|
71
|
+
) -> None: # noqa
|
|
68
72
|
self.wait_random_min = _utils.to_seconds(min)
|
|
69
73
|
self.wait_random_max = _utils.to_seconds(max)
|
|
70
74
|
|
|
71
75
|
def __call__(self, retry_state: "RetryCallState") -> float:
|
|
72
|
-
return self.wait_random_min + (
|
|
76
|
+
return self.wait_random_min + (
|
|
77
|
+
random.random() * (self.wait_random_max - self.wait_random_min)
|
|
78
|
+
)
|
|
73
79
|
|
|
74
80
|
|
|
75
81
|
class wait_combine(wait_base):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: tenacity
|
|
3
|
-
Version: 8.
|
|
3
|
+
Version: 8.3.0
|
|
4
4
|
Summary: Retry code until it succeeds
|
|
5
5
|
Home-page: https://github.com/jd/tenacity
|
|
6
6
|
Author: Julien Danjou
|
|
@@ -11,17 +11,20 @@ Classifier: License :: OSI Approved :: Apache Software License
|
|
|
11
11
|
Classifier: Programming Language :: Python
|
|
12
12
|
Classifier: Programming Language :: Python :: 3
|
|
13
13
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
14
|
-
Classifier: Programming Language :: Python :: 3.7
|
|
15
14
|
Classifier: Programming Language :: Python :: 3.8
|
|
16
15
|
Classifier: Programming Language :: Python :: 3.9
|
|
17
16
|
Classifier: Programming Language :: Python :: 3.10
|
|
18
17
|
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
19
|
Classifier: Topic :: Utilities
|
|
20
|
-
Requires-Python: >=3.
|
|
20
|
+
Requires-Python: >=3.8
|
|
21
21
|
License-File: LICENSE
|
|
22
22
|
Provides-Extra: doc
|
|
23
23
|
Requires-Dist: reno ; extra == 'doc'
|
|
24
24
|
Requires-Dist: sphinx ; extra == 'doc'
|
|
25
|
-
|
|
25
|
+
Provides-Extra: test
|
|
26
|
+
Requires-Dist: pytest ; extra == 'test'
|
|
27
|
+
Requires-Dist: tornado >=4.5 ; extra == 'test'
|
|
28
|
+
Requires-Dist: typeguard ; extra == 'test'
|
|
26
29
|
|
|
27
30
|
Tenacity is a general-purpose retrying library to simplify the task of adding retry behavior to just about anything.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
tenacity/__init__.py,sha256=pXkuWY13-Ual7lJs3t2GfFBaFY8Epq6ipLxA5TA4c04,23415
|
|
2
|
+
tenacity/_asyncio.py,sha256=AcnW-qYS1Lf9d7P_Y9FMd7hyh6bJlQU1C073sIbZ6U4,5369
|
|
3
|
+
tenacity/_utils.py,sha256=GGNfSHJRrfph7kPySn-2xnY_dVUopdl6LUZdXRPuGVg,2601
|
|
4
|
+
tenacity/after.py,sha256=NR4rGyslG7xF1hDJZb2Wf8wVApafX0HZwz2nFVLvaqE,1658
|
|
5
|
+
tenacity/before.py,sha256=7zDTpZ3b6rkY9sOdS-qbpjBgSjVr3xBqcIqdYAh9ZKM,1544
|
|
6
|
+
tenacity/before_sleep.py,sha256=upKssiY5poOO3Bv0amADZA-a5CrY5tnb4_97s8_88SM,2360
|
|
7
|
+
tenacity/nap.py,sha256=fRWvnz1aIzbIq9Ap3gAkAZgDH6oo5zxMrU6ZOVByq0I,1383
|
|
8
|
+
tenacity/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
|
+
tenacity/retry.py,sha256=_Yb081RabODdc6FbI2Ipg8Cno4SpHKGao0i2jDvXPJQ,8794
|
|
10
|
+
tenacity/stop.py,sha256=wQuwGfCLw8OH1C3x0G9lH9xtJCyhgviePQ40HRAUg54,4113
|
|
11
|
+
tenacity/tornadoweb.py,sha256=vS1ONfPYoGzPx1asQaVbfoo6D9tPIzSysJipm61Yqw8,2125
|
|
12
|
+
tenacity/wait.py,sha256=Q9XoZCtFra53aQOyfABpvRDuUeB-NpUUXImHsUiRQI0,8042
|
|
13
|
+
tenacity-8.3.0.dist-info/LICENSE,sha256=WNHhf_5RCaeuKWyq_K39vmp9F28LxKsB4SpomwSZ2L0,11357
|
|
14
|
+
tenacity-8.3.0.dist-info/METADATA,sha256=EkN7WPJ0qwEErJNURA8THEGpBSKbYrpT5SEZMQkH7yk,1155
|
|
15
|
+
tenacity-8.3.0.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
|
16
|
+
tenacity-8.3.0.dist-info/top_level.txt,sha256=Zf8AOZMN7hr1EEcUo9U5KzXsM4TOC1pBZ22D8913JYs,9
|
|
17
|
+
tenacity-8.3.0.dist-info/RECORD,,
|
tenacity-8.2.3.dist-info/RECORD
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
tenacity/__init__.py,sha256=gToezN7ZiWd_GjOzGmIgjOv_H7r-D7XG0HpxAaJjxUw,20214
|
|
2
|
-
tenacity/_asyncio.py,sha256=VLYBFVbYGPPegt-DstLBvpJ7b5QBGjzZ2RT_7z4HetE,3491
|
|
3
|
-
tenacity/_utils.py,sha256=ubs6a7sxj3JDNRKWCyCU2j5r1CB7rgyONgZzYZq6D_4,2179
|
|
4
|
-
tenacity/after.py,sha256=NR4rGyslG7xF1hDJZb2Wf8wVApafX0HZwz2nFVLvaqE,1658
|
|
5
|
-
tenacity/before.py,sha256=Oqrd_9zVSuhd-OVN5WJeSq7t0PwT13lF2UPFxtLS_uM,1538
|
|
6
|
-
tenacity/before_sleep.py,sha256=nUotitueDNDDbxM74rXMIEngcHUze1JpWXASgBXyK8A,2348
|
|
7
|
-
tenacity/nap.py,sha256=fRWvnz1aIzbIq9Ap3gAkAZgDH6oo5zxMrU6ZOVByq0I,1383
|
|
8
|
-
tenacity/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
|
-
tenacity/retry.py,sha256=r8MbifrSsqhpdnv8r4knkQINdlJUpieqna_8MnHtm68,8734
|
|
10
|
-
tenacity/stop.py,sha256=Ecxg66eJwa0dhuJfLaH2g7jKd7f3KMrPRjz24bX9ZMY,3062
|
|
11
|
-
tenacity/tornadoweb.py,sha256=arKDIDu61nQDj5vIbQoR3Uo-m8vGOjd5ypGdOHZcTVk,2094
|
|
12
|
-
tenacity/wait.py,sha256=KlQW4UQBOdQjDQWa5aqlO67Ou73y72N_i85CWE1BOkQ,8000
|
|
13
|
-
tenacity-8.2.3.dist-info/LICENSE,sha256=WNHhf_5RCaeuKWyq_K39vmp9F28LxKsB4SpomwSZ2L0,11357
|
|
14
|
-
tenacity-8.2.3.dist-info/METADATA,sha256=g2xkakn1Zzo_4cTLL2dLSxdQ-xqBr3koN7l9toDVQLk,1049
|
|
15
|
-
tenacity-8.2.3.dist-info/WHEEL,sha256=5sUXSg9e4bi7lTLOHcm6QEYwO5TIF1TNbTSVFVjcJcc,92
|
|
16
|
-
tenacity-8.2.3.dist-info/top_level.txt,sha256=Zf8AOZMN7hr1EEcUo9U5KzXsM4TOC1pBZ22D8913JYs,9
|
|
17
|
-
tenacity-8.2.3.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|