tenacity 9.0.0__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
@@ -76,7 +77,7 @@ from .before import before_nothing # noqa
76
77
  from .after import after_log # noqa
77
78
  from .after import after_nothing # noqa
78
79
 
79
- # Import all built-in after strategies for easier usage.
80
+ # Import all built-in before sleep strategies for easier usage.
80
81
  from .before_sleep import before_sleep_log # noqa
81
82
  from .before_sleep import before_sleep_nothing # noqa
82
83
 
@@ -88,6 +89,8 @@ except ImportError:
88
89
  if t.TYPE_CHECKING:
89
90
  import types
90
91
 
92
+ from typing_extensions import Self
93
+
91
94
  from . import asyncio as tasyncio
92
95
  from .retry import RetryBaseT
93
96
  from .stop import StopBaseT
@@ -96,14 +99,11 @@ if t.TYPE_CHECKING:
96
99
 
97
100
  WrappedFnReturnT = t.TypeVar("WrappedFnReturnT")
98
101
  WrappedFn = t.TypeVar("WrappedFn", bound=t.Callable[..., t.Any])
102
+ P = t.ParamSpec("P")
103
+ R = t.TypeVar("R")
99
104
 
100
105
 
101
- dataclass_kwargs = {}
102
- if sys.version_info >= (3, 10):
103
- dataclass_kwargs.update({"slots": True})
104
-
105
-
106
- @dataclasses.dataclass(**dataclass_kwargs)
106
+ @dataclasses.dataclass(slots=True)
107
107
  class IterState:
108
108
  actions: t.List[t.Callable[["RetryCallState"], t.Any]] = dataclasses.field(
109
109
  default_factory=list
@@ -255,7 +255,7 @@ class BaseRetrying(ABC):
255
255
  retry_error_callback: t.Union[
256
256
  t.Optional[t.Callable[["RetryCallState"], t.Any]], object
257
257
  ] = _unset,
258
- ) -> "BaseRetrying":
258
+ ) -> "Self":
259
259
  """Copy this object with some parameters changed if needed."""
260
260
  return self.__class__(
261
261
  sleep=_first_set(sleep, self.sleep),
@@ -305,19 +305,15 @@ class BaseRetrying(ABC):
305
305
  future we may provide a way to aggregate the various
306
306
  statistics from each thread).
307
307
  """
308
- try:
309
- return self._local.statistics # type: ignore[no-any-return]
310
- except AttributeError:
308
+ if not hasattr(self._local, "statistics"):
311
309
  self._local.statistics = t.cast(t.Dict[str, t.Any], {})
312
- return self._local.statistics
310
+ return self._local.statistics # type: ignore[no-any-return]
313
311
 
314
312
  @property
315
313
  def iter_state(self) -> IterState:
316
- try:
317
- return self._local.iter_state # type: ignore[no-any-return]
318
- except AttributeError:
314
+ if not hasattr(self._local, "iter_state"):
319
315
  self._local.iter_state = IterState()
320
- return self._local.iter_state
316
+ return self._local.iter_state # type: ignore[no-any-return]
321
317
 
322
318
  def wraps(self, f: WrappedFn) -> WrappedFn:
323
319
  """Wrap a function for retrying.
@@ -487,13 +483,7 @@ class Retrying(BaseRetrying):
487
483
  return do # type: ignore[no-any-return]
488
484
 
489
485
 
490
- if sys.version_info >= (3, 9):
491
- FutureGenericT = futures.Future[t.Any]
492
- else:
493
- FutureGenericT = futures.Future
494
-
495
-
496
- class Future(FutureGenericT):
486
+ class Future(futures.Future[t.Any]):
497
487
  """Encapsulates a (future or past) attempted call to a target function."""
498
488
 
499
489
  def __init__(self, attempt_number: int) -> None:
@@ -601,7 +591,27 @@ def retry(func: WrappedFn) -> WrappedFn: ...
601
591
 
602
592
  @t.overload
603
593
  def retry(
604
- 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,
605
615
  stop: "StopBaseT" = stop_never,
606
616
  wait: "WaitBaseT" = wait_none(),
607
617
  retry: "t.Union[RetryBaseT, tasyncio.retry.RetryBaseT]" = retry_if_exception_type(),
@@ -640,7 +650,10 @@ def retry(*dargs: t.Any, **dkw: t.Any) -> t.Any:
640
650
  f"this will probably hang indefinitely (did you mean retry={f.__class__.__name__}(...)?)"
641
651
  )
