xtr-http-client 5.2.0__tar.gz → 6.0.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.
- {xtr_http_client-5.2.0 → xtr_http_client-6.0.0}/PKG-INFO +9 -6
- {xtr_http_client-5.2.0 → xtr_http_client-6.0.0}/README.md +3 -0
- {xtr_http_client-5.2.0 → xtr_http_client-6.0.0}/pyproject.toml +11 -10
- {xtr_http_client-5.2.0 → xtr_http_client-6.0.0}/pyproject.toml.orig +12 -10
- {xtr_http_client-5.2.0 → xtr_http_client-6.0.0}/src/xtr_http_client/httpx_http_client.py +17 -42
- {xtr_http_client-5.2.0 → xtr_http_client-6.0.0}/src/xtr_http_client/internal/httpx_clients.py +11 -2
- xtr_http_client-6.0.0/src/xtr_http_client/internal/httpx_redirect.py +109 -0
- xtr_http_client-5.2.0/src/xtr_http_client/internal/httpx_redirect.py +0 -63
- {xtr_http_client-5.2.0 → xtr_http_client-6.0.0}/LICENSE +0 -0
- {xtr_http_client-5.2.0 → xtr_http_client-6.0.0}/src/xtr_http_client/.agents/skills/xtr-http-client/SKILL.md +0 -0
- {xtr_http_client-5.2.0 → xtr_http_client-6.0.0}/src/xtr_http_client/__init__.py +0 -0
- {xtr_http_client-5.2.0 → xtr_http_client-6.0.0}/src/xtr_http_client/bundle/__init__.py +0 -0
- {xtr_http_client-5.2.0 → xtr_http_client-6.0.0}/src/xtr_http_client/bundle/_clients.py +0 -0
- {xtr_http_client-5.2.0 → xtr_http_client-6.0.0}/src/xtr_http_client/bundle/client_options_config.py +0 -0
- {xtr_http_client-5.2.0 → xtr_http_client-6.0.0}/src/xtr_http_client/bundle/http_client_bundle.py +0 -0
- {xtr_http_client-5.2.0 → xtr_http_client-6.0.0}/src/xtr_http_client/bundle/http_client_config.py +0 -0
- {xtr_http_client-5.2.0 → xtr_http_client-6.0.0}/src/xtr_http_client/bundle/retry_failed_config.py +0 -0
- {xtr_http_client-5.2.0 → xtr_http_client-6.0.0}/src/xtr_http_client/bundle/scoped_client_config.py +0 -0
- {xtr_http_client-5.2.0 → xtr_http_client-6.0.0}/src/xtr_http_client/exception/__init__.py +0 -0
- {xtr_http_client-5.2.0 → xtr_http_client-6.0.0}/src/xtr_http_client/exception/_http_error_message.py +0 -0
- {xtr_http_client-5.2.0 → xtr_http_client-6.0.0}/src/xtr_http_client/exception/logic_error.py +0 -0
- {xtr_http_client-5.2.0 → xtr_http_client-6.0.0}/src/xtr_http_client/http_client.py +0 -0
- {xtr_http_client-5.2.0 → xtr_http_client-6.0.0}/src/xtr_http_client/http_client_mixin.py +0 -0
- {xtr_http_client-5.2.0 → xtr_http_client-6.0.0}/src/xtr_http_client/internal/__init__.py +0 -0
- {xtr_http_client-5.2.0 → xtr_http_client-6.0.0}/src/xtr_http_client/internal/debug_type.py +0 -0
- {xtr_http_client-5.2.0 → xtr_http_client-6.0.0}/src/xtr_http_client/internal/httpx_body.py +0 -0
- {xtr_http_client-5.2.0 → xtr_http_client-6.0.0}/src/xtr_http_client/internal/httpx_errors.py +0 -0
- {xtr_http_client-5.2.0 → xtr_http_client-6.0.0}/src/xtr_http_client/internal/httpx_request.py +0 -0
- {xtr_http_client-5.2.0 → xtr_http_client-6.0.0}/src/xtr_http_client/internal/mock_head.py +0 -0
- {xtr_http_client-5.2.0 → xtr_http_client-6.0.0}/src/xtr_http_client/internal/mock_info.py +0 -0
- {xtr_http_client-5.2.0 → xtr_http_client-6.0.0}/src/xtr_http_client/internal/option_merge.py +0 -0
- {xtr_http_client-5.2.0 → xtr_http_client-6.0.0}/src/xtr_http_client/internal/response_body.py +0 -0
- {xtr_http_client-5.2.0 → xtr_http_client-6.0.0}/src/xtr_http_client/internal/retry_count_response.py +0 -0
- {xtr_http_client-5.2.0 → xtr_http_client-6.0.0}/src/xtr_http_client/internal/retry_delay.py +0 -0
- {xtr_http_client-5.2.0 → xtr_http_client-6.0.0}/src/xtr_http_client/internal/retry_options.py +0 -0
- {xtr_http_client-5.2.0 → xtr_http_client-6.0.0}/src/xtr_http_client/mock_http_client.py +0 -0
- {xtr_http_client-5.2.0 → xtr_http_client-6.0.0}/src/xtr_http_client/no_private_network_http_client.py +0 -0
- {xtr_http_client-5.2.0 → xtr_http_client-6.0.0}/src/xtr_http_client/py.typed +0 -0
- {xtr_http_client-5.2.0 → xtr_http_client-6.0.0}/src/xtr_http_client/recipe/__init__.py +0 -0
- {xtr_http_client-5.2.0 → xtr_http_client-6.0.0}/src/xtr_http_client/recipe/files/config/http_client.py.tmpl +0 -0
- {xtr_http_client-5.2.0 → xtr_http_client-6.0.0}/src/xtr_http_client/recipe/manifest.toml +0 -0
- {xtr_http_client-5.2.0 → xtr_http_client-6.0.0}/src/xtr_http_client/response/__init__.py +0 -0
- {xtr_http_client-5.2.0 → xtr_http_client-6.0.0}/src/xtr_http_client/response/http_response.py +0 -0
- {xtr_http_client-5.2.0 → xtr_http_client-6.0.0}/src/xtr_http_client/response/httpx_response.py +0 -0
- {xtr_http_client-5.2.0 → xtr_http_client-6.0.0}/src/xtr_http_client/response/json_mock_response.py +0 -0
- {xtr_http_client-5.2.0 → xtr_http_client-6.0.0}/src/xtr_http_client/response/mock_response.py +0 -0
- {xtr_http_client-5.2.0 → xtr_http_client-6.0.0}/src/xtr_http_client/retry/__init__.py +0 -0
- {xtr_http_client-5.2.0 → xtr_http_client-6.0.0}/src/xtr_http_client/retry/generic_retry_strategy.py +0 -0
- {xtr_http_client-5.2.0 → xtr_http_client-6.0.0}/src/xtr_http_client/retry/retry_context.py +0 -0
- {xtr_http_client-5.2.0 → xtr_http_client-6.0.0}/src/xtr_http_client/retry/retry_strategy_interface.py +0 -0
- {xtr_http_client-5.2.0 → xtr_http_client-6.0.0}/src/xtr_http_client/retryable_http_client.py +0 -0
- {xtr_http_client-5.2.0 → xtr_http_client-6.0.0}/src/xtr_http_client/scoping_http_client.py +0 -0
- {xtr_http_client-5.2.0 → xtr_http_client-6.0.0}/src/xtr_http_client/throttling_http_client.py +0 -0
- {xtr_http_client-5.2.0 → xtr_http_client-6.0.0}/src/xtr_http_client/traceable_http_client.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: xtr-http-client
|
|
3
|
-
Version:
|
|
3
|
+
Version: 6.0.0
|
|
4
4
|
Summary: An async HTTP client behind one interface: typed request options, bodies read on demand, scoped clients, a private-network guard and a mock client for tests.
|
|
5
5
|
Keywords: http,http-client,request,response,mock,asyncio
|
|
6
6
|
Author: Xterr
|
|
@@ -19,11 +19,11 @@ Classifier: Typing :: Typed
|
|
|
19
19
|
Requires-Dist: typing-extensions>=4.4
|
|
20
20
|
Requires-Dist: anyio>=4.0
|
|
21
21
|
Requires-Dist: httpx>=0.27
|
|
22
|
-
Requires-Dist: xtr-http-client-contracts>=
|
|
23
|
-
Requires-Dist: xtr-http-message>=
|
|
24
|
-
Requires-Dist: xtr-http-message-contracts>=
|
|
25
|
-
Requires-Dist: xtr-logging-contracts>=
|
|
26
|
-
Requires-Dist: xtr-service-contracts>=
|
|
22
|
+
Requires-Dist: xtr-http-client-contracts>=6.0,<7
|
|
23
|
+
Requires-Dist: xtr-http-message>=6.0,<7
|
|
24
|
+
Requires-Dist: xtr-http-message-contracts>=6.0,<7
|
|
25
|
+
Requires-Dist: xtr-logging-contracts>=6.0,<7
|
|
26
|
+
Requires-Dist: xtr-service-contracts>=6.0,<7
|
|
27
27
|
Requires-Dist: httpx[http2]>=0.27 ; extra == 'http2'
|
|
28
28
|
Requires-Python: >=3.11
|
|
29
29
|
Provides-Extra: http2
|
|
@@ -142,6 +142,9 @@ the rest. An option this client does not know is refused with `InvalidArgumentEr
|
|
|
142
142
|
the closest one it does, because an option spelled wrong that was quietly dropped is a setting
|
|
143
143
|
that silently never applied.
|
|
144
144
|
|
|
145
|
+
A host `resolve` pins to an address stays pinned for the requests after it; `reset()` forgets
|
|
146
|
+
every pin and keeps the connections open.
|
|
147
|
+
|
|
145
148
|
A `body` mapping holding a file opened `"rb"` is uploaded as `multipart/form-data` — plain
|
|
146
149
|
values as `data`, each file as a part named after its file and typed from that name — and a
|
|
147
150
|
mapping without one stays url-encoded:
|
|
@@ -111,6 +111,9 @@ the rest. An option this client does not know is refused with `InvalidArgumentEr
|
|
|
111
111
|
the closest one it does, because an option spelled wrong that was quietly dropped is a setting
|
|
112
112
|
that silently never applied.
|
|
113
113
|
|
|
114
|
+
A host `resolve` pins to an address stays pinned for the requests after it; `reset()` forgets
|
|
115
|
+
every pin and keeps the connections open.
|
|
116
|
+
|
|
114
117
|
A `body` mapping holding a file opened `"rb"` is uploaded as `multipart/form-data` — plain
|
|
115
118
|
values as `data`, each file as a part named after its file and typed from that name — and a
|
|
116
119
|
mapping without one stays url-encoded:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "xtr-http-client"
|
|
3
|
-
version = "
|
|
3
|
+
version = "6.0.0"
|
|
4
4
|
description = "An async HTTP client behind one interface: typed request options, bodies read on demand, scoped clients, a private-network guard and a mock client for tests."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.11"
|
|
@@ -29,11 +29,11 @@ dependencies = [
|
|
|
29
29
|
"typing-extensions>=4.4",
|
|
30
30
|
"anyio>=4.0",
|
|
31
31
|
"httpx>=0.27",
|
|
32
|
-
"xtr-http-client-contracts>=
|
|
33
|
-
"xtr-http-message>=
|
|
34
|
-
"xtr-http-message-contracts>=
|
|
35
|
-
"xtr-logging-contracts>=
|
|
36
|
-
"xtr-service-contracts>=
|
|
32
|
+
"xtr-http-client-contracts>=6.0,<7",
|
|
33
|
+
"xtr-http-message>=6.0,<7",
|
|
34
|
+
"xtr-http-message-contracts>=6.0,<7",
|
|
35
|
+
"xtr-logging-contracts>=6.0,<7",
|
|
36
|
+
"xtr-service-contracts>=6.0,<7",
|
|
37
37
|
]
|
|
38
38
|
|
|
39
39
|
[[project.authors]]
|
|
@@ -58,10 +58,11 @@ dev = [
|
|
|
58
58
|
"anyio>=4.0",
|
|
59
59
|
"h2>=4.0",
|
|
60
60
|
"ty>=0.0.83",
|
|
61
|
-
"xtr-clock>=
|
|
62
|
-
"xtr-dependency-injection>=
|
|
63
|
-
"xtr-http-foundation>=
|
|
64
|
-
"xtr-
|
|
61
|
+
"xtr-clock>=6.0,<7",
|
|
62
|
+
"xtr-dependency-injection>=6.0,<7",
|
|
63
|
+
"xtr-http-foundation>=6.0,<7",
|
|
64
|
+
"xtr-messenger>=6.0,<7",
|
|
65
|
+
"xtr-rate-limiter>=6.0,<7",
|
|
65
66
|
]
|
|
66
67
|
|
|
67
68
|
[build-system]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "xtr-http-client"
|
|
3
|
-
version = "
|
|
3
|
+
version = "6.0.0"
|
|
4
4
|
description = "An async HTTP client behind one interface: typed request options, bodies read on demand, scoped clients, a private-network guard and a mock client for tests."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [
|
|
@@ -27,11 +27,11 @@ dependencies = [
|
|
|
27
27
|
"typing-extensions>=4.4",
|
|
28
28
|
"anyio>=4.0",
|
|
29
29
|
"httpx>=0.27",
|
|
30
|
-
"xtr-http-client-contracts>=
|
|
31
|
-
"xtr-http-message>=
|
|
32
|
-
"xtr-http-message-contracts>=
|
|
33
|
-
"xtr-logging-contracts>=
|
|
34
|
-
"xtr-service-contracts>=
|
|
30
|
+
"xtr-http-client-contracts>=6.0,<7",
|
|
31
|
+
"xtr-http-message>=6.0,<7",
|
|
32
|
+
"xtr-http-message-contracts>=6.0,<7",
|
|
33
|
+
"xtr-logging-contracts>=6.0,<7",
|
|
34
|
+
"xtr-service-contracts>=6.0,<7",
|
|
35
35
|
]
|
|
36
36
|
|
|
37
37
|
[project.optional-dependencies]
|
|
@@ -58,11 +58,13 @@ dev = [
|
|
|
58
58
|
"anyio>=4.0",
|
|
59
59
|
"h2>=4.0",
|
|
60
60
|
"ty>=0.0.83",
|
|
61
|
-
"xtr-clock>=
|
|
62
|
-
"xtr-dependency-injection>=
|
|
61
|
+
"xtr-clock>=6.0,<7",
|
|
62
|
+
"xtr-dependency-injection>=6.0,<7",
|
|
63
63
|
# The networks NoPrivateNetworkHttpClient refuses; optional for an application.
|
|
64
|
-
"xtr-http-foundation>=
|
|
65
|
-
|
|
64
|
+
"xtr-http-foundation>=6.0,<7",
|
|
65
|
+
# A worker resets the clients between the messages it handles in the kernel tests.
|
|
66
|
+
"xtr-messenger>=6.0,<7",
|
|
67
|
+
"xtr-rate-limiter>=6.0,<7",
|
|
66
68
|
]
|
|
67
69
|
|
|
68
70
|
[build-system]
|
|
@@ -10,11 +10,12 @@ import httpx
|
|
|
10
10
|
from typing_extensions import Unpack, override
|
|
11
11
|
from xtr_http_client_contracts import HttpClientInterface
|
|
12
12
|
from xtr_http_message import Request
|
|
13
|
+
from xtr_service_contracts import ResetInterface
|
|
13
14
|
|
|
14
15
|
from .http_client_mixin import HttpClientMixin
|
|
15
16
|
from .internal.httpx_clients import HttpxClients
|
|
16
17
|
from .internal.httpx_errors import timed_out, transport_error
|
|
17
|
-
from .internal.httpx_redirect import
|
|
18
|
+
from .internal.httpx_redirect import exchange
|
|
18
19
|
from .internal.httpx_request import (
|
|
19
20
|
assemble_headers,
|
|
20
21
|
assemble_payload,
|
|
@@ -38,7 +39,7 @@ __all__ = ["HttpxHttpClient"]
|
|
|
38
39
|
|
|
39
40
|
|
|
40
41
|
@final
|
|
41
|
-
class HttpxHttpClient(HttpClientMixin, HttpClientInterface):
|
|
42
|
+
class HttpxHttpClient(HttpClientMixin, HttpClientInterface, ResetInterface):
|
|
42
43
|
"""Sends requests over real connections, and hands back answers as they arrive.
|
|
43
44
|
|
|
44
45
|
A request returns once the status line and the headers are in, whatever
|
|
@@ -51,8 +52,10 @@ class HttpxHttpClient(HttpClientMixin, HttpClientInterface):
|
|
|
51
52
|
has no ``Authorization`` of its own — which is what lets one set of
|
|
52
53
|
client-wide defaults sit under requests that each know their own mind.
|
|
53
54
|
What each part of an outgoing request is assembled from is in
|
|
54
|
-
:mod:`xtr_http_client.internal.httpx_request
|
|
55
|
-
exchange
|
|
55
|
+
:mod:`xtr_http_client.internal.httpx_request`, and the redirect chain an
|
|
56
|
+
exchange may turn into is followed in
|
|
57
|
+
:mod:`xtr_http_client.internal.httpx_redirect`; what is here is the
|
|
58
|
+
exchange itself, under its deadline.
|
|
56
59
|
|
|
57
60
|
Connections are pooled by how they connect rather than by host: a proxy,
|
|
58
61
|
a trust store, a protocol version and the deadlines all decide what a
|
|
@@ -60,6 +63,10 @@ class HttpxHttpClient(HttpClientMixin, HttpClientInterface):
|
|
|
60
63
|
``max_host_connections`` is each pool's ceiling across every host it
|
|
61
64
|
reaches.
|
|
62
65
|
|
|
66
|
+
A host a request pins to an address through ``resolve`` stays pinned for
|
|
67
|
+
the requests after it, in the pool it went through; :meth:`reset` forgets
|
|
68
|
+
every pin, and keeps the connections open until the client is closed.
|
|
69
|
+
|
|
63
70
|
```python
|
|
64
71
|
async with HttpxHttpClient(max_host_connections=4) as client:
|
|
65
72
|
response = await client.request("GET", "https://example.com/orders")
|
|
@@ -164,6 +171,11 @@ class HttpxHttpClient(HttpClientMixin, HttpClientInterface):
|
|
|
164
171
|
|
|
165
172
|
return await self._send(client, outgoing, merged, headers)
|
|
166
173
|
|
|
174
|
+
@override
|
|
175
|
+
async def reset(self) -> None:
|
|
176
|
+
"""Forget the address every host was pinned to, keeping the connections open."""
|
|
177
|
+
self._clients.reset()
|
|
178
|
+
|
|
167
179
|
async def aclose(self) -> None:
|
|
168
180
|
"""Close every pool, and with it every connection this client holds."""
|
|
169
181
|
await self._clients.aclose()
|
|
@@ -207,7 +219,7 @@ class HttpxHttpClient(HttpClientMixin, HttpClientInterface):
|
|
|
207
219
|
|
|
208
220
|
try:
|
|
209
221
|
with anyio.fail_after(limit):
|
|
210
|
-
response, history = await
|
|
222
|
+
response, history = await exchange(
|
|
211
223
|
client,
|
|
212
224
|
outgoing,
|
|
213
225
|
_basic_credentials(options, headers),
|
|
@@ -229,43 +241,6 @@ class HttpxHttpClient(HttpClientMixin, HttpClientInterface):
|
|
|
229
241
|
)
|
|
230
242
|
|
|
231
243
|
|
|
232
|
-
async def _exchange(
|
|
233
|
-
client: httpx.AsyncClient,
|
|
234
|
-
outgoing: httpx.Request,
|
|
235
|
-
auth: tuple[str, str] | None,
|
|
236
|
-
max_redirects: int,
|
|
237
|
-
) -> tuple[httpx.Response, list[httpx.Response]]:
|
|
238
|
-
"""Send, then follow where each answer points until one points nowhere.
|
|
239
|
-
|
|
240
|
-
Following is done here, a hop at a time, for the one thing the backend
|
|
241
|
-
cannot be told: credentials belong to the host they were meant for, so an
|
|
242
|
-
``Authorization`` the backend dropped on the way out of that host is put
|
|
243
|
-
back the moment the chain returns to it, and nowhere else.
|
|
244
|
-
"""
|
|
245
|
-
response = await client.send(outgoing, auth=auth, stream=True, follow_redirects=False)
|
|
246
|
-
history: list[httpx.Response] = []
|
|
247
|
-
origin = response.request.url
|
|
248
|
-
authorization = cast("str | None", response.request.headers.get("authorization"))
|
|
249
|
-
|
|
250
|
-
while len(history) < max_redirects:
|
|
251
|
-
following = next_hop(response)
|
|
252
|
-
if following is None:
|
|
253
|
-
break
|
|
254
|
-
|
|
255
|
-
if (
|
|
256
|
-
authorization is not None
|
|
257
|
-
and "authorization" not in following.headers
|
|
258
|
-
and same_origin(following.url, origin)
|
|
259
|
-
):
|
|
260
|
-
following.headers["Authorization"] = authorization
|
|
261
|
-
|
|
262
|
-
history.append(response)
|
|
263
|
-
await response.aclose()
|
|
264
|
-
response = await client.send(following, stream=True, follow_redirects=False)
|
|
265
|
-
|
|
266
|
-
return response, history
|
|
267
|
-
|
|
268
|
-
|
|
269
244
|
def _reachable(url: str) -> str:
|
|
270
245
|
"""``url`` with every part of it written in the form that reaches it.
|
|
271
246
|
|
{xtr_http_client-5.2.0 → xtr_http_client-6.0.0}/src/xtr_http_client/internal/httpx_clients.py
RENAMED
|
@@ -43,8 +43,8 @@ class HttpxClients:
|
|
|
43
43
|
change in them asks for a different pool rather than being ignored.
|
|
44
44
|
|
|
45
45
|
Each pool also remembers the addresses that requests pinned their hosts
|
|
46
|
-
to,
|
|
47
|
-
reached at it again, without asking a resolver a second time.
|
|
46
|
+
to, until :meth:`reset` or :meth:`aclose`: a host reached at an address
|
|
47
|
+
once is reached at it again, without asking a resolver a second time.
|
|
48
48
|
|
|
49
49
|
``max_connections`` is the backend's pool-wide ceiling, not a per-host
|
|
50
50
|
one: it counts every connection the pool holds, whichever host each of
|
|
@@ -90,6 +90,15 @@ class HttpxClients:
|
|
|
90
90
|
|
|
91
91
|
return client, self._resolved[key]
|
|
92
92
|
|
|
93
|
+
def reset(self) -> None:
|
|
94
|
+
"""Forget the address every host was pinned to, keeping every pool open.
|
|
95
|
+
|
|
96
|
+
A pin is what the requests of one unit of work asked for; the
|
|
97
|
+
connections are how the next one gets anywhere, so they stay.
|
|
98
|
+
"""
|
|
99
|
+
for resolved in self._resolved.values():
|
|
100
|
+
resolved.clear()
|
|
101
|
+
|
|
93
102
|
async def aclose(self) -> None:
|
|
94
103
|
"""Close every pool, and with it every connection any of them holds."""
|
|
95
104
|
clients = list(self._clients.values())
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""Following a redirect chain: where each answer points, and whether it stays where it began."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, cast
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
__all__ = ["exchange", "next_hop", "same_origin"]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
async def exchange(
|
|
14
|
+
client: httpx.AsyncClient,
|
|
15
|
+
outgoing: httpx.Request,
|
|
16
|
+
auth: tuple[str, str] | None,
|
|
17
|
+
max_redirects: int,
|
|
18
|
+
) -> tuple[httpx.Response, list[httpx.Response]]:
|
|
19
|
+
"""Send, then follow where each answer points until one points nowhere.
|
|
20
|
+
|
|
21
|
+
Following is done here, a hop at a time, for the one thing the backend
|
|
22
|
+
cannot be told: credentials belong to the host they were meant for, so an
|
|
23
|
+
``Authorization`` the backend dropped on the way out of that host is put
|
|
24
|
+
back the moment the chain returns to it, and nowhere else.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
client: The pool the request and every hop after it are sent through.
|
|
28
|
+
outgoing: The request to send.
|
|
29
|
+
auth: The user and password the first request authenticates with.
|
|
30
|
+
max_redirects: How many hops may be followed.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
The last answer, and every answer before it, oldest first.
|
|
34
|
+
"""
|
|
35
|
+
response = await client.send(outgoing, auth=auth, stream=True, follow_redirects=False)
|
|
36
|
+
history: list[httpx.Response] = []
|
|
37
|
+
origin = response.request.url
|
|
38
|
+
authorization = cast("str | None", response.request.headers.get("authorization"))
|
|
39
|
+
|
|
40
|
+
while len(history) < max_redirects:
|
|
41
|
+
following = next_hop(response)
|
|
42
|
+
if following is None:
|
|
43
|
+
break
|
|
44
|
+
|
|
45
|
+
if (
|
|
46
|
+
authorization is not None
|
|
47
|
+
and "authorization" not in following.headers
|
|
48
|
+
and same_origin(following.url, origin)
|
|
49
|
+
):
|
|
50
|
+
following.headers["Authorization"] = authorization
|
|
51
|
+
|
|
52
|
+
history.append(response)
|
|
53
|
+
await response.aclose()
|
|
54
|
+
response = await client.send(following, stream=True, follow_redirects=False)
|
|
55
|
+
|
|
56
|
+
return response, history
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def next_hop(response: httpx.Response) -> httpx.Request | None:
|
|
60
|
+
"""Return the request that following ``response`` would make.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
response: The answer to read a redirect off.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
The next request, or ``None`` when there is nowhere to go — either
|
|
67
|
+
because the answer is not a redirect, or because where it points is
|
|
68
|
+
not somewhere a request can be made to.
|
|
69
|
+
"""
|
|
70
|
+
following = response.next_request
|
|
71
|
+
if following is None:
|
|
72
|
+
return None
|
|
73
|
+
location = cast("str", response.headers.get("location", ""))
|
|
74
|
+
if _names_no_host(location) or not following.url.host:
|
|
75
|
+
return None
|
|
76
|
+
|
|
77
|
+
return following
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def same_origin(one: httpx.URL, other: httpx.URL) -> bool:
|
|
81
|
+
"""Tell whether two URLs name the same scheme, host and port.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
one: A URL.
|
|
85
|
+
other: The URL to compare it with.
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
``True`` when a connection to either would be a connection to the
|
|
89
|
+
same place.
|
|
90
|
+
"""
|
|
91
|
+
return (one.scheme, one.host, one.port) == (other.scheme, other.host, other.port)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _names_no_host(location: str) -> bool:
|
|
95
|
+
"""Whether a Location names an authority and then leaves it empty.
|
|
96
|
+
|
|
97
|
+
``//?foo=bar`` says "another host, the same scheme" and names no host, so
|
|
98
|
+
there is nowhere for it to point; a relative reference such as ``..``
|
|
99
|
+
names no authority at all, which is not the same thing — it is resolved
|
|
100
|
+
against the URL it arrived on.
|
|
101
|
+
"""
|
|
102
|
+
if not location.startswith("//"):
|
|
103
|
+
return False
|
|
104
|
+
|
|
105
|
+
authority = location[2:]
|
|
106
|
+
for delimiter in ("/", "?", "#"):
|
|
107
|
+
authority = authority.partition(delimiter)[0]
|
|
108
|
+
|
|
109
|
+
return authority == ""
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
"""Where an answer points, and whether following it stays where it started."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
from typing import TYPE_CHECKING, cast
|
|
6
|
-
|
|
7
|
-
if TYPE_CHECKING:
|
|
8
|
-
import httpx
|
|
9
|
-
|
|
10
|
-
__all__ = ["next_hop", "same_origin"]
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
def next_hop(response: httpx.Response) -> httpx.Request | None:
|
|
14
|
-
"""Return the request that following ``response`` would make.
|
|
15
|
-
|
|
16
|
-
Args:
|
|
17
|
-
response: The answer to read a redirect off.
|
|
18
|
-
|
|
19
|
-
Returns:
|
|
20
|
-
The next request, or ``None`` when there is nowhere to go — either
|
|
21
|
-
because the answer is not a redirect, or because where it points is
|
|
22
|
-
not somewhere a request can be made to.
|
|
23
|
-
"""
|
|
24
|
-
following = response.next_request
|
|
25
|
-
if following is None:
|
|
26
|
-
return None
|
|
27
|
-
location = cast("str", response.headers.get("location", ""))
|
|
28
|
-
if _names_no_host(location) or not following.url.host:
|
|
29
|
-
return None
|
|
30
|
-
|
|
31
|
-
return following
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
def same_origin(one: httpx.URL, other: httpx.URL) -> bool:
|
|
35
|
-
"""Tell whether two URLs name the same scheme, host and port.
|
|
36
|
-
|
|
37
|
-
Args:
|
|
38
|
-
one: A URL.
|
|
39
|
-
other: The URL to compare it with.
|
|
40
|
-
|
|
41
|
-
Returns:
|
|
42
|
-
``True`` when a connection to either would be a connection to the
|
|
43
|
-
same place.
|
|
44
|
-
"""
|
|
45
|
-
return (one.scheme, one.host, one.port) == (other.scheme, other.host, other.port)
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
def _names_no_host(location: str) -> bool:
|
|
49
|
-
"""Whether a Location names an authority and then leaves it empty.
|
|
50
|
-
|
|
51
|
-
``//?foo=bar`` says "another host, the same scheme" and names no host, so
|
|
52
|
-
there is nowhere for it to point; a relative reference such as ``..``
|
|
53
|
-
names no authority at all, which is not the same thing — it is resolved
|
|
54
|
-
against the URL it arrived on.
|
|
55
|
-
"""
|
|
56
|
-
if not location.startswith("//"):
|
|
57
|
-
return False
|
|
58
|
-
|
|
59
|
-
authority = location[2:]
|
|
60
|
-
for delimiter in ("/", "?", "#"):
|
|
61
|
-
authority = authority.partition(delimiter)[0]
|
|
62
|
-
|
|
63
|
-
return authority == ""
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{xtr_http_client-5.2.0 → xtr_http_client-6.0.0}/src/xtr_http_client/bundle/client_options_config.py
RENAMED
|
File without changes
|
{xtr_http_client-5.2.0 → xtr_http_client-6.0.0}/src/xtr_http_client/bundle/http_client_bundle.py
RENAMED
|
File without changes
|
{xtr_http_client-5.2.0 → xtr_http_client-6.0.0}/src/xtr_http_client/bundle/http_client_config.py
RENAMED
|
File without changes
|
{xtr_http_client-5.2.0 → xtr_http_client-6.0.0}/src/xtr_http_client/bundle/retry_failed_config.py
RENAMED
|
File without changes
|
{xtr_http_client-5.2.0 → xtr_http_client-6.0.0}/src/xtr_http_client/bundle/scoped_client_config.py
RENAMED
|
File without changes
|
|
File without changes
|
{xtr_http_client-5.2.0 → xtr_http_client-6.0.0}/src/xtr_http_client/exception/_http_error_message.py
RENAMED
|
File without changes
|
{xtr_http_client-5.2.0 → xtr_http_client-6.0.0}/src/xtr_http_client/exception/logic_error.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{xtr_http_client-5.2.0 → xtr_http_client-6.0.0}/src/xtr_http_client/internal/httpx_errors.py
RENAMED
|
File without changes
|
{xtr_http_client-5.2.0 → xtr_http_client-6.0.0}/src/xtr_http_client/internal/httpx_request.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{xtr_http_client-5.2.0 → xtr_http_client-6.0.0}/src/xtr_http_client/internal/option_merge.py
RENAMED
|
File without changes
|
{xtr_http_client-5.2.0 → xtr_http_client-6.0.0}/src/xtr_http_client/internal/response_body.py
RENAMED
|
File without changes
|
{xtr_http_client-5.2.0 → xtr_http_client-6.0.0}/src/xtr_http_client/internal/retry_count_response.py
RENAMED
|
File without changes
|
|
File without changes
|
{xtr_http_client-5.2.0 → xtr_http_client-6.0.0}/src/xtr_http_client/internal/retry_options.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{xtr_http_client-5.2.0 → xtr_http_client-6.0.0}/src/xtr_http_client/response/http_response.py
RENAMED
|
File without changes
|
{xtr_http_client-5.2.0 → xtr_http_client-6.0.0}/src/xtr_http_client/response/httpx_response.py
RENAMED
|
File without changes
|
{xtr_http_client-5.2.0 → xtr_http_client-6.0.0}/src/xtr_http_client/response/json_mock_response.py
RENAMED
|
File without changes
|
{xtr_http_client-5.2.0 → xtr_http_client-6.0.0}/src/xtr_http_client/response/mock_response.py
RENAMED
|
File without changes
|
|
File without changes
|
{xtr_http_client-5.2.0 → xtr_http_client-6.0.0}/src/xtr_http_client/retry/generic_retry_strategy.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{xtr_http_client-5.2.0 → xtr_http_client-6.0.0}/src/xtr_http_client/retryable_http_client.py
RENAMED
|
File without changes
|
|
File without changes
|
{xtr_http_client-5.2.0 → xtr_http_client-6.0.0}/src/xtr_http_client/throttling_http_client.py
RENAMED
|
File without changes
|
{xtr_http_client-5.2.0 → xtr_http_client-6.0.0}/src/xtr_http_client/traceable_http_client.py
RENAMED
|
File without changes
|