httpx2 2.0.0__tar.gz → 2.1.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.
- {httpx2-2.0.0 → httpx2-2.1.0}/CHANGELOG.md +12 -0
- httpx2-2.1.0/LICENSE.md +13 -0
- {httpx2-2.0.0 → httpx2-2.1.0}/PKG-INFO +18 -31
- {httpx2-2.0.0 → httpx2-2.1.0}/README.md +6 -12
- {httpx2-2.0.0 → httpx2-2.1.0}/httpx2/_auth.py +9 -29
- {httpx2-2.0.0 → httpx2-2.1.0}/httpx2/_client.py +28 -83
- {httpx2-2.0.0 → httpx2-2.1.0}/httpx2/_config.py +3 -13
- {httpx2-2.0.0 → httpx2-2.1.0}/httpx2/_content.py +1 -3
- {httpx2-2.0.0 → httpx2-2.1.0}/httpx2/_decoders.py +36 -23
- {httpx2-2.0.0 → httpx2-2.1.0}/httpx2/_exceptions.py +2 -8
- {httpx2-2.0.0 → httpx2-2.1.0}/httpx2/_main.py +9 -29
- {httpx2-2.0.0 → httpx2-2.1.0}/httpx2/_models.py +15 -54
- {httpx2-2.0.0 → httpx2-2.1.0}/httpx2/_multipart.py +10 -31
- {httpx2-2.0.0 → httpx2-2.1.0}/httpx2/_transports/asgi.py +3 -7
- {httpx2-2.0.0 → httpx2-2.1.0}/httpx2/_transports/base.py +2 -6
- {httpx2-2.0.0 → httpx2-2.1.0}/httpx2/_transports/default.py +2 -4
- {httpx2-2.0.0 → httpx2-2.1.0}/httpx2/_transports/wsgi.py +1 -4
- {httpx2-2.0.0 → httpx2-2.1.0}/httpx2/_types.py +2 -6
- {httpx2-2.0.0 → httpx2-2.1.0}/httpx2/_urlparse.py +11 -36
- {httpx2-2.0.0 → httpx2-2.1.0}/httpx2/_urls.py +6 -21
- {httpx2-2.0.0 → httpx2-2.1.0}/httpx2/_utils.py +2 -8
- {httpx2-2.0.0 → httpx2-2.1.0}/pyproject.toml +9 -8
- {httpx2-2.0.0 → httpx2-2.1.0}/.gitignore +0 -0
- {httpx2-2.0.0 → httpx2-2.1.0}/httpx2/__init__.py +0 -0
- {httpx2-2.0.0 → httpx2-2.1.0}/httpx2/__version__.py +0 -0
- {httpx2-2.0.0 → httpx2-2.1.0}/httpx2/_api.py +0 -0
- {httpx2-2.0.0 → httpx2-2.1.0}/httpx2/_status_codes.py +0 -0
- {httpx2-2.0.0 → httpx2-2.1.0}/httpx2/_transports/__init__.py +0 -0
- {httpx2-2.0.0 → httpx2-2.1.0}/httpx2/_transports/mock.py +0 -0
- {httpx2-2.0.0 → httpx2-2.1.0}/httpx2/py.typed +0 -0
|
@@ -4,6 +4,18 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|
6
6
|
|
|
7
|
+
## 2.1.0 (May 15th, 2026)
|
|
8
|
+
|
|
9
|
+
### Removed
|
|
10
|
+
|
|
11
|
+
* Drop support for Python 3.9. ([#208](https://github.com/pydantic/httpx2/pull/208))
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
* Add support for Python 3.14. ([#208](https://github.com/pydantic/httpx2/pull/208))
|
|
16
|
+
* Use stdlib `compression.zstd` for Zstd decompression on Python 3.14+; fall back to the `zstandard` package on older versions. ([#932](https://github.com/pydantic/httpx2/pull/932))
|
|
17
|
+
* Bundle `LICENSE.md` in the sdist. ([#938](https://github.com/pydantic/httpx2/pull/938))
|
|
18
|
+
|
|
7
19
|
## 2.0.0
|
|
8
20
|
|
|
9
21
|
Official first release of `httpx2`. No changes since `2.0.0b1`.
|
httpx2-2.1.0/LICENSE.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Copyright © 2026 to present Pydantic Services Inc. and individual contributors.
|
|
2
|
+
Copyright © 2019, [Encode OSS Ltd](https://www.encode.io/).
|
|
3
|
+
All rights reserved.
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
|
6
|
+
|
|
7
|
+
* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
|
8
|
+
|
|
9
|
+
* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
|
10
|
+
|
|
11
|
+
* Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
|
|
12
|
+
|
|
13
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: httpx2
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.1.0
|
|
4
4
|
Summary: The next generation HTTP client.
|
|
5
|
-
Project-URL: Changelog, https://github.com/pydantic/httpx2/blob/main/CHANGELOG.md
|
|
5
|
+
Project-URL: Changelog, https://github.com/pydantic/httpx2/blob/main/src/httpx2/CHANGELOG.md
|
|
6
6
|
Project-URL: Homepage, https://github.com/pydantic/httpx2
|
|
7
7
|
Project-URL: Source, https://github.com/pydantic/httpx2
|
|
8
8
|
Author-email: Tom Christie <tom@tomchristie.com>
|
|
@@ -17,16 +17,16 @@ Classifier: License :: OSI Approved :: BSD License
|
|
|
17
17
|
Classifier: Operating System :: OS Independent
|
|
18
18
|
Classifier: Programming Language :: Python :: 3
|
|
19
19
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
20
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
21
20
|
Classifier: Programming Language :: Python :: 3.10
|
|
22
21
|
Classifier: Programming Language :: Python :: 3.11
|
|
23
22
|
Classifier: Programming Language :: Python :: 3.12
|
|
24
23
|
Classifier: Programming Language :: Python :: 3.13
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
25
25
|
Classifier: Topic :: Internet :: WWW/HTTP
|
|
26
|
-
Requires-Python: >=3.
|
|
26
|
+
Requires-Python: >=3.10
|
|
27
27
|
Requires-Dist: anyio
|
|
28
28
|
Requires-Dist: certifi
|
|
29
|
-
Requires-Dist: httpcore2==2.
|
|
29
|
+
Requires-Dist: httpcore2==2.1.0
|
|
30
30
|
Requires-Dist: idna
|
|
31
31
|
Provides-Extra: brotli
|
|
32
32
|
Requires-Dist: brotli; (platform_python_implementation == 'CPython') and extra == 'brotli'
|
|
@@ -40,7 +40,7 @@ Requires-Dist: h2<5,>=3; extra == 'http2'
|
|
|
40
40
|
Provides-Extra: socks
|
|
41
41
|
Requires-Dist: socksio==1.*; extra == 'socks'
|
|
42
42
|
Provides-Extra: zstd
|
|
43
|
-
Requires-Dist: zstandard>=0.18.0; extra == 'zstd'
|
|
43
|
+
Requires-Dist: zstandard>=0.18.0; (python_version <= '3.13') and extra == 'zstd'
|
|
44
44
|
Description-Content-Type: text/markdown
|
|
45
45
|
|
|
46
46
|
<h1 align="center">HTTPX2</h1>
|
|
@@ -88,17 +88,11 @@ Or, using the command-line client.
|
|
|
88
88
|
pip install 'httpx2[cli]' # The command line client is an optional dependency.
|
|
89
89
|
```
|
|
90
90
|
|
|
91
|
-
Which now allows us to use HTTPX2 directly from the command-line
|
|
91
|
+
Which now allows us to use HTTPX2 directly from the command-line:
|
|
92
92
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
Sending a request...
|
|
98
|
-
|
|
99
|
-
<p align="center">
|
|
100
|
-
<img width="700" src="https://raw.githubusercontent.com/pydantic/httpx2/main/docs/img/httpx-request.png" alt='httpx2 http://httpbin.org/json'>
|
|
101
|
-
</p>
|
|
93
|
+
```shell
|
|
94
|
+
httpx2 --help
|
|
95
|
+
```
|
|
102
96
|
|
|
103
97
|
## Features
|
|
104
98
|
|
|
@@ -145,7 +139,7 @@ Or, to include the optional HTTP/2 support, use:
|
|
|
145
139
|
pip install httpx2[http2]
|
|
146
140
|
```
|
|
147
141
|
|
|
148
|
-
HTTPX2 requires Python 3.
|
|
142
|
+
HTTPX2 requires Python 3.10+.
|
|
149
143
|
|
|
150
144
|
## Documentation
|
|
151
145
|
|
|
@@ -180,7 +174,7 @@ As well as these optional installs:
|
|
|
180
174
|
* `rich` - Rich terminal support. *(Optional, with `httpx2[cli]`)*
|
|
181
175
|
* `click` - Command line client support. *(Optional, with `httpx2[cli]`)*
|
|
182
176
|
* `brotli` or `brotlicffi` - Decoding for "brotli" compressed responses. *(Optional, with `httpx2[brotli]`)*
|
|
183
|
-
* `zstandard` - Decoding for "zstd" compressed responses. *(Optional, with `httpx2[zstd]`)*
|
|
177
|
+
* `zstandard` - Decoding for "zstd" compressed responses on Python 3.13 and below. *(Optional, with `httpx2[zstd]`. On Python 3.14+, `zstd` is supported natively via the stdlib [`compression.zstd`](https://docs.python.org/3/library/compression.zstd.html) module.)*
|
|
184
178
|
|
|
185
179
|
A huge amount of credit is due to `requests` for the API layout that
|
|
186
180
|
much of this work follows, as well as to `urllib3` for plenty of design
|
|
@@ -192,24 +186,17 @@ inspiration around the lower-level networking details.
|
|
|
192
186
|
|
|
193
187
|
## Release Information
|
|
194
188
|
|
|
195
|
-
###
|
|
189
|
+
### Removed
|
|
196
190
|
|
|
197
|
-
*
|
|
198
|
-
* **Renamed transitive dependency**: `httpcore` -> `httpcore2`, vendored into the same repository as a uv workspace member. Code that imports `httpcore` directly should switch to `httpcore2`. `httpx2` always depends on a specific `httpcore2` version, pinned exactly.
|
|
191
|
+
* Drop support for Python 3.9. ([#208](https://github.com/pydantic/httpx2/pull/208))
|
|
199
192
|
|
|
200
193
|
### Added
|
|
201
194
|
|
|
202
|
-
*
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
* Eliminate `PytestUnraisableExceptionWarning` from async generator finalization during stream teardown under trio. ([#137](https://github.com/pydantic/httpx2/pull/137), backporting [encode/httpcore#1019](https://github.com/encode/httpcore/pull/1019) by @agronholm.)
|
|
207
|
-
|
|
208
|
-
---
|
|
209
|
-
|
|
210
|
-
Historical entries below are from upstream `encode/httpx`.
|
|
195
|
+
* Add support for Python 3.14. ([#208](https://github.com/pydantic/httpx2/pull/208))
|
|
196
|
+
* Use stdlib `compression.zstd` for Zstd decompression on Python 3.14+; fall back to the `zstandard` package on older versions. ([#932](https://github.com/pydantic/httpx2/pull/932))
|
|
197
|
+
* Bundle `LICENSE.md` in the sdist. ([#938](https://github.com/pydantic/httpx2/pull/938))
|
|
211
198
|
|
|
212
199
|
|
|
213
200
|
---
|
|
214
201
|
|
|
215
|
-
[Full changelog](https://github.com/pydantic/httpx2/blob/main/CHANGELOG.md)
|
|
202
|
+
[Full changelog](https://github.com/pydantic/httpx2/blob/main/src/httpx2/CHANGELOG.md)
|
|
@@ -43,17 +43,11 @@ Or, using the command-line client.
|
|
|
43
43
|
pip install 'httpx2[cli]' # The command line client is an optional dependency.
|
|
44
44
|
```
|
|
45
45
|
|
|
46
|
-
Which now allows us to use HTTPX2 directly from the command-line
|
|
46
|
+
Which now allows us to use HTTPX2 directly from the command-line:
|
|
47
47
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
Sending a request...
|
|
53
|
-
|
|
54
|
-
<p align="center">
|
|
55
|
-
<img width="700" src="docs/img/httpx-request.png" alt='httpx2 http://httpbin.org/json'>
|
|
56
|
-
</p>
|
|
48
|
+
```shell
|
|
49
|
+
httpx2 --help
|
|
50
|
+
```
|
|
57
51
|
|
|
58
52
|
## Features
|
|
59
53
|
|
|
@@ -100,7 +94,7 @@ Or, to include the optional HTTP/2 support, use:
|
|
|
100
94
|
pip install httpx2[http2]
|
|
101
95
|
```
|
|
102
96
|
|
|
103
|
-
HTTPX2 requires Python 3.
|
|
97
|
+
HTTPX2 requires Python 3.10+.
|
|
104
98
|
|
|
105
99
|
## Documentation
|
|
106
100
|
|
|
@@ -135,7 +129,7 @@ As well as these optional installs:
|
|
|
135
129
|
* `rich` - Rich terminal support. *(Optional, with `httpx2[cli]`)*
|
|
136
130
|
* `click` - Command line client support. *(Optional, with `httpx2[cli]`)*
|
|
137
131
|
* `brotli` or `brotlicffi` - Decoding for "brotli" compressed responses. *(Optional, with `httpx2[brotli]`)*
|
|
138
|
-
* `zstandard` - Decoding for "zstd" compressed responses. *(Optional, with `httpx2[zstd]`)*
|
|
132
|
+
* `zstandard` - Decoding for "zstd" compressed responses on Python 3.13 and below. *(Optional, with `httpx2[zstd]`. On Python 3.14+, `zstd` is supported natively via the stdlib [`compression.zstd`](https://docs.python.org/3/library/compression.zstd.html) module.)*
|
|
139
133
|
|
|
140
134
|
A huge amount of credit is due to `requests` for the API layout that
|
|
141
135
|
much of this work follows, as well as to `urllib3` for plenty of design
|
|
@@ -59,9 +59,7 @@ class Auth:
|
|
|
59
59
|
"""
|
|
60
60
|
yield request
|
|
61
61
|
|
|
62
|
-
def sync_auth_flow(
|
|
63
|
-
self, request: Request
|
|
64
|
-
) -> typing.Generator[Request, Response, None]:
|
|
62
|
+
def sync_auth_flow(self, request: Request) -> typing.Generator[Request, Response, None]:
|
|
65
63
|
"""
|
|
66
64
|
Execute the authentication flow synchronously.
|
|
67
65
|
|
|
@@ -84,9 +82,7 @@ class Auth:
|
|
|
84
82
|
except StopIteration:
|
|
85
83
|
break
|
|
86
84
|
|
|
87
|
-
async def async_auth_flow(
|
|
88
|
-
self, request: Request
|
|
89
|
-
) -> typing.AsyncGenerator[Request, Response]:
|
|
85
|
+
async def async_auth_flow(self, request: Request) -> typing.AsyncGenerator[Request, Response]:
|
|
90
86
|
"""
|
|
91
87
|
Execute the authentication flow asynchronously.
|
|
92
88
|
|
|
@@ -161,9 +157,7 @@ class NetRCAuth(Auth):
|
|
|
161
157
|
yield request
|
|
162
158
|
else:
|
|
163
159
|
# Build a basic auth header with credentials from the netrc file.
|
|
164
|
-
request.headers["Authorization"] = self._build_auth_header(
|
|
165
|
-
username=auth_info[0], password=auth_info[2]
|
|
166
|
-
)
|
|
160
|
+
request.headers["Authorization"] = self._build_auth_header(username=auth_info[0], password=auth_info[2])
|
|
167
161
|
yield request
|
|
168
162
|
|
|
169
163
|
def _build_auth_header(self, username: str | bytes, password: str | bytes) -> str:
|
|
@@ -192,9 +186,7 @@ class DigestAuth(Auth):
|
|
|
192
186
|
|
|
193
187
|
def auth_flow(self, request: Request) -> typing.Generator[Request, Response, None]:
|
|
194
188
|
if self._last_challenge:
|
|
195
|
-
request.headers["Authorization"] = self._build_auth_header(
|
|
196
|
-
request, self._last_challenge
|
|
197
|
-
)
|
|
189
|
+
request.headers["Authorization"] = self._build_auth_header(request, self._last_challenge)
|
|
198
190
|
|
|
199
191
|
response = yield request
|
|
200
192
|
|
|
@@ -214,16 +206,12 @@ class DigestAuth(Auth):
|
|
|
214
206
|
self._last_challenge = self._parse_challenge(request, response, auth_header)
|
|
215
207
|
self._nonce_count = 1
|
|
216
208
|
|
|
217
|
-
request.headers["Authorization"] = self._build_auth_header(
|
|
218
|
-
request, self._last_challenge
|
|
219
|
-
)
|
|
209
|
+
request.headers["Authorization"] = self._build_auth_header(request, self._last_challenge)
|
|
220
210
|
if response.cookies:
|
|
221
211
|
Cookies(response.cookies).set_cookie_header(request=request)
|
|
222
212
|
yield request
|
|
223
213
|
|
|
224
|
-
def _parse_challenge(
|
|
225
|
-
self, request: Request, response: Response, auth_header: str
|
|
226
|
-
) -> _DigestAuthChallenge:
|
|
214
|
+
def _parse_challenge(self, request: Request, response: Response, auth_header: str) -> _DigestAuthChallenge:
|
|
227
215
|
"""
|
|
228
216
|
Returns a challenge from a Digest WWW-Authenticate header.
|
|
229
217
|
These take the form of:
|
|
@@ -245,16 +233,12 @@ class DigestAuth(Auth):
|
|
|
245
233
|
algorithm = header_dict.get("algorithm", "MD5")
|
|
246
234
|
opaque = header_dict["opaque"].encode() if "opaque" in header_dict else None
|
|
247
235
|
qop = header_dict["qop"].encode() if "qop" in header_dict else None
|
|
248
|
-
return _DigestAuthChallenge(
|
|
249
|
-
realm=realm, nonce=nonce, algorithm=algorithm, opaque=opaque, qop=qop
|
|
250
|
-
)
|
|
236
|
+
return _DigestAuthChallenge(realm=realm, nonce=nonce, algorithm=algorithm, opaque=opaque, qop=qop)
|
|
251
237
|
except KeyError as exc:
|
|
252
238
|
message = "Malformed Digest WWW-Authenticate header"
|
|
253
239
|
raise ProtocolError(message, request=request) from exc
|
|
254
240
|
|
|
255
|
-
def _build_auth_header(
|
|
256
|
-
self, request: Request, challenge: _DigestAuthChallenge
|
|
257
|
-
) -> str:
|
|
241
|
+
def _build_auth_header(self, request: Request, challenge: _DigestAuthChallenge) -> str:
|
|
258
242
|
hash_func = self._ALGORITHM_TO_HASH_FUNCTION[challenge.algorithm.upper()]
|
|
259
243
|
|
|
260
244
|
def digest(data: bytes) -> bytes:
|
|
@@ -317,11 +301,7 @@ class DigestAuth(Auth):
|
|
|
317
301
|
for i, (field, value) in enumerate(header_fields.items()):
|
|
318
302
|
if i > 0:
|
|
319
303
|
header_value += ", "
|
|
320
|
-
template =
|
|
321
|
-
QUOTED_TEMPLATE
|
|
322
|
-
if field not in NON_QUOTED_FIELDS
|
|
323
|
-
else NON_QUOTED_TEMPLATE
|
|
324
|
-
)
|
|
304
|
+
template = QUOTED_TEMPLATE if field not in NON_QUOTED_FIELDS else NON_QUOTED_TEMPLATE
|
|
325
305
|
header_value += template.format(field, to_str(value))
|
|
326
306
|
|
|
327
307
|
return header_value
|
|
@@ -84,11 +84,7 @@ def _same_origin(url: URL, other: URL) -> bool:
|
|
|
84
84
|
"""
|
|
85
85
|
Return 'True' if the given URLs share the same origin.
|
|
86
86
|
"""
|
|
87
|
-
return (
|
|
88
|
-
url.scheme == other.scheme
|
|
89
|
-
and url.host == other.host
|
|
90
|
-
and _port_or_default(url) == _port_or_default(other)
|
|
91
|
-
)
|
|
87
|
+
return url.scheme == other.scheme and url.host == other.host and _port_or_default(url) == _port_or_default(other)
|
|
92
88
|
|
|
93
89
|
|
|
94
90
|
class UseClientDefault:
|
|
@@ -117,9 +113,7 @@ USE_CLIENT_DEFAULT = UseClientDefault()
|
|
|
117
113
|
logger = logging.getLogger("httpx2")
|
|
118
114
|
|
|
119
115
|
USER_AGENT = f"python-httpx2/{__version__}"
|
|
120
|
-
ACCEPT_ENCODING = ", ".join(
|
|
121
|
-
[key for key in SUPPORTED_DECODERS.keys() if key != "identity"]
|
|
122
|
-
)
|
|
116
|
+
ACCEPT_ENCODING = ", ".join([key for key in SUPPORTED_DECODERS.keys() if key != "identity"])
|
|
123
117
|
|
|
124
118
|
|
|
125
119
|
class ClientState(enum.Enum):
|
|
@@ -142,9 +136,7 @@ class BoundSyncStream(SyncByteStream):
|
|
|
142
136
|
ensures the `response.elapsed` is set once the response is closed.
|
|
143
137
|
"""
|
|
144
138
|
|
|
145
|
-
def __init__(
|
|
146
|
-
self, stream: SyncByteStream, response: Response, start: float
|
|
147
|
-
) -> None:
|
|
139
|
+
def __init__(self, stream: SyncByteStream, response: Response, start: float) -> None:
|
|
148
140
|
self._stream = stream
|
|
149
141
|
self._response = response
|
|
150
142
|
self._start = start
|
|
@@ -165,9 +157,7 @@ class BoundAsyncStream(AsyncByteStream):
|
|
|
165
157
|
ensures the `response.elapsed` is set once the response is closed.
|
|
166
158
|
"""
|
|
167
159
|
|
|
168
|
-
def __init__(
|
|
169
|
-
self, stream: AsyncByteStream, response: Response, start: float
|
|
170
|
-
) -> None:
|
|
160
|
+
def __init__(self, stream: AsyncByteStream, response: Response, start: float) -> None:
|
|
171
161
|
self._stream = stream
|
|
172
162
|
self._response = response
|
|
173
163
|
self._start = start
|
|
@@ -236,15 +226,10 @@ class BaseClient:
|
|
|
236
226
|
return url
|
|
237
227
|
return url.copy_with(raw_path=url.raw_path + b"/")
|
|
238
228
|
|
|
239
|
-
def _get_proxy_map(
|
|
240
|
-
self, proxy: ProxyTypes | None, allow_env_proxies: bool
|
|
241
|
-
) -> dict[str, Proxy | None]:
|
|
229
|
+
def _get_proxy_map(self, proxy: ProxyTypes | None, allow_env_proxies: bool) -> dict[str, Proxy | None]:
|
|
242
230
|
if proxy is None:
|
|
243
231
|
if allow_env_proxies:
|
|
244
|
-
return {
|
|
245
|
-
key: None if url is None else Proxy(url=url)
|
|
246
|
-
for key, url in get_environment_proxies().items()
|
|
247
|
-
}
|
|
232
|
+
return {key: None if url is None else Proxy(url=url) for key, url in get_environment_proxies().items()}
|
|
248
233
|
return {}
|
|
249
234
|
else:
|
|
250
235
|
proxy = Proxy(url=proxy) if isinstance(proxy, (str, URL)) else proxy
|
|
@@ -369,11 +354,7 @@ class BaseClient:
|
|
|
369
354
|
params = self._merge_queryparams(params)
|
|
370
355
|
extensions = {} if extensions is None else extensions
|
|
371
356
|
if "timeout" not in extensions:
|
|
372
|
-
timeout = (
|
|
373
|
-
self.timeout
|
|
374
|
-
if isinstance(timeout, UseClientDefault)
|
|
375
|
-
else Timeout(timeout)
|
|
376
|
-
)
|
|
357
|
+
timeout = self.timeout if isinstance(timeout, UseClientDefault) else Timeout(timeout)
|
|
377
358
|
extensions = dict(**extensions, timeout=timeout.as_dict())
|
|
378
359
|
return Request(
|
|
379
360
|
method,
|
|
@@ -430,9 +411,7 @@ class BaseClient:
|
|
|
430
411
|
merged_headers.update(headers)
|
|
431
412
|
return merged_headers
|
|
432
413
|
|
|
433
|
-
def _merge_queryparams(
|
|
434
|
-
self, params: QueryParamTypes | None = None
|
|
435
|
-
) -> QueryParamTypes | None:
|
|
414
|
+
def _merge_queryparams(self, params: QueryParamTypes | None = None) -> QueryParamTypes | None:
|
|
436
415
|
"""
|
|
437
416
|
Merge a queryparams argument together with any queryparams on the client,
|
|
438
417
|
to create the queryparams used for the outgoing request.
|
|
@@ -459,9 +438,7 @@ class BaseClient:
|
|
|
459
438
|
request: Request,
|
|
460
439
|
auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT,
|
|
461
440
|
) -> Auth:
|
|
462
|
-
auth = (
|
|
463
|
-
self._auth if isinstance(auth, UseClientDefault) else self._build_auth(auth)
|
|
464
|
-
)
|
|
441
|
+
auth = self._auth if isinstance(auth, UseClientDefault) else self._build_auth(auth)
|
|
465
442
|
|
|
466
443
|
if auth is not None:
|
|
467
444
|
return auth
|
|
@@ -523,9 +500,7 @@ class BaseClient:
|
|
|
523
500
|
try:
|
|
524
501
|
url = URL(location)
|
|
525
502
|
except InvalidURL as exc:
|
|
526
|
-
raise RemoteProtocolError(
|
|
527
|
-
f"Invalid URL in location header: {exc}.", request=request
|
|
528
|
-
) from None
|
|
503
|
+
raise RemoteProtocolError(f"Invalid URL in location header: {exc}.", request=request) from None
|
|
529
504
|
|
|
530
505
|
# Handle malformed 'Location' headers that are "absolute" form, have no host.
|
|
531
506
|
# See: https://github.com/encode/httpx/issues/771
|
|
@@ -570,9 +545,7 @@ class BaseClient:
|
|
|
570
545
|
|
|
571
546
|
return headers
|
|
572
547
|
|
|
573
|
-
def _redirect_stream(
|
|
574
|
-
self, request: Request, method: str
|
|
575
|
-
) -> SyncByteStream | AsyncByteStream | None:
|
|
548
|
+
def _redirect_stream(self, request: Request, method: str) -> SyncByteStream | AsyncByteStream | None:
|
|
576
549
|
"""
|
|
577
550
|
Return the body that should be used for the redirect request.
|
|
578
551
|
"""
|
|
@@ -583,11 +556,7 @@ class BaseClient:
|
|
|
583
556
|
|
|
584
557
|
def _set_timeout(self, request: Request) -> None:
|
|
585
558
|
if "timeout" not in request.extensions:
|
|
586
|
-
timeout = (
|
|
587
|
-
self.timeout
|
|
588
|
-
if isinstance(self.timeout, UseClientDefault)
|
|
589
|
-
else Timeout(self.timeout)
|
|
590
|
-
)
|
|
559
|
+
timeout = self.timeout if isinstance(self.timeout, UseClientDefault) else Timeout(self.timeout)
|
|
591
560
|
request.extensions = dict(**request.extensions, timeout=timeout.as_dict())
|
|
592
561
|
|
|
593
562
|
|
|
@@ -620,6 +589,8 @@ class Client(BaseClient):
|
|
|
620
589
|
* **http2** - *(optional)* A boolean indicating if HTTP/2 support should be
|
|
621
590
|
enabled. Defaults to `False`.
|
|
622
591
|
* **proxy** - *(optional)* A proxy URL where all the traffic should be routed.
|
|
592
|
+
* **mounts** - *(optional)* A dictionary mapping URL patterns to transports,
|
|
593
|
+
used to route requests through specific transports based on the URL.
|
|
623
594
|
* **timeout** - *(optional)* The timeout configuration to use when sending
|
|
624
595
|
requests.
|
|
625
596
|
* **limits** - *(optional)* The limits configuration to use.
|
|
@@ -709,9 +680,7 @@ class Client(BaseClient):
|
|
|
709
680
|
for key, proxy in proxy_map.items()
|
|
710
681
|
}
|
|
711
682
|
if mounts is not None:
|
|
712
|
-
self._mounts.update(
|
|
713
|
-
{URLPattern(key): transport for key, transport in mounts.items()}
|
|
714
|
-
)
|
|
683
|
+
self._mounts.update({URLPattern(key): transport for key, transport in mounts.items()})
|
|
715
684
|
|
|
716
685
|
self._mounts = dict(sorted(self._mounts.items()))
|
|
717
686
|
|
|
@@ -901,11 +870,7 @@ class Client(BaseClient):
|
|
|
901
870
|
raise RuntimeError("Cannot send a request, as the client has been closed.")
|
|
902
871
|
|
|
903
872
|
self._state = ClientState.OPENED
|
|
904
|
-
follow_redirects = (
|
|
905
|
-
self.follow_redirects
|
|
906
|
-
if isinstance(follow_redirects, UseClientDefault)
|
|
907
|
-
else follow_redirects
|
|
908
|
-
)
|
|
873
|
+
follow_redirects = self.follow_redirects if isinstance(follow_redirects, UseClientDefault) else follow_redirects
|
|
909
874
|
|
|
910
875
|
self._set_timeout(request)
|
|
911
876
|
|
|
@@ -969,9 +934,7 @@ class Client(BaseClient):
|
|
|
969
934
|
) -> Response:
|
|
970
935
|
while True:
|
|
971
936
|
if len(history) > self.max_redirects:
|
|
972
|
-
raise TooManyRedirects(
|
|
973
|
-
"Exceeded maximum allowed redirects.", request=request
|
|
974
|
-
)
|
|
937
|
+
raise TooManyRedirects("Exceeded maximum allowed redirects.", request=request)
|
|
975
938
|
|
|
976
939
|
for hook in self._event_hooks["request"]:
|
|
977
940
|
hook(request)
|
|
@@ -1006,9 +969,7 @@ class Client(BaseClient):
|
|
|
1006
969
|
start = time.perf_counter()
|
|
1007
970
|
|
|
1008
971
|
if not isinstance(request.stream, SyncByteStream):
|
|
1009
|
-
raise RuntimeError(
|
|
1010
|
-
"Attempted to send an async request with a sync Client instance."
|
|
1011
|
-
)
|
|
972
|
+
raise RuntimeError("Attempted to send an async request with a sync Client instance.")
|
|
1012
973
|
|
|
1013
974
|
with request_context(request=request):
|
|
1014
975
|
response = transport.handle_request(request)
|
|
@@ -1016,9 +977,7 @@ class Client(BaseClient):
|
|
|
1016
977
|
assert isinstance(response.stream, SyncByteStream)
|
|
1017
978
|
|
|
1018
979
|
response.request = request
|
|
1019
|
-
response.stream = BoundSyncStream(
|
|
1020
|
-
response.stream, response=response, start=start
|
|
1021
|
-
)
|
|
980
|
+
response.stream = BoundSyncStream(response.stream, response=response, start=start)
|
|
1022
981
|
self.cookies.extract_cookies(response)
|
|
1023
982
|
response.default_encoding = self._default_encoding
|
|
1024
983
|
|
|
@@ -1276,9 +1235,7 @@ class Client(BaseClient):
|
|
|
1276
1235
|
if self._state != ClientState.UNOPENED:
|
|
1277
1236
|
msg = {
|
|
1278
1237
|
ClientState.OPENED: "Cannot open a client instance more than once.",
|
|
1279
|
-
ClientState.CLOSED: (
|
|
1280
|
-
"Cannot reopen a client instance, once it has been closed."
|
|
1281
|
-
),
|
|
1238
|
+
ClientState.CLOSED: ("Cannot reopen a client instance, once it has been closed."),
|
|
1282
1239
|
}[self._state]
|
|
1283
1240
|
raise RuntimeError(msg)
|
|
1284
1241
|
|
|
@@ -1334,6 +1291,8 @@ class AsyncClient(BaseClient):
|
|
|
1334
1291
|
* **http2** - *(optional)* A boolean indicating if HTTP/2 support should be
|
|
1335
1292
|
enabled. Defaults to `False`.
|
|
1336
1293
|
* **proxy** - *(optional)* A proxy URL where all the traffic should be routed.
|
|
1294
|
+
* **mounts** - *(optional)* A dictionary mapping URL patterns to transports,
|
|
1295
|
+
used to route requests through specific transports based on the URL.
|
|
1337
1296
|
* **timeout** - *(optional)* The timeout configuration to use when sending
|
|
1338
1297
|
requests.
|
|
1339
1298
|
* **limits** - *(optional)* The limits configuration to use.
|
|
@@ -1424,9 +1383,7 @@ class AsyncClient(BaseClient):
|
|
|
1424
1383
|
for key, proxy in proxy_map.items()
|
|
1425
1384
|
}
|
|
1426
1385
|
if mounts is not None:
|
|
1427
|
-
self._mounts.update(
|
|
1428
|
-
{URLPattern(key): transport for key, transport in mounts.items()}
|
|
1429
|
-
)
|
|
1386
|
+
self._mounts.update({URLPattern(key): transport for key, transport in mounts.items()})
|
|
1430
1387
|
self._mounts = dict(sorted(self._mounts.items()))
|
|
1431
1388
|
|
|
1432
1389
|
def _init_transport(
|
|
@@ -1616,11 +1573,7 @@ class AsyncClient(BaseClient):
|
|
|
1616
1573
|
raise RuntimeError("Cannot send a request, as the client has been closed.")
|
|
1617
1574
|
|
|
1618
1575
|
self._state = ClientState.OPENED
|
|
1619
|
-
follow_redirects = (
|
|
1620
|
-
self.follow_redirects
|
|
1621
|
-
if isinstance(follow_redirects, UseClientDefault)
|
|
1622
|
-
else follow_redirects
|
|
1623
|
-
)
|
|
1576
|
+
follow_redirects = self.follow_redirects if isinstance(follow_redirects, UseClientDefault) else follow_redirects
|
|
1624
1577
|
|
|
1625
1578
|
self._set_timeout(request)
|
|
1626
1579
|
|
|
@@ -1684,9 +1637,7 @@ class AsyncClient(BaseClient):
|
|
|
1684
1637
|
) -> Response:
|
|
1685
1638
|
while True:
|
|
1686
1639
|
if len(history) > self.max_redirects:
|
|
1687
|
-
raise TooManyRedirects(
|
|
1688
|
-
"Exceeded maximum allowed redirects.", request=request
|
|
1689
|
-
)
|
|
1640
|
+
raise TooManyRedirects("Exceeded maximum allowed redirects.", request=request)
|
|
1690
1641
|
|
|
1691
1642
|
for hook in self._event_hooks["request"]:
|
|
1692
1643
|
await hook(request)
|
|
@@ -1722,18 +1673,14 @@ class AsyncClient(BaseClient):
|
|
|
1722
1673
|
start = time.perf_counter()
|
|
1723
1674
|
|
|
1724
1675
|
if not isinstance(request.stream, AsyncByteStream):
|
|
1725
|
-
raise RuntimeError(
|
|
1726
|
-
"Attempted to send a sync request with an AsyncClient instance."
|
|
1727
|
-
)
|
|
1676
|
+
raise RuntimeError("Attempted to send a sync request with an AsyncClient instance.")
|
|
1728
1677
|
|
|
1729
1678
|
with request_context(request=request):
|
|
1730
1679
|
response = await transport.handle_async_request(request)
|
|
1731
1680
|
|
|
1732
1681
|
assert isinstance(response.stream, AsyncByteStream)
|
|
1733
1682
|
response.request = request
|
|
1734
|
-
response.stream = BoundAsyncStream(
|
|
1735
|
-
response.stream, response=response, start=start
|
|
1736
|
-
)
|
|
1683
|
+
response.stream = BoundAsyncStream(response.stream, response=response, start=start)
|
|
1737
1684
|
self.cookies.extract_cookies(response)
|
|
1738
1685
|
response.default_encoding = self._default_encoding
|
|
1739
1686
|
|
|
@@ -1991,9 +1938,7 @@ class AsyncClient(BaseClient):
|
|
|
1991
1938
|
if self._state != ClientState.UNOPENED:
|
|
1992
1939
|
msg = {
|
|
1993
1940
|
ClientState.OPENED: "Cannot open a client instance more than once.",
|
|
1994
|
-
ClientState.CLOSED: (
|
|
1995
|
-
"Cannot reopen a client instance, once it has been closed."
|
|
1996
|
-
),
|
|
1941
|
+
ClientState.CLOSED: ("Cannot reopen a client instance, once it has been closed."),
|
|
1997
1942
|
}[self._state]
|
|
1998
1943
|
raise RuntimeError(msg)
|
|
1999
1944
|
|
|
@@ -120,10 +120,7 @@ class Timeout:
|
|
|
120
120
|
self.pool = pool
|
|
121
121
|
else:
|
|
122
122
|
if isinstance(timeout, UnsetType):
|
|
123
|
-
raise ValueError(
|
|
124
|
-
"httpx2.Timeout must either include a default, or set all "
|
|
125
|
-
"four parameters explicitly."
|
|
126
|
-
)
|
|
123
|
+
raise ValueError("httpx2.Timeout must either include a default, or set all four parameters explicitly.")
|
|
127
124
|
self.connect = timeout if isinstance(connect, UnsetType) else connect
|
|
128
125
|
self.read = timeout if isinstance(read, UnsetType) else read
|
|
129
126
|
self.write = timeout if isinstance(write, UnsetType) else write
|
|
@@ -150,10 +147,7 @@ class Timeout:
|
|
|
150
147
|
class_name = self.__class__.__name__
|
|
151
148
|
if len({self.connect, self.read, self.write, self.pool}) == 1:
|
|
152
149
|
return f"{class_name}(timeout={self.connect})"
|
|
153
|
-
return (
|
|
154
|
-
f"{class_name}(connect={self.connect}, "
|
|
155
|
-
f"read={self.read}, write={self.write}, pool={self.pool})"
|
|
156
|
-
)
|
|
150
|
+
return f"{class_name}(connect={self.connect}, read={self.read}, write={self.write}, pool={self.pool})"
|
|
157
151
|
|
|
158
152
|
|
|
159
153
|
class Limits:
|
|
@@ -226,11 +220,7 @@ class Proxy:
|
|
|
226
220
|
@property
|
|
227
221
|
def raw_auth(self) -> tuple[bytes, bytes] | None:
|
|
228
222
|
# The proxy authentication as raw bytes.
|
|
229
|
-
return (
|
|
230
|
-
None
|
|
231
|
-
if self.auth is None
|
|
232
|
-
else (self.auth[0].encode("utf-8"), self.auth[1].encode("utf-8"))
|
|
233
|
-
)
|
|
223
|
+
return None if self.auth is None else (self.auth[0].encode("utf-8"), self.auth[1].encode("utf-8"))
|
|
234
224
|
|
|
235
225
|
def __repr__(self) -> str:
|
|
236
226
|
# The authentication is represented with the password component masked.
|
|
@@ -174,9 +174,7 @@ def encode_html(html: str) -> tuple[dict[str, str], ByteStream]:
|
|
|
174
174
|
|
|
175
175
|
|
|
176
176
|
def encode_json(json: Any) -> tuple[dict[str, str], ByteStream]:
|
|
177
|
-
body = json_dumps(
|
|
178
|
-
json, ensure_ascii=False, separators=(",", ":"), allow_nan=False
|
|
179
|
-
).encode("utf-8")
|
|
177
|
+
body = json_dumps(json, ensure_ascii=False, separators=(",", ":"), allow_nan=False).encode("utf-8")
|
|
180
178
|
content_length = str(len(body))
|
|
181
179
|
content_type = "application/json"
|
|
182
180
|
headers = {"Content-Length": content_length, "Content-Type": content_type}
|