tenacity 9.1.2__py3-none-any.whl → 9.1.3__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 CHANGED
@@ -59,6 +59,7 @@ from .stop import stop_when_event_set # noqa
59
59
  # Import all built-in wait strategies for easier usage.
60
60
  from .wait import wait_chain # noqa
61
61
  from .wait import wait_combine # noqa
62
+ from .wait import wait_exception # noqa
62
63
  from .wait import wait_exponential # noqa
63
64
  from .wait import wait_fixed # noqa
64
65
  from .wait import wait_incrementing # noqa
@@ -98,14 +99,11 @@ if t.TYPE_CHECKING:
98
99
 
99
100
  WrappedFnReturnT = t.TypeVar("WrappedFnReturnT")
100
101
  WrappedFn = t.TypeVar("WrappedFn", bound=t.Callable[..., t.Any])
102
+ P = t.ParamSpec("P")
103
+ R = t.TypeVar("R")
101
104
 
102
105
 
103
- dataclass_kwargs = {}
104
- if sys.version_info >= (3, 10):
105
- dataclass_kwargs.update({"slots": True})
106
-
107
-
108
- @dataclasses.dataclass(**dataclass_kwargs)
106
+ @dataclasses.dataclass(slots=True)
109
107
  class IterState:
110
108
  actions: t.List[t.Callable[["RetryCallState"], t.Any]] = dataclasses.field(
111
109
  default_factory=list
@@ -307,19 +305,15 @@ class BaseRetrying(ABC):
307
305
  future we may provide a way to aggregate the various
308
306
  statistics from each thread).
309
307
  """
310
- try:
311
- return self._local.statistics # type: ignore[no-any-return]
312
- except AttributeError:
308
+ if not hasattr(self._local, "statistics"):
313
309
  self._local.statistics = t.cast(t.Dict[str, t.Any], {})
314
- return self._local.statistics
310
+ return self._local.statistics # type: ignore[no-any-return]
315
311
 
316
312
  @property
317
313
  def iter_state(self) -> IterState:
318
- try:
319
- return self._local.iter_state # type: ignore[no-any-return]
320
- except AttributeError:
314
+ if not hasattr(self._local, "iter_state"):
321
315
  self._local.iter_state = IterState()
322
- return self._local.iter_state
316
+ return self._local.iter_state # type: ignore[no-any-return]
323
317
 
324
318
  def wraps(self, f: WrappedFn) -> WrappedFn:
325
319
  """Wrap a function for retrying.
@@ -489,13 +483,7 @@ class Retrying(BaseRetrying):
489
483
  return do # type: ignore[no-any-return]
490
484
 
491
485
 
492
- if sys.version_info >= (3, 9):
493
- FutureGenericT = futures.Future[t.Any]
494
- else:
495
- FutureGenericT = futures.Future
496
-
497
-
498
- class Future(FutureGenericT):
486
+ class Future(futures.Future[t.Any]):
499
487
  """Encapsulates a (future or past) attempted call to a target function."""
500
488
 
501
489
  def __init__(self, attempt_number: int) -> None:
@@ -603,7 +591,27 @@ def retry(func: WrappedFn) -> WrappedFn: ...
603
591
 
604
592
  @t.overload
605
593
  def retry(
606
- sleep: t.Callable[[t.Union[int, float]], t.Union[None, t.Awaitable[None]]] = sleep,
594
+ *,
595
+ sleep: t.Callable[[t.Union[int, float]], t.Awaitable[None]],
596
+ stop: "StopBaseT" = ...,
597
+ wait: "WaitBaseT" = ...,
598
+ retry: "t.Union[RetryBaseT, tasyncio.retry.RetryBaseT]" = ...,
599
+ before: t.Callable[["RetryCallState"], t.Union[None, t.Awaitable[None]]] = ...,
600
+ after: t.Callable[["RetryCallState"], t.Union[None, t.Awaitable[None]]] = ...,
601
+ before_sleep: t.Optional[
602
+ t.Callable[["RetryCallState"], t.Union[None, t.Awaitable[None]]]
603
+ ] = ...,
604
+ reraise: bool = ...,
605
+ retry_error_cls: t.Type["RetryError"] = ...,
606
+ retry_error_callback: t.Optional[
607
+ t.Callable[["RetryCallState"], t.Union[t.Any, t.Awaitable[t.Any]]]
608
+ ] = ...,
609
+ ) -> t.Callable[[t.Callable[P, R | t.Awaitable[R]]], t.Callable[P, t.Awaitable[R]]]: ...
610
+
611
+
612
+ @t.overload
613
+ def retry(
614
+ sleep: t.Callable[[t.Union[int, float]], None] = sleep,
607
615
  stop: "StopBaseT" = stop_never,
608
616
  wait: "WaitBaseT" = wait_none(),
609
617
  retry: "t.Union[RetryBaseT, tasyncio.retry.RetryBaseT]" = retry_if_exception_type(),
@@ -642,7 +650,10 @@ def retry(*dargs: t.Any, **dkw: t.Any) -> t.Any:
642
650
  f"this will probably hang indefinitely (did you mean retry={f.__class__.__name__}(...)?)"
643
651
  )