642
652
  r: "BaseRetrying"
643
- 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
+ ):
644
657
  r = AsyncRetrying(*dargs, **dkw)
645
658
  elif (
646
659
  tornado
@@ -688,6 +701,7 @@ __all__ = [
688
701
  "stop_when_event_set",
689
702
  "wait_chain",
690
703
  "wait_combine",
704
+ "wait_exception",
691
705
  "wait_exponential",
692
706
  "wait_fixed",
693
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,21 +19,20 @@ 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
 
27
25
  def before_sleep_nothing(retry_state: "RetryCallState") -> None:
28
- """Before call strategy that does nothing."""
26
+ """Before sleep strategy that does nothing."""
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
- """Before call strategy that logs to some logger the attempt."""
35
+ """Before sleep strategy that logs to some logger the attempt."""
37
36
 
38
37
  def log_it(retry_state: "RetryCallState") -> None:
39
38
  local_exc_info: BaseException | bool | None
@@ -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/retry.py CHANGED
@@ -207,7 +207,7 @@ class retry_if_exception_message(retry_if_exception):
207
207
  def __init__(
208
208
  self,
209
209
  message: typing.Optional[str] = None,
210
- match: typing.Optional[str] = None,
210
+ match: typing.Union[None, str, typing.Pattern[str]] = None,
211
211
  ) -> None:
212
212
  if message and match:
213
213
  raise TypeError(
@@ -242,7 +242,7 @@ class retry_if_not_exception_message(retry_if_exception_message):
242
242
  def __init__(
243
243
  self,
244
244
  message: typing.Optional[str] = None,
245
- match: typing.Optional[str] = None,
245
+ match: typing.Union[None, str, typing.Pattern[str]] = None,
246
246
  ) -> None:
247
247
  super().__init__(message, match)
248
248
  # invert predicate
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
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: tenacity
3
- Version: 9.0.0
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,20 +11,21 @@ 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.8
15
- Classifier: Programming Language :: Python :: 3.9
16
14
  Classifier: Programming Language :: Python :: 3.10
17
15
  Classifier: Programming Language :: Python :: 3.11
18
16
  Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Programming Language :: Python :: 3.14
19
19
  Classifier: Topic :: Utilities
20
- Requires-Python: >=3.8
20
+ Requires-Python: >=3.10
21
21
  License-File: LICENSE
22
22
  Provides-Extra: doc
23
- Requires-Dist: reno ; extra == 'doc'
24
- Requires-Dist: sphinx ; extra == 'doc'
23
+ Requires-Dist: reno; extra == "doc"
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
+ Requires-Dist: pytest; extra == "test"
27
+ Requires-Dist: tornado>=4.5; extra == "test"
28
+ Requires-Dist: typeguard; extra == "test"
29
+ Dynamic: license-file
29
30
 
30
31
  Tenacity is a general-purpose retrying library to simplify the task of adding retry behavior to just about anything.
@@ -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: bdist_wheel (0.43.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=VEqa73Lqmwd0kWnyf1nDCRvp4JthP9RwUiNQZmfnTTw,24021
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=1oye0erAqJkSIysEQIfgtZXHunv7t31BwI2gHL1FsSk,8049
12
- tenacity/asyncio/__init__.py,sha256=PoDGzHN-neTr2GgA1Ti3ORMrSmUIEUblbiSiuyxKHTI,7773
13
- tenacity/asyncio/retry.py,sha256=ymu8F1JfAerc5vDO0V4-2kCvHXqyce2jpO1Nlt6aKxI,4244
14
- tenacity-9.0.0.dist-info/LICENSE,sha256=WNHhf_5RCaeuKWyq_K39vmp9F28LxKsB4SpomwSZ2L0,11357
15
- tenacity-9.0.0.dist-info/METADATA,sha256=S1rIs_pHq1DxIotdWMKlpb1u4Dy0o2nzFmlrDeNBN1M,1155
16
- tenacity-9.0.0.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
17
- tenacity-9.0.0.dist-info/top_level.txt,sha256=Zf8AOZMN7hr1EEcUo9U5KzXsM4TOC1pBZ22D8913JYs,9
18
- tenacity-9.0.0.dist-info/RECORD,,