tenacity 8.2.3__py3-none-any.whl → 8.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.
- tenacity/__init__.py +155 -47
- tenacity/_utils.py +27 -2
- tenacity/before.py +3 -1
- tenacity/before_sleep.py +2 -1
- tenacity/retry.py +14 -4
- tenacity/stop.py +28 -1
- tenacity/tornadoweb.py +5 -1
- tenacity/wait.py +9 -3
- {tenacity-8.2.3.dist-info → tenacity-8.4.0.dist-info}/METADATA +7 -4
- tenacity-8.4.0.dist-info/RECORD +16 -0
- {tenacity-8.2.3.dist-info → tenacity-8.4.0.dist-info}/WHEEL +1 -1
- tenacity/_asyncio.py +0 -94
- tenacity-8.2.3.dist-info/RECORD +0 -17
- {tenacity-8.2.3.dist-info → tenacity-8.4.0.dist-info}/LICENSE +0 -0
- {tenacity-8.2.3.dist-info → tenacity-8.4.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
|
|
@@ -25,7 +24,8 @@ import typing as t
|
|
|
25
24
|
import warnings
|
|
26
25
|
from abc import ABC, abstractmethod
|
|
27
26
|
from concurrent import futures
|
|
28
|
-
|
|
27
|
+
|
|
28
|
+
from . import _utils
|
|
29
29
|
|
|
30
30
|
# Import all built-in retry strategies for easier usage.
|
|
31
31
|
from .retry import retry_base # noqa
|
|
@@ -50,6 +50,7 @@ from .nap import sleep_using_event # noqa
|
|
|
50
50
|
# Import all built-in stop strategies for easier usage.
|
|
51
51
|
from .stop import stop_after_attempt # noqa
|
|
52
52
|
from .stop import stop_after_delay # noqa
|
|
53
|
+
from .stop import stop_before_delay # noqa
|
|
53
54
|
from .stop import stop_all # noqa
|
|
54
55
|
from .stop import stop_any # noqa
|
|
55
56
|
from .stop import stop_never # noqa
|
|
@@ -87,6 +88,7 @@ except ImportError:
|
|
|
87
88
|
if t.TYPE_CHECKING:
|
|
88
89
|
import types
|
|
89
90
|
|
|
91
|
+
from . import asyncio as tasyncio
|
|
90
92
|
from .retry import RetryBaseT
|
|
91
93
|
from .stop import StopBaseT
|
|
92
94
|
from .wait import WaitBaseT
|
|
@@ -96,6 +98,29 @@ WrappedFnReturnT = t.TypeVar("WrappedFnReturnT")
|
|
|
96
98
|
WrappedFn = t.TypeVar("WrappedFn", bound=t.Callable[..., t.Any])
|
|
97
99
|
|
|
98
100
|
|
|
101
|
+
dataclass_kwargs = {}
|
|
102
|
+
if sys.version_info >= (3, 10):
|
|
103
|
+
dataclass_kwargs.update({"slots": True})
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@dataclasses.dataclass(**dataclass_kwargs)
|
|
107
|
+
class IterState:
|
|
108
|
+
actions: t.List[t.Callable[["RetryCallState"], t.Any]] = dataclasses.field(
|
|
109
|
+
default_factory=list
|
|
110
|
+
)
|
|
111
|
+
retry_run_result: bool = False
|
|
112
|
+
delay_since_first_attempt: int = 0
|
|
113
|
+
stop_run_result: bool = False
|
|
114
|
+
is_explicit_retry: bool = False
|
|
115
|
+
|
|
116
|
+
def reset(self) -> None:
|
|
117
|
+
self.actions = []
|
|
118
|
+
self.retry_run_result = False
|
|
119
|
+
self.delay_since_first_attempt = 0
|
|
120
|
+
self.stop_run_result = False
|
|
121
|
+
self.is_explicit_retry = False
|
|
122
|
+
|
|
123
|
+
|
|
99
124
|
class TryAgain(Exception):
|
|
100
125
|
"""Always retry the executed function when raised."""
|
|
101
126
|
|
|
@@ -124,7 +149,9 @@ class BaseAction:
|
|
|
124
149
|
NAME: t.Optional[str] = None
|
|
125
150
|
|
|
126
151
|
def __repr__(self) -> str:
|
|
127
|
-
state_str = ", ".join(
|
|
152
|
+
state_str = ", ".join(
|
|
153
|
+
f"{field}={getattr(self, field)!r}" for field in self.REPR_FIELDS
|
|
154
|
+
)
|
|
128
155
|
return f"{self.__class__.__name__}({state_str})"
|
|
129
156
|
|
|
130
157
|
def __str__(self) -> str:
|
|
@@ -220,10 +247,14 @@ class BaseRetrying(ABC):
|
|
|
220
247
|
retry: t.Union[retry_base, object] = _unset,
|
|
221
248
|
before: t.Union[t.Callable[["RetryCallState"], None], object] = _unset,
|
|
222
249
|
after: t.Union[t.Callable[["RetryCallState"], None], object] = _unset,
|
|
223
|
-
before_sleep: t.Union[
|
|
250
|
+
before_sleep: t.Union[
|
|
251
|
+
t.Optional[t.Callable[["RetryCallState"], None]], object
|
|
252
|
+
] = _unset,
|
|
224
253
|
reraise: t.Union[bool, object] = _unset,
|
|
225
254
|
retry_error_cls: t.Union[t.Type[RetryError], object] = _unset,
|
|
226
|
-
retry_error_callback: t.Union[
|
|
255
|
+
retry_error_callback: t.Union[
|
|
256
|
+
t.Optional[t.Callable[["RetryCallState"], t.Any]], object
|
|
257
|
+
] = _unset,
|
|
227
258
|
) -> "BaseRetrying":
|
|
228
259
|
"""Copy this object with some parameters changed if needed."""
|
|
229
260
|
return self.__class__(
|
|
@@ -236,7 +267,9 @@ class BaseRetrying(ABC):
|
|
|
236
267
|
before_sleep=_first_set(before_sleep, self.before_sleep),
|
|
237
268
|
reraise=_first_set(reraise, self.reraise),
|
|
238
269
|
retry_error_cls=_first_set(retry_error_cls, self.retry_error_cls),
|
|
239
|
-
retry_error_callback=_first_set(
|
|
270
|
+
retry_error_callback=_first_set(
|
|
271
|
+
retry_error_callback, self.retry_error_callback
|
|
272
|
+
),
|
|
240
273
|
)
|
|
241
274
|
|
|
242
275
|
def __repr__(self) -> str:
|
|
@@ -278,13 +311,23 @@ class BaseRetrying(ABC):
|
|
|
278
311
|
self._local.statistics = t.cast(t.Dict[str, t.Any], {})
|
|
279
312
|
return self._local.statistics
|
|
280
313
|
|
|
314
|
+
@property
|
|
315
|
+
def iter_state(self) -> IterState:
|
|
316
|
+
try:
|
|
317
|
+
return self._local.iter_state # type: ignore[no-any-return]
|
|
318
|
+
except AttributeError:
|
|
319
|
+
self._local.iter_state = IterState()
|
|
320
|
+
return self._local.iter_state
|
|
321
|
+
|
|
281
322
|
def wraps(self, f: WrappedFn) -> WrappedFn:
|
|
282
323
|
"""Wrap a function for retrying.
|
|
283
324
|
|
|
284
325
|
:param f: A function to wraps for retrying.
|
|
285
326
|
"""
|
|
286
327
|
|
|
287
|
-
@functools.wraps(
|
|
328
|
+
@functools.wraps(
|
|
329
|
+
f, functools.WRAPPER_ASSIGNMENTS + ("__defaults__", "__kwdefaults__")
|
|
330
|
+
)
|
|
288
331
|
def wrapped_f(*args: t.Any, **kw: t.Any) -> t.Any:
|
|
289
332
|
return self(f, *args, **kw)
|
|
290
333
|
|
|
@@ -302,42 +345,89 @@ class BaseRetrying(ABC):
|
|
|
302
345
|
self.statistics["attempt_number"] = 1
|
|
303
346
|
self.statistics["idle_for"] = 0
|
|
304
347
|
|
|
348
|
+
def _add_action_func(self, fn: t.Callable[..., t.Any]) -> None:
|
|
349
|
+
self.iter_state.actions.append(fn)
|
|
350
|
+
|
|
351
|
+
def _run_retry(self, retry_state: "RetryCallState") -> None:
|
|
352
|
+
self.iter_state.retry_run_result = self.retry(retry_state)
|
|
353
|
+
|
|
354
|
+
def _run_wait(self, retry_state: "RetryCallState") -> None:
|
|
355
|
+
if self.wait:
|
|
356
|
+
sleep = self.wait(retry_state)
|
|
357
|
+
else:
|
|
358
|
+
sleep = 0.0
|
|
359
|
+
|
|
360
|
+
retry_state.upcoming_sleep = sleep
|
|
361
|
+
|
|
362
|
+
def _run_stop(self, retry_state: "RetryCallState") -> None:
|
|
363
|
+
self.statistics["delay_since_first_attempt"] = retry_state.seconds_since_start
|
|
364
|
+
self.iter_state.stop_run_result = self.stop(retry_state)
|
|
365
|
+
|
|
305
366
|
def iter(self, retry_state: "RetryCallState") -> t.Union[DoAttempt, DoSleep, t.Any]: # noqa
|
|
367
|
+
self._begin_iter(retry_state)
|
|
368
|
+
result = None
|
|
369
|
+
for action in self.iter_state.actions:
|
|
370
|
+
result = action(retry_state)
|
|
371
|
+
return result
|
|
372
|
+
|
|
373
|
+
def _begin_iter(self, retry_state: "RetryCallState") -> None: # noqa
|
|
374
|
+
self.iter_state.reset()
|
|
375
|
+
|
|
306
376
|
fut = retry_state.outcome
|
|
307
377
|
if fut is None:
|
|
308
378
|
if self.before is not None:
|
|
309
|
-
self.before
|
|
310
|
-
|
|
379
|
+
self._add_action_func(self.before)
|
|
380
|
+
self._add_action_func(lambda rs: DoAttempt())
|
|
381
|
+
return
|
|
311
382
|
|
|
312
|
-
is_explicit_retry = fut.failed and isinstance(
|
|
313
|
-
|
|
314
|
-
|
|
383
|
+
self.iter_state.is_explicit_retry = fut.failed and isinstance(
|
|
384
|
+
fut.exception(), TryAgain
|
|
385
|
+
)
|
|
386
|
+
if not self.iter_state.is_explicit_retry:
|
|
387
|
+
self._add_action_func(self._run_retry)
|
|
388
|
+
self._add_action_func(self._post_retry_check_actions)
|
|
389
|
+
|
|
390
|
+
def _post_retry_check_actions(self, retry_state: "RetryCallState") -> None:
|
|
391
|
+
if not (self.iter_state.is_explicit_retry or self.iter_state.retry_run_result):
|
|
392
|
+
self._add_action_func(lambda rs: rs.outcome.result())
|
|
393
|
+
return
|
|
315
394
|
|
|
316
395
|
if self.after is not None:
|
|
317
|
-
self.after
|
|
396
|
+
self._add_action_func(self.after)
|
|
318
397
|
|
|
319
|
-
self.
|
|
320
|
-
|
|
398
|
+
self._add_action_func(self._run_wait)
|
|
399
|
+
self._add_action_func(self._run_stop)
|
|
400
|
+
self._add_action_func(self._post_stop_check_actions)
|
|
401
|
+
|
|
402
|
+
def _post_stop_check_actions(self, retry_state: "RetryCallState") -> None:
|
|
403
|
+
if self.iter_state.stop_run_result:
|
|
321
404
|
if self.retry_error_callback:
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
if self.reraise:
|
|
325
|
-
raise retry_exc.reraise()
|
|
326
|
-
raise retry_exc from fut.exception()
|
|
405
|
+
self._add_action_func(self.retry_error_callback)
|
|
406
|
+
return
|
|
327
407
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
408
|
+
def exc_check(rs: "RetryCallState") -> None:
|
|
409
|
+
fut = t.cast(Future, rs.outcome)
|
|
410
|
+
retry_exc = self.retry_error_cls(fut)
|
|
411
|
+
if self.reraise:
|
|
412
|
+
raise retry_exc.reraise()
|
|
413
|
+
raise retry_exc from fut.exception()
|
|
414
|
+
|
|
415
|
+
self._add_action_func(exc_check)
|
|
416
|
+
return
|
|
417
|
+
|
|
418
|
+
def next_action(rs: "RetryCallState") -> None:
|
|
419
|
+
sleep = rs.upcoming_sleep
|
|
420
|
+
rs.next_action = RetryAction(sleep)
|
|
421
|
+
rs.idle_for += sleep
|
|
422
|
+
self.statistics["idle_for"] += sleep
|
|
423
|
+
self.statistics["attempt_number"] += 1
|
|
424
|
+
|
|
425
|
+
self._add_action_func(next_action)
|
|
336
426
|
|
|
337
427
|
if self.before_sleep is not None:
|
|
338
|
-
self.before_sleep
|
|
428
|
+
self._add_action_func(self.before_sleep)
|
|
339
429
|
|
|
340
|
-
|
|
430
|
+
self._add_action_func(lambda rs: DoSleep(rs.upcoming_sleep))
|
|
341
431
|
|
|
342
432
|
def __iter__(self) -> t.Generator[AttemptManager, None, None]:
|
|
343
433
|
self.begin()
|
|
@@ -391,7 +481,7 @@ class Retrying(BaseRetrying):
|
|
|
391
481
|
return do # type: ignore[no-any-return]
|
|
392
482
|
|
|
393
483
|
|
|
394
|
-
if sys.version_info
|
|
484
|
+
if sys.version_info >= (3, 9):
|
|
395
485
|
FutureGenericT = futures.Future[t.Any]
|
|
396
486
|
else:
|
|
397
487
|
FutureGenericT = futures.Future
|
|
@@ -410,7 +500,9 @@ class Future(FutureGenericT):
|
|
|
410
500
|
return self.exception() is not None
|
|
411
501
|
|
|
412
502
|
@classmethod
|
|
413
|
-
def construct(
|
|
503
|
+
def construct(
|
|
504
|
+
cls, attempt_number: int, value: t.Any, has_exception: bool
|
|
505
|
+
) -> "Future":
|
|
414
506
|
"""Construct a new Future object."""
|
|
415
507
|
fut = cls(attempt_number)
|
|
416
508
|
if has_exception:
|
|
@@ -451,6 +543,8 @@ class RetryCallState:
|
|
|
451
543
|
self.idle_for: float = 0.0
|
|
452
544
|
#: Next action as decided by the retry manager
|
|
453
545
|
self.next_action: t.Optional[RetryAction] = None
|
|
546
|
+
#: Next sleep time as decided by the retry manager.
|
|
547
|
+
self.upcoming_sleep: float = 0.0
|
|
454
548
|
|
|
455
549
|
@property
|
|
456
550
|
def seconds_since_start(self) -> t.Optional[float]:
|
|
@@ -471,7 +565,10 @@ class RetryCallState:
|
|
|
471
565
|
self.outcome, self.outcome_timestamp = fut, ts
|
|
472
566
|
|
|
473
567
|
def set_exception(
|
|
474
|
-
self,
|
|
568
|
+
self,
|
|
569
|
+
exc_info: t.Tuple[
|
|
570
|
+
t.Type[BaseException], BaseException, "types.TracebackType| None"
|
|
571
|
+
],
|
|
475
572
|
) -> None:
|
|
476
573
|
ts = time.monotonic()
|
|
477
574
|
fut = Future(self.attempt_number)
|
|
@@ -493,24 +590,30 @@ class RetryCallState:
|
|
|
493
590
|
|
|
494
591
|
|
|
495
592
|
@t.overload
|
|
496
|
-
def retry(func: WrappedFn) -> WrappedFn:
|
|
497
|
-
...
|
|
593
|
+
def retry(func: WrappedFn) -> WrappedFn: ...
|
|
498
594
|
|
|
499
595
|
|
|
500
596
|
@t.overload
|
|
501
597
|
def retry(
|
|
502
|
-
sleep: t.Callable[[t.Union[int, float]], t.
|
|
598
|
+
sleep: t.Callable[[t.Union[int, float]], t.Union[None, t.Awaitable[None]]] = sleep,
|
|
503
599
|
stop: "StopBaseT" = stop_never,
|
|
504
600
|
wait: "WaitBaseT" = wait_none(),
|
|
505
|
-
retry: "RetryBaseT" = retry_if_exception_type(),
|
|
506
|
-
before: t.Callable[
|
|
507
|
-
|
|
508
|
-
|
|
601
|
+
retry: "t.Union[RetryBaseT, tasyncio.retry.RetryBaseT]" = retry_if_exception_type(),
|
|
602
|
+
before: t.Callable[
|
|
603
|
+
["RetryCallState"], t.Union[None, t.Awaitable[None]]
|
|
604
|
+
] = before_nothing,
|
|
605
|
+
after: t.Callable[
|
|
606
|
+
["RetryCallState"], t.Union[None, t.Awaitable[None]]
|
|
607
|
+
] = after_nothing,
|
|
608
|
+
before_sleep: t.Optional[
|
|
609
|
+
t.Callable[["RetryCallState"], t.Union[None, t.Awaitable[None]]]
|
|
610
|
+
] = None,
|
|
509
611
|
reraise: bool = False,
|
|
510
612
|
retry_error_cls: t.Type["RetryError"] = RetryError,
|
|
511
|
-
retry_error_callback: t.Optional[
|
|
512
|
-
|
|
513
|
-
|
|
613
|
+
retry_error_callback: t.Optional[
|
|
614
|
+
t.Callable[["RetryCallState"], t.Union[t.Any, t.Awaitable[t.Any]]]
|
|
615
|
+
] = None,
|
|
616
|
+
) -> t.Callable[[WrappedFn], WrappedFn]: ...
|
|
514
617
|
|
|
515
618
|
|
|
516
619
|
def retry(*dargs: t.Any, **dkw: t.Any) -> t.Any:
|
|
@@ -531,9 +634,13 @@ def retry(*dargs: t.Any, **dkw: t.Any) -> t.Any:
|
|
|
531
634
|
f"this will probably hang indefinitely (did you mean retry={f.__class__.__name__}(...)?)"
|
|
532
635
|
)
|
|
533
636
|
r: "BaseRetrying"
|
|
534
|
-
if
|
|
637
|
+
if _utils.is_coroutine_callable(f):
|
|
535
638
|
r = AsyncRetrying(*dargs, **dkw)
|
|
536
|
-
elif
|
|
639
|
+
elif (
|
|
640
|
+
tornado
|
|
641
|
+
and hasattr(tornado.gen, "is_coroutine_function")
|
|
642
|
+
and tornado.gen.is_coroutine_function(f)
|
|
643
|
+
):
|
|
537
644
|
r = TornadoRetrying(*dargs, **dkw)
|
|
538
645
|
else:
|
|
539
646
|
r = Retrying(*dargs, **dkw)
|
|
@@ -543,7 +650,7 @@ def retry(*dargs: t.Any, **dkw: t.Any) -> t.Any:
|
|
|
543
650
|
return wrap
|
|
544
651
|
|
|
545
652
|
|
|
546
|
-
from tenacity.
|
|
653
|
+
from tenacity.asyncio import AsyncRetrying # noqa:E402,I100
|
|
547
654
|
|
|
548
655
|
if tornado:
|
|
549
656
|
from tenacity.tornadoweb import TornadoRetrying
|
|
@@ -568,6 +675,7 @@ __all__ = [
|
|
|
568
675
|
"sleep_using_event",
|
|
569
676
|
"stop_after_attempt",
|
|
570
677
|
"stop_after_delay",
|
|
678
|
+
"stop_before_delay",
|
|
571
679
|
"stop_all",
|
|
572
680
|
"stop_any",
|
|
573
681
|
"stop_never",
|
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,28 @@ 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)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def wrap_to_async_func(
|
|
93
|
+
call: typing.Callable[..., typing.Any],
|
|
94
|
+
) -> typing.Callable[..., typing.Awaitable[typing.Any]]:
|
|
95
|
+
if is_coroutine_callable(call):
|
|
96
|
+
return call
|
|
97
|
+
|
|
98
|
+
async def inner(*args: typing.Any, **kwargs: typing.Any) -> typing.Any:
|
|
99
|
+
return call(*args, **kwargs)
|
|
100
|
+
|
|
101
|
+
return inner
|
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
|
@@ -30,10 +30,16 @@ class retry_base(abc.ABC):
|
|
|
30
30
|
pass
|
|
31
31
|
|
|
32
32
|
def __and__(self, other: "retry_base") -> "retry_all":
|
|
33
|
-
return
|
|
33
|
+
return other.__rand__(self)
|
|
34
|
+
|
|
35
|
+
def __rand__(self, other: "retry_base") -> "retry_all":
|
|
36
|
+
return retry_all(other, self)
|
|
34
37
|
|
|
35
38
|
def __or__(self, other: "retry_base") -> "retry_any":
|
|
36
|
-
return
|
|
39
|
+
return other.__ror__(self)
|
|
40
|
+
|
|
41
|
+
def __ror__(self, other: "retry_base") -> "retry_any":
|
|
42
|
+
return retry_any(other, self)
|
|
37
43
|
|
|
38
44
|
|
|
39
45
|
RetryBaseT = typing.Union[retry_base, typing.Callable[["RetryCallState"], bool]]
|
|
@@ -204,7 +210,9 @@ class retry_if_exception_message(retry_if_exception):
|
|
|
204
210
|
match: typing.Optional[str] = None,
|
|
205
211
|
) -> None:
|
|
206
212
|
if message and match:
|
|
207
|
-
raise TypeError(
|
|
213
|
+
raise TypeError(
|
|
214
|
+
f"{self.__class__.__name__}() takes either 'message' or 'match', not both"
|
|
215
|
+
)
|
|
208
216
|
|
|
209
217
|
# set predicate
|
|
210
218
|
if message:
|
|
@@ -221,7 +229,9 @@ class retry_if_exception_message(retry_if_exception):
|
|
|
221
229
|
|
|
222
230
|
predicate = match_fnc
|
|
223
231
|
else:
|
|
224
|
-
raise TypeError(
|
|
232
|
+
raise TypeError(
|
|
233
|
+
f"{self.__class__.__name__}() missing 1 required argument 'message' or 'match'"
|
|
234
|
+
)
|
|
225
235
|
|
|
226
236
|
super().__init__(predicate)
|
|
227
237
|
|
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.4.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,16 @@
|
|
|
1
|
+
tenacity/__init__.py,sha256=dT216tDrncc6QKV6_1AfuhzmCMfUzM_hIwW34ivIRsQ,23651
|
|
2
|
+
tenacity/_utils.py,sha256=5AwPoFrGOIfPkCtqeJdFBi7KKQNcJXJ3erbtGOXtn6w,2916
|
|
3
|
+
tenacity/after.py,sha256=NR4rGyslG7xF1hDJZb2Wf8wVApafX0HZwz2nFVLvaqE,1658
|
|
4
|
+
tenacity/before.py,sha256=7zDTpZ3b6rkY9sOdS-qbpjBgSjVr3xBqcIqdYAh9ZKM,1544
|
|
5
|
+
tenacity/before_sleep.py,sha256=upKssiY5poOO3Bv0amADZA-a5CrY5tnb4_97s8_88SM,2360
|
|
6
|
+
tenacity/nap.py,sha256=fRWvnz1aIzbIq9Ap3gAkAZgDH6oo5zxMrU6ZOVByq0I,1383
|
|
7
|
+
tenacity/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
+
tenacity/retry.py,sha256=PVnZfXBeLN2DzsytKexw23SPLn82AdZfFPC2OhVQ_1Y,8986
|
|
9
|
+
tenacity/stop.py,sha256=wQuwGfCLw8OH1C3x0G9lH9xtJCyhgviePQ40HRAUg54,4113
|
|
10
|
+
tenacity/tornadoweb.py,sha256=vS1ONfPYoGzPx1asQaVbfoo6D9tPIzSysJipm61Yqw8,2125
|
|
11
|
+
tenacity/wait.py,sha256=Q9XoZCtFra53aQOyfABpvRDuUeB-NpUUXImHsUiRQI0,8042
|
|
12
|
+
tenacity-8.4.0.dist-info/LICENSE,sha256=WNHhf_5RCaeuKWyq_K39vmp9F28LxKsB4SpomwSZ2L0,11357
|
|
13
|
+
tenacity-8.4.0.dist-info/METADATA,sha256=rnsKmqyLFhyN5Ydqjehhxuls6kPQ27yu6M4l2t5WeuM,1155
|
|
14
|
+
tenacity-8.4.0.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
|
15
|
+
tenacity-8.4.0.dist-info/top_level.txt,sha256=Zf8AOZMN7hr1EEcUo9U5KzXsM4TOC1pBZ22D8913JYs,9
|
|
16
|
+
tenacity-8.4.0.dist-info/RECORD,,
|
tenacity/_asyncio.py
DELETED
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
# Copyright 2016 Étienne Bersac
|
|
2
|
-
# Copyright 2016 Julien Danjou
|
|
3
|
-
# Copyright 2016 Joshua Harlow
|
|
4
|
-
# Copyright 2013-2014 Ray Holder
|
|
5
|
-
#
|
|
6
|
-
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
7
|
-
# you may not use this file except in compliance with the License.
|
|
8
|
-
# You may obtain a copy of the License at
|
|
9
|
-
#
|
|
10
|
-
# http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
-
#
|
|
12
|
-
# Unless required by applicable law or agreed to in writing, software
|
|
13
|
-
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
14
|
-
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
|
-
# See the License for the specific language governing permissions and
|
|
16
|
-
# limitations under the License.
|
|
17
|
-
|
|
18
|
-
import functools
|
|
19
|
-
import sys
|
|
20
|
-
import typing as t
|
|
21
|
-
from asyncio import sleep
|
|
22
|
-
|
|
23
|
-
from tenacity import AttemptManager
|
|
24
|
-
from tenacity import BaseRetrying
|
|
25
|
-
from tenacity import DoAttempt
|
|
26
|
-
from tenacity import DoSleep
|
|
27
|
-
from tenacity import RetryCallState
|
|
28
|
-
|
|
29
|
-
WrappedFnReturnT = t.TypeVar("WrappedFnReturnT")
|
|
30
|
-
WrappedFn = t.TypeVar("WrappedFn", bound=t.Callable[..., t.Awaitable[t.Any]])
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
class AsyncRetrying(BaseRetrying):
|
|
34
|
-
sleep: t.Callable[[float], t.Awaitable[t.Any]]
|
|
35
|
-
|
|
36
|
-
def __init__(self, sleep: t.Callable[[float], t.Awaitable[t.Any]] = sleep, **kwargs: t.Any) -> None:
|
|
37
|
-
super().__init__(**kwargs)
|
|
38
|
-
self.sleep = sleep
|
|
39
|
-
|
|
40
|
-
async def __call__( # type: ignore[override]
|
|
41
|
-
self, fn: WrappedFn, *args: t.Any, **kwargs: t.Any
|
|
42
|
-
) -> WrappedFnReturnT:
|
|
43
|
-
self.begin()
|
|
44
|
-
|
|
45
|
-
retry_state = RetryCallState(retry_object=self, fn=fn, args=args, kwargs=kwargs)
|
|
46
|
-
while True:
|
|
47
|
-
do = self.iter(retry_state=retry_state)
|
|
48
|
-
if isinstance(do, DoAttempt):
|
|
49
|
-
try:
|
|
50
|
-
result = await fn(*args, **kwargs)
|
|
51
|
-
except BaseException: # noqa: B902
|
|
52
|
-
retry_state.set_exception(sys.exc_info()) # type: ignore[arg-type]
|
|
53
|
-
else:
|
|
54
|
-
retry_state.set_result(result)
|
|
55
|
-
elif isinstance(do, DoSleep):
|
|
56
|
-
retry_state.prepare_for_next_attempt()
|
|
57
|
-
await self.sleep(do)
|
|
58
|
-
else:
|
|
59
|
-
return do # type: ignore[no-any-return]
|
|
60
|
-
|
|
61
|
-
def __iter__(self) -> t.Generator[AttemptManager, None, None]:
|
|
62
|
-
raise TypeError("AsyncRetrying object is not iterable")
|
|
63
|
-
|
|
64
|
-
def __aiter__(self) -> "AsyncRetrying":
|
|
65
|
-
self.begin()
|
|
66
|
-
self._retry_state = RetryCallState(self, fn=None, args=(), kwargs={})
|
|
67
|
-
return self
|
|
68
|
-
|
|
69
|
-
async def __anext__(self) -> AttemptManager:
|
|
70
|
-
while True:
|
|
71
|
-
do = self.iter(retry_state=self._retry_state)
|
|
72
|
-
if do is None:
|
|
73
|
-
raise StopAsyncIteration
|
|
74
|
-
elif isinstance(do, DoAttempt):
|
|
75
|
-
return AttemptManager(retry_state=self._retry_state)
|
|
76
|
-
elif isinstance(do, DoSleep):
|
|
77
|
-
self._retry_state.prepare_for_next_attempt()
|
|
78
|
-
await self.sleep(do)
|
|
79
|
-
else:
|
|
80
|
-
raise StopAsyncIteration
|
|
81
|
-
|
|
82
|
-
def wraps(self, fn: WrappedFn) -> WrappedFn:
|
|
83
|
-
fn = super().wraps(fn)
|
|
84
|
-
# Ensure wrapper is recognized as a coroutine function.
|
|
85
|
-
|
|
86
|
-
@functools.wraps(fn)
|
|
87
|
-
async def async_wrapped(*args: t.Any, **kwargs: t.Any) -> t.Any:
|
|
88
|
-
return await fn(*args, **kwargs)
|
|
89
|
-
|
|
90
|
-
# Preserve attributes
|
|
91
|
-
async_wrapped.retry = fn.retry # type: ignore[attr-defined]
|
|
92
|
-
async_wrapped.retry_with = fn.retry_with # type: ignore[attr-defined]
|
|
93
|
-
|
|
94
|
-
return async_wrapped # type: ignore[return-value]
|
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
|