644
652
  r: "BaseRetrying"
645
- if _utils.is_coroutine_callable(f):
653
+ sleep = dkw.get("sleep")
654
+ if _utils.is_coroutine_callable(f) or (
655
+ sleep is not None and _utils.is_coroutine_callable(sleep)
656
+ ):
646
657
  r = AsyncRetrying(*dargs, **dkw)
647
658
  elif (
648
659
  tornado
@@ -690,6 +701,7 @@ __all__ = [
690
701
  "stop_when_event_set",
691
702
  "wait_chain",
692
703
  "wait_combine",
704
+ "wait_exception",
693
705
  "wait_exponential",
694
706
  "wait_fixed",
695
707
  "wait_incrementing",
tenacity/_utils.py CHANGED
@@ -25,6 +25,18 @@ from datetime import timedelta
25
25
  MAX_WAIT = sys.maxsize / 2
26
26
 
27
27
 
28
+ class LoggerProtocol(typing.Protocol):
29
+ """
30
+ Protocol used by utils expecting a logger (eg: before_log).
31
+
32
+ Compatible with logging, structlog, loguru, etc...
33
+ """
34
+
35
+ def log(
36
+ self, level: int, msg: str, /, *args: typing.Any, **kwargs: typing.Any
37
+ ) -> typing.Any: ...
38
+
39
+
28
40
  def find_ordinal(pos_num: int) -> str:
29
41
  # See: https://en.wikipedia.org/wiki/English_numerals#Ordinal_numbers
30
42
  if pos_num == 0:
tenacity/after.py CHANGED
@@ -19,8 +19,6 @@ import typing
19
19
  from tenacity import _utils
20
20
 
21
21
  if typing.TYPE_CHECKING:
22
- import logging
23
-
24
22
  from tenacity import RetryCallState
25
23
 
26
24
 
@@ -29,9 +27,9 @@ def after_nothing(retry_state: "RetryCallState") -> None:
29
27
 
30
28
 
31
29
  def after_log(
32
- logger: "logging.Logger",
30
+ logger: _utils.LoggerProtocol,
33
31
  log_level: int,
34
- sec_format: str = "%0.3f",
32
+ sec_format: str = "%.3g",
35
33
  ) -> typing.Callable[["RetryCallState"], None]:
36
34
  """After call strategy that logs to some logger the finished attempt."""
37
35
 
@@ -107,11 +107,15 @@ class AsyncRetrying(BaseRetrying):
107
107
  self.begin()
108
108
 
109
109
  retry_state = RetryCallState(retry_object=self, fn=fn, args=args, kwargs=kwargs)
110
+ is_async = _utils.is_coroutine_callable(fn)
110
111
  while True:
111
112
  do = await self.iter(retry_state=retry_state)
112
113
  if isinstance(do, DoAttempt):
113
114
  try:
114
- result = await fn(*args, **kwargs)
115
+ if is_async:
116
+ result = await fn(*args, **kwargs)
117
+ else:
118
+ result = fn(*args, **kwargs)
115
119
  except BaseException: # noqa: B902
116
120
  retry_state.set_exception(sys.exc_info()) # type: ignore[arg-type]
117
121
  else:
tenacity/before.py CHANGED
@@ -19,8 +19,6 @@ import typing
19
19
  from tenacity import _utils
20
20
 
21
21
  if typing.TYPE_CHECKING:
22
- import logging
23
-
24
22
  from tenacity import RetryCallState
25
23
 
26
24
 
@@ -29,7 +27,7 @@ def before_nothing(retry_state: "RetryCallState") -> None:
29
27
 
30
28
 
31
29
  def before_log(
32
- logger: "logging.Logger", log_level: int
30
+ logger: _utils.LoggerProtocol, log_level: int
33
31
  ) -> typing.Callable[["RetryCallState"], None]:
34
32
  """Before call strategy that logs to some logger the attempt."""
35
33
 
tenacity/before_sleep.py CHANGED
@@ -19,8 +19,6 @@ import typing
19
19
  from tenacity import _utils
20
20
 
21
21
  if typing.TYPE_CHECKING:
22
- import logging
23
-
24
22
  from tenacity import RetryCallState
25
23
 
26
24
 
@@ -29,9 +27,10 @@ def before_sleep_nothing(retry_state: "RetryCallState") -> None:
29
27
 
30
28
 
31
29
  def before_sleep_log(
32
- logger: "logging.Logger",
30
+ logger: _utils.LoggerProtocol,
33
31
  log_level: int,
34
32
  exc_info: bool = False,
33
+ sec_format: str = "%.3g",
35
34
  ) -> typing.Callable[["RetryCallState"], None]:
36
35
  """Before sleep strategy that logs to some logger the attempt."""
37
36
 
@@ -65,7 +64,7 @@ def before_sleep_log(
65
64
  logger.log(
66
65
  log_level,
67
66
  f"Retrying {fn_name} "
68
- f"in {retry_state.next_action.sleep} seconds as it {verb} {value}.",
67
+ f"in {sec_format % retry_state.next_action.sleep} seconds as it {verb} {value}.",
69
68
  exc_info=local_exc_info,
70
69
  )
71
70
 
tenacity/tornadoweb.py CHANGED
@@ -37,7 +37,7 @@ class TornadoRetrying(BaseRetrying):
37
37
  super().__init__(**kwargs)
38
38
  self.sleep = sleep
39
39
 
40
- @gen.coroutine # type: ignore[misc]
40
+ @gen.coroutine # type: ignore[untyped-decorator]
41
41
  def __call__(
42
42
  self,
43
43
  fn: "typing.Callable[..., typing.Union[typing.Generator[typing.Any, typing.Any, _RetValT], Future[_RetValT]]]",
tenacity/wait.py CHANGED
@@ -98,10 +98,10 @@ class wait_chain(wait_base):
98
98
 
99
99
  @retry(wait=wait_chain(*[wait_fixed(1) for i in range(3)] +
100
100
  [wait_fixed(2) for j in range(5)] +
101
- [wait_fixed(5) for k in range(4)))
101
+ [wait_fixed(5) for k in range(4)]))
102
102
  def wait_chained():
103
- print("Wait 1s for 3 attempts, 2s for 5 attempts and 5s
104
- thereafter.")
103
+ print("Wait 1s for 3 attempts, 2s for 5 attempts and 5s "
104
+ "thereafter.")
105
105
  """
106
106
 
107
107
  def __init__(self, *strategies: wait_base) -> None:
@@ -113,6 +113,45 @@ class wait_chain(wait_base):
113
113
  return wait_func(retry_state=retry_state)
114
114
 
115
115
 
116
+ class wait_exception(wait_base):
117
+ """Wait strategy that waits the amount of time returned by the predicate.
118
+
119
+ The predicate is passed the exception object. Based on the exception, the
120
+ user can decide how much time to wait before retrying.
121
+
122
+ For example::
123
+
124
+ def http_error(exception: BaseException) -> float:
125
+ if (
126
+ isinstance(exception, requests.HTTPError)
127
+ and exception.response.status_code == requests.codes.too_many_requests
128
+ ):
129
+ return float(exception.response.headers.get("Retry-After", "1"))
130
+ return 60.0
131
+
132
+
133
+ @retry(
134
+ stop=stop_after_attempt(3),
135
+ wait=wait_exception(http_error),
136
+ )
137
+ def http_get_request(url: str) -> None:
138
+ response = requests.get(url)
139
+ response.raise_for_status()
140
+ """
141
+
142
+ def __init__(self, predicate: typing.Callable[[BaseException], float]) -> None:
143
+ self.predicate = predicate
144
+
145
+ def __call__(self, retry_state: "RetryCallState") -> float:
146
+ if retry_state.outcome is None:
147
+ raise RuntimeError("__call__() called before outcome was set")
148
+
149
+ exception = retry_state.outcome.exception()
150
+ if exception is None:
151
+ raise RuntimeError("outcome failed but the exception is None")
152
+ return self.predicate(exception)
153
+
154
+
116
155
  class wait_incrementing(wait_base):
117
156
  """Wait an incremental amount of time after each attempt.
118
157
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tenacity
3
- Version: 9.1.2
3
+ Version: 9.1.3
4
4
  Summary: Retry code until it succeeds
5
5
  Home-page: https://github.com/jd/tenacity
6
6
  Author: Julien Danjou
@@ -11,13 +11,13 @@ 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.9
15
14
  Classifier: Programming Language :: Python :: 3.10
16
15
  Classifier: Programming Language :: Python :: 3.11
17
16
  Classifier: Programming Language :: Python :: 3.12
18
17
  Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Programming Language :: Python :: 3.14
19
19
  Classifier: Topic :: Utilities
20
- Requires-Python: >=3.9
20
+ Requires-Python: >=3.10
21
21
  License-File: LICENSE
22
22
  Provides-Extra: doc
23
23
  Requires-Dist: reno; extra == "doc"
@@ -0,0 +1,18 @@
1
+ tenacity/__init__.py,sha256=4hlAUSM3kIMm_StxMCqLxPyqfU_JWHienwwi1VLWzS8,24761
2
+ tenacity/_utils.py,sha256=3t9RTQghqIA3bdFKfJFQJWigkNvmj2Kzwci6GM6SA_c,3211
3
+ tenacity/after.py,sha256=PIquwn3mfjqtjbLSWPFilMN9z03vAFBbNXa5No6I8F0,1642
4
+ tenacity/before.py,sha256=XfSkW2vxjZ0hnYq7zQm-5cuhP-cIeQngVC4xGrdECxU,1529
5
+ tenacity/before_sleep.py,sha256=KJrNnT2F6q5Ue4Ys52b71c8gmvuhYGyurfXOTy8rzrM,2390
6
+ tenacity/nap.py,sha256=fRWvnz1aIzbIq9Ap3gAkAZgDH6oo5zxMrU6ZOVByq0I,1383
7
+ tenacity/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
+ tenacity/retry.py,sha256=cZrAzWmMqAORKjmz9B1Pq2Kvn9GX2Znzs_3ybgvQwnE,9034
9
+ tenacity/stop.py,sha256=wQuwGfCLw8OH1C3x0G9lH9xtJCyhgviePQ40HRAUg54,4113
10
+ tenacity/tornadoweb.py,sha256=0oWRxWkJJijVoZT_nRJ2Id6YHmcZ0V6SUIaGHDcpwu8,2138
11
+ tenacity/wait.py,sha256=FLJG9wTFvDpkwMrK8QEW7Dhue22wHtOaoSZWN8k4sPk,9413
12
+ tenacity/asyncio/__init__.py,sha256=cgKuW91lVneg_3TBCJhWm6gqyl0GaWOSYHKq8yMbNlg,7941
13
+ tenacity/asyncio/retry.py,sha256=ymu8F1JfAerc5vDO0V4-2kCvHXqyce2jpO1Nlt6aKxI,4244
14
+ tenacity-9.1.3.dist-info/licenses/LICENSE,sha256=WNHhf_5RCaeuKWyq_K39vmp9F28LxKsB4SpomwSZ2L0,11357
15
+ tenacity-9.1.3.dist-info/METADATA,sha256=CnjNwf3FaryedkK6fGDnbHhsSMwCa3cn2LOF0vaBRvU,1174
16
+ tenacity-9.1.3.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
17
+ tenacity-9.1.3.dist-info/top_level.txt,sha256=Zf8AOZMN7hr1EEcUo9U5KzXsM4TOC1pBZ22D8913JYs,9
18
+ tenacity-9.1.3.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (78.1.0)
2
+ Generator: setuptools (80.10.2)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,18 +0,0 @@
1
- tenacity/__init__.py,sha256=LLTmuzCs-hqXeQSVlAig6GwSCj3V1PewC9-b3mTXn1g,24060
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=J8emgLL-jkzbctXE7uzEUfCxWL4SKfGuOmilWowCP9c,2362
6
- tenacity/nap.py,sha256=fRWvnz1aIzbIq9Ap3gAkAZgDH6oo5zxMrU6ZOVByq0I,1383
7
- tenacity/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
- tenacity/retry.py,sha256=cZrAzWmMqAORKjmz9B1Pq2Kvn9GX2Znzs_3ybgvQwnE,9034
9
- tenacity/stop.py,sha256=wQuwGfCLw8OH1C3x0G9lH9xtJCyhgviePQ40HRAUg54,4113
10
- tenacity/tornadoweb.py,sha256=vS1ONfPYoGzPx1asQaVbfoo6D9tPIzSysJipm61Yqw8,2125
11
- tenacity/wait.py,sha256=1oye0erAqJkSIysEQIfgtZXHunv7t31BwI2gHL1FsSk,8049
12
- tenacity/asyncio/__init__.py,sha256=PoDGzHN-neTr2GgA1Ti3ORMrSmUIEUblbiSiuyxKHTI,7773
13
- tenacity/asyncio/retry.py,sha256=ymu8F1JfAerc5vDO0V4-2kCvHXqyce2jpO1Nlt6aKxI,4244
14
- tenacity-9.1.2.dist-info/licenses/LICENSE,sha256=WNHhf_5RCaeuKWyq_K39vmp9F28LxKsB4SpomwSZ2L0,11357
15
- tenacity-9.1.2.dist-info/METADATA,sha256=1vPK9y_B7kDK-RNa33tdRKRRABFbTqkrgPGe0YOxV6U,1172
16
- tenacity-9.1.2.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
17
- tenacity-9.1.2.dist-info/top_level.txt,sha256=Zf8AOZMN7hr1EEcUo9U5KzXsM4TOC1pBZ22D8913JYs,9
18
- tenacity-9.1.2.dist-info/RECORD,,