httpx-tenacity 0.1.0__tar.gz → 0.2.0__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: httpx-tenacity
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: A lightweight package that provides tenacious (retrying) httpx transports.
5
5
  Project-URL: Homepage, https://github.com/Midnighter/httpx-tenacity
6
6
  Project-URL: Documentation, https://midnighter.github.io/httpx-tenacity/
@@ -131,6 +131,9 @@ case-sensitive = true
131
131
  known-first-party = ["src", "httpx_tenacity"]
132
132
  lines-after-imports = 2
133
133
 
134
+ [tool.ruff.lint.pylint]
135
+ max-args = 6
136
+
134
137
  [tool.pydoclint]
135
138
  style = "google"
136
139
  arg-type-hints-in-docstring = false
@@ -142,6 +145,9 @@ exclude = "_version.py"
142
145
  # Hatch Environments
143
146
  ################################################################################
144
147
 
148
+ [tool.hatch.env]
149
+ requires = ["hatch-pip-compile"]
150
+
145
151
  [tool.hatch.envs.default]
146
152
  description = """
147
153
  Base environment with default configuration values.
@@ -149,7 +155,17 @@ Base environment with default configuration values.
149
155
  All environments inheriting from the default also inherit the configuration values set
150
156
  here.
151
157
  """
152
- installer = "uv"
158
+ type = "pip-compile"
159
+ lock-filename = "requirements/requirements-{env_name}.txt"
160
+ pip-compile-constraint = "dev"
161
+ pip-compile-hashes = true
162
+ pip-compile-resolver = "uv"
163
+ pip-compile-installer = "uv"
164
+
165
+ [tool.hatch.envs.no-install]
166
+ description = """A base environment for use cases that do not install the project."""
167
+ skip-install = true
168
+ features = []
153
169
 
154
170
  [tool.hatch.envs.dev]
155
171
  description = """Development environment."""
@@ -161,9 +177,8 @@ PYTHONASYNCIODEBUG = "1"
161
177
 
162
178
  [tool.hatch.envs.style]
163
179
  description = """Check the style of the codebase."""
180
+ template = "no-install"
164
181
  dependency-groups = ["style"]
165
- detached = true
166
- installer = "uv"
167
182
 
168
183
  [tool.hatch.envs.style.scripts]
169
184
  docstrings = ["pydoclint src", "pydoclint tests"]
@@ -197,9 +212,8 @@ deploy = "mike deploy {args}"
197
212
 
198
213
  [tool.hatch.envs.distribution]
199
214
  description = """Test the source and wheel distribution of the package."""
215
+ template = "no-install"
200
216
  dependencies = ["pip", "twine"]
201
- detached = true
202
- installer = "uv"
203
217
  builder = true
204
218
 
205
219
  [tool.hatch.envs.distribution.scripts]
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
28
28
  commit_id: COMMIT_ID
29
29
  __commit_id__: COMMIT_ID
30
30
 
31
- __version__ = version = '0.1.0'
32
- __version_tuple__ = version_tuple = (0, 1, 0)
31
+ __version__ = version = '0.2.0'
32
+ __version_tuple__ = version_tuple = (0, 2, 0)
33
33
 
34
34
  __commit_id__ = commit_id = None
@@ -39,7 +39,6 @@ if TYPE_CHECKING: # pragma: no cover
39
39
 
40
40
  from .types import HTTPXHTTPTransportKeywordArguments
41
41
 
42
-
43
42
  logger = logging.getLogger(__name__)
44
43
 
45
44
 
@@ -71,6 +70,7 @@ class AsyncTenaciousTransport(httpx.AsyncBaseTransport):
71
70
  max_wait_seconds: float | timedelta = 60,
72
71
  min_wait_seconds: float | timedelta = 0.02,
73
72
  exponent_base: float = 2,
73
+ transport: httpx.AsyncBaseTransport | None = None,
74
74
  **kwargs: Unpack[HTTPXHTTPTransportKeywordArguments],
75
75
  ) -> AsyncTenaciousTransport:
76
76
  """
@@ -90,6 +90,8 @@ class AsyncTenaciousTransport(httpx.AsyncBaseTransport):
90
90
  max_wait_seconds: Maximum wait time between retries.
91
91
  min_wait_seconds: Minimum wait time between retries.
92
92
  exponent_base: The base for the exponential backoff.
93
+ transport: Pass an asynchronous transport instance to handle requests
94
+ instead of the default one.
93
95
  **kwargs: Additional keyword arguments are used in the construction of an
94
96
  `httpx.AsyncHTTPTransport`.
95
97
 
@@ -99,7 +101,8 @@ class AsyncTenaciousTransport(httpx.AsyncBaseTransport):
99
101
  """
100
102
  return cls(
101
103
  retry=tenacity.AsyncRetrying(
102
- retry=tenacity.retry_if_result(is_server_side_issue),
104
+ retry=tenacity.retry_if_exception_type(httpx.RequestError)
105
+ | tenacity.retry_if_result(is_server_side_issue),
103
106
  stop=tenacity.stop_after_attempt(max_attempts),
104
107
  wait=smart_wait(
105
108
  multiplier=multiplier,
@@ -107,10 +110,12 @@ class AsyncTenaciousTransport(httpx.AsyncBaseTransport):
107
110
  min=min_wait_seconds,
108
111
  exp_base=exponent_base,
109
112
  ),
110
- before_sleep=tenacity.before_sleep_log(logger, logging.DEBUG),
113
+ before_sleep=tenacity.before_sleep_log(logger, logging.DEBUG), # type: ignore[invalid-argument-type]
111
114
  reraise=True,
112
115
  ),
113
- transport=httpx.AsyncHTTPTransport(**kwargs),
116
+ transport=httpx.AsyncHTTPTransport(**kwargs)
117
+ if transport is None
118
+ else transport,
114
119
  )
115
120
 
116
121
  async def handle_async_request(
@@ -39,7 +39,6 @@ if TYPE_CHECKING: # pragma: no cover
39
39
 
40
40
  from .types import HTTPXHTTPTransportKeywordArguments
41
41
 
42
-
43
42
  logger = logging.getLogger(__name__)
44
43
 
45
44
 
@@ -71,6 +70,7 @@ class TenaciousTransport(httpx.BaseTransport):
71
70
  max_wait_seconds: float | timedelta = 60,
72
71
  min_wait_seconds: float | timedelta = 0.02,
73
72
  exponent_base: float = 2,
73
+ transport: httpx.BaseTransport | None = None,
74
74
  **kwargs: Unpack[HTTPXHTTPTransportKeywordArguments],
75
75
  ) -> TenaciousTransport:
76
76
  """
@@ -90,6 +90,8 @@ class TenaciousTransport(httpx.BaseTransport):
90
90
  max_wait_seconds: Maximum wait time between retries.
91
91
  min_wait_seconds: Minimum wait time between retries.
92
92
  exponent_base: The base for the exponential backoff.
93
+ transport: Pass a transport instance to handle requests instead of the
94
+ default one.
93
95
  **kwargs: Additional keyword arguments are used in the construction of an
94
96
  `httpx.HTTPTransport`.
95
97
 
@@ -99,7 +101,8 @@ class TenaciousTransport(httpx.BaseTransport):
99
101
  """
100
102
  return cls(
101
103
  retry=tenacity.Retrying(
102
- retry=tenacity.retry_if_result(is_server_side_issue),
104
+ retry=tenacity.retry_if_exception_type(httpx.RequestError)
105
+ | tenacity.retry_if_result(is_server_side_issue),
103
106
  stop=tenacity.stop_after_attempt(max_attempts),
104
107
  wait=smart_wait(
105
108
  multiplier=multiplier,
@@ -107,10 +110,10 @@ class TenaciousTransport(httpx.BaseTransport):
107
110
  min=min_wait_seconds,
108
111
  exp_base=exponent_base,
109
112
  ),
110
- before_sleep=tenacity.before_sleep_log(logger, logging.DEBUG),
113
+ before_sleep=tenacity.before_sleep_log(logger, logging.DEBUG), # type: ignore[invalid-argument-type]
111
114
  reraise=True,
112
115
  ),
113
- transport=httpx.HTTPTransport(**kwargs),
116
+ transport=httpx.HTTPTransport(**kwargs) if transport is None else transport,
114
117
  )
115
118
 
116
119
  def handle_request(
@@ -0,0 +1,85 @@
1
+ # Copyright (c) 2026 Moritz E. Beber
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License"); you may not
4
+ # use this file except in compliance with the License. You may obtain a copy of
5
+ # the License at
6
+ #
7
+ # https://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11
+ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12
+ # License for the specific language governing permissions and limitations under
13
+ # the License.
14
+
15
+
16
+ """Provide custom wait functions."""
17
+
18
+ from datetime import timedelta
19
+ from typing import TYPE_CHECKING
20
+
21
+ import httpx
22
+ import tenacity
23
+ from tenacity.wait import wait_random_exponential
24
+
25
+
26
+ if TYPE_CHECKING:
27
+ from collections.abc import Callable
28
+
29
+
30
+ # We follow tenacity's convention here to use lower snake-case class names for the wait
31
+ # strategies.
32
+ class smart_wait(wait_random_exponential): # noqa: N801
33
+ """
34
+ Define a wait strategy that smartly backs-off.
35
+
36
+ This strategy first checks HTTP responses for an expected status code (by
37
+ default 429) that indicates that the server is busy. If the status code
38
+ matches, it checks the response header (by default 'Retry-After') for a
39
+ wait time indication. Otherwise, it uses random exponential backoff.
40
+
41
+ """
42
+
43
+ def __init__(
44
+ self,
45
+ multiplier: float = 1,
46
+ max: timedelta | float = 60, # noqa: A002
47
+ exp_base: float = 2,
48
+ min: timedelta | float = 0, # noqa: A002
49
+ status_code: int | tuple[int, ...] = httpx.codes.TOO_MANY_REQUESTS,
50
+ header: str = "Retry-After",
51
+ ) -> None:
52
+ super().__init__(
53
+ multiplier=multiplier,
54
+ max=max,
55
+ exp_base=exp_base,
56
+ min=min,
57
+ )
58
+ self._status_code = status_code
59
+ self._check: Callable[[int], bool]
60
+ if isinstance(self._status_code, tuple):
61
+ self._check = lambda code: code in self._status_code # type: ignore[operator]
62
+ else:
63
+ self._check = lambda code: code == self._status_code
64
+ self._header = header
65
+
66
+ def __call__(self, retry_state: tenacity.RetryCallState) -> float:
67
+ """
68
+ Return a wait time in seconds based on response headers or retry state.
69
+
70
+ When we have a successful outcome, i.e., we received an HTTP response
71
+ without encountering an exception, and the HTTP status code of the
72
+ response matches the specified status code, check if the response header
73
+ indicates a wait time; otherwise use the random exponential strategy to
74
+ determine the wait time.
75
+
76
+ """
77
+ if retry_state.outcome and not retry_state.outcome.failed:
78
+ response: httpx.Response = retry_state.outcome.result()
79
+ if (
80
+ self._check(response.status_code)
81
+ and (wait := response.headers.get(self._header)) is not None
82
+ ):
83
+ return float(wait)
84
+
85
+ return super().__call__(retry_state=retry_state)
@@ -1,47 +0,0 @@
1
- # Copyright (c) 2026 Moritz E. Beber
2
- #
3
- # Licensed under the Apache License, Version 2.0 (the "License"); you may not
4
- # use this file except in compliance with the License. You may obtain a copy of
5
- # the License at
6
- #
7
- # https://www.apache.org/licenses/LICENSE-2.0
8
- #
9
- # Unless required by applicable law or agreed to in writing, software
10
- # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11
- # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12
- # License for the specific language governing permissions and limitations under
13
- # the License.
14
-
15
-
16
- """Provide custom wait functions."""
17
-
18
- import httpx
19
- import tenacity
20
- from tenacity.wait import wait_random_exponential
21
-
22
-
23
- # We follow tenacity's convention here to use lower snake-case class names for the wait
24
- # strategies.
25
- class smart_wait(wait_random_exponential): # noqa: N801
26
- """
27
- Wait strategy that smartly backs-off.
28
-
29
- This strategy first checks the Retry-After response header for a wait time
30
- indication and uses random exponential backoff if that is not the case.
31
-
32
- """
33
-
34
- def __call__(self, retry_state: tenacity.RetryCallState) -> float: # noqa: D102
35
- # When we have a successful outcome, i.e., we received an HTTP response without
36
- # encountering a Python exception, check if the response headers indicate a wait
37
- # time; otherwise use the random exponential strategy to determine the wait
38
- # time.
39
- if retry_state.outcome and not retry_state.outcome.failed:
40
- response: httpx.Response = retry_state.outcome.result()
41
- if (
42
- response.status_code == httpx.codes.TOO_MANY_REQUESTS
43
- and (wait := response.headers.get("Retry-After")) is not None
44
- ):
45
- return float(wait)
46
-
47
- return super().__call__(retry_state=retry_state)
File without changes
File without changes