httpx2 2.2.0__tar.gz → 2.4.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.2.0 → httpx2-2.4.0}/.gitignore +1 -0
- {httpx2-2.2.0 → httpx2-2.4.0}/CHANGELOG.md +31 -2
- {httpx2-2.2.0 → httpx2-2.4.0}/PKG-INFO +20 -14
- {httpx2-2.2.0 → httpx2-2.4.0}/README.md +3 -5
- {httpx2-2.2.0 → httpx2-2.4.0}/httpx2/_api.py +24 -36
- {httpx2-2.2.0 → httpx2-2.4.0}/httpx2/_auth.py +3 -3
- {httpx2-2.2.0 → httpx2-2.4.0}/httpx2/_client.py +21 -21
- {httpx2-2.2.0 → httpx2-2.4.0}/httpx2/_config.py +23 -12
- {httpx2-2.2.0 → httpx2-2.4.0}/httpx2/_content.py +4 -11
- {httpx2-2.2.0 → httpx2-2.4.0}/httpx2/_decoders.py +20 -14
- {httpx2-2.2.0 → httpx2-2.4.0}/httpx2/_exceptions.py +12 -3
- {httpx2-2.2.0 → httpx2-2.4.0}/httpx2/_main.py +6 -7
- {httpx2-2.2.0 → httpx2-2.4.0}/httpx2/_models.py +17 -22
- {httpx2-2.2.0 → httpx2-2.4.0}/httpx2/_multipart.py +4 -5
- httpx2-2.4.0/httpx2/_transports/__init__.py +15 -0
- {httpx2-2.2.0 → httpx2-2.4.0}/httpx2/_transports/asgi.py +11 -16
- {httpx2-2.2.0 → httpx2-2.4.0}/httpx2/_transports/base.py +1 -0
- {httpx2-2.2.0 → httpx2-2.4.0}/httpx2/_transports/default.py +7 -11
- {httpx2-2.2.0 → httpx2-2.4.0}/httpx2/_transports/mock.py +1 -4
- {httpx2-2.2.0 → httpx2-2.4.0}/httpx2/_transports/wsgi.py +7 -10
- httpx2-2.4.0/httpx2/_types.py +86 -0
- {httpx2-2.2.0 → httpx2-2.4.0}/httpx2/_urlparse.py +3 -3
- {httpx2-2.2.0 → httpx2-2.4.0}/httpx2/_urls.py +11 -5
- {httpx2-2.2.0 → httpx2-2.4.0}/httpx2/_utils.py +1 -5
- {httpx2-2.2.0 → httpx2-2.4.0}/pyproject.toml +8 -5
- httpx2-2.2.0/httpx2/_transports/__init__.py +0 -15
- httpx2-2.2.0/httpx2/_types.py +0 -110
- {httpx2-2.2.0 → httpx2-2.4.0}/LICENSE.md +0 -0
- {httpx2-2.2.0 → httpx2-2.4.0}/httpx2/__init__.py +0 -0
- {httpx2-2.2.0 → httpx2-2.4.0}/httpx2/__version__.py +0 -0
- {httpx2-2.2.0 → httpx2-2.4.0}/httpx2/_status_codes.py +0 -0
- {httpx2-2.2.0 → httpx2-2.4.0}/httpx2/py.typed +0 -0
|
@@ -4,6 +4,35 @@ 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.4.0 (June 11th, 2026)
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
* Add `HTTPXDeprecationWarning`, a `UserWarning` subclass shown by default so deprecations are visible without enabling warnings. ([#1029](https://github.com/pydantic/httpx2/pull/1029))
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
* Limit the number of chained `Content-Encoding` decoders to 5. ([#1027](https://github.com/pydantic/httpx2/pull/1027))
|
|
16
|
+
* Allow version 15 of `rich` in the `cli` extra. ([#1015](https://github.com/pydantic/httpx2/pull/1015))
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
* Parse an empty `Digest` auth realm without crashing. ([#1023](https://github.com/pydantic/httpx2/pull/1023))
|
|
21
|
+
* Decode IDNA labels in non-leading host positions. ([#1018](https://github.com/pydantic/httpx2/pull/1018))
|
|
22
|
+
|
|
23
|
+
## 2.3.0 (June 1st, 2026)
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
|
|
27
|
+
* Use `truststore` instead of `certifi` for default SSL verification, loading the operating system's trust store. ([#209](https://github.com/pydantic/httpx2/pull/209))
|
|
28
|
+
|
|
29
|
+
### Fixed
|
|
30
|
+
|
|
31
|
+
* Raise when `verify` is a string path and a client-side `cert` is also provided to `create_ssl_context`. ([#990](https://github.com/pydantic/httpx2/pull/990))
|
|
32
|
+
* Allow a `default_encoding` callable to return `None`. ([#951](https://github.com/pydantic/httpx2/pull/951))
|
|
33
|
+
* Make the `zstd` import optional on Python 3.14. ([#1000](https://github.com/pydantic/httpx2/pull/1000))
|
|
34
|
+
* Store elapsed time on the stream wrapper to avoid reference cycles. ([#948](https://github.com/pydantic/httpx2/pull/948))
|
|
35
|
+
|
|
7
36
|
## 2.2.0 (May 16th, 2026)
|
|
8
37
|
|
|
9
38
|
### Fixed
|
|
@@ -57,7 +86,7 @@ Historical entries below are from upstream `encode/httpx`.
|
|
|
57
86
|
|
|
58
87
|
## 0.28.0 (28th November, 2024)
|
|
59
88
|
|
|
60
|
-
Be aware that the default *JSON request bodies now use a more compact representation*. This is generally considered a
|
|
89
|
+
Be aware that the default *JSON request bodies now use a more compact representation*. This is generally considered a preferred style, tho may require updates to test suites.
|
|
61
90
|
|
|
62
91
|
The 0.28 release includes a limited set of deprecations...
|
|
63
92
|
|
|
@@ -192,7 +221,7 @@ Our revised [SSL documentation](docs/advanced/ssl.md) covers how to implement th
|
|
|
192
221
|
|
|
193
222
|
### Removed
|
|
194
223
|
|
|
195
|
-
* The `rfc3986`
|
|
224
|
+
* The `rfc3986` dependency has been removed. (#2252)
|
|
196
225
|
|
|
197
226
|
## 0.23.3 (4th January, 2023)
|
|
198
227
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: httpx2
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.4.0
|
|
4
4
|
Summary: The next generation HTTP client.
|
|
5
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
|
|
@@ -8,8 +8,9 @@ Project-URL: Source, https://github.com/pydantic/httpx2
|
|
|
8
8
|
Author-email: Tom Christie <tom@tomchristie.com>
|
|
9
9
|
Maintainer-email: "Pydantic Services Inc." <engineering@pydantic.dev>
|
|
10
10
|
License-Expression: BSD-3-Clause
|
|
11
|
-
Classifier: Development Status ::
|
|
11
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
12
12
|
Classifier: Environment :: Web Environment
|
|
13
|
+
Classifier: Framework :: AnyIO
|
|
13
14
|
Classifier: Framework :: AsyncIO
|
|
14
15
|
Classifier: Framework :: Trio
|
|
15
16
|
Classifier: Intended Audience :: Developers
|
|
@@ -25,16 +26,17 @@ Classifier: Programming Language :: Python :: 3.14
|
|
|
25
26
|
Classifier: Topic :: Internet :: WWW/HTTP
|
|
26
27
|
Requires-Python: >=3.10
|
|
27
28
|
Requires-Dist: anyio
|
|
28
|
-
Requires-Dist:
|
|
29
|
-
Requires-Dist:
|
|
30
|
-
Requires-Dist:
|
|
29
|
+
Requires-Dist: httpcore2==2.4.0
|
|
30
|
+
Requires-Dist: idna>=3.18
|
|
31
|
+
Requires-Dist: truststore>=0.10
|
|
32
|
+
Requires-Dist: typing-extensions>=4.5.0; python_version < '3.13'
|
|
31
33
|
Provides-Extra: brotli
|
|
32
34
|
Requires-Dist: brotli; (platform_python_implementation == 'CPython') and extra == 'brotli'
|
|
33
35
|
Requires-Dist: brotlicffi; (platform_python_implementation != 'CPython') and extra == 'brotli'
|
|
34
36
|
Provides-Extra: cli
|
|
35
37
|
Requires-Dist: click==8.*; extra == 'cli'
|
|
36
38
|
Requires-Dist: pygments==2.*; extra == 'cli'
|
|
37
|
-
Requires-Dist: rich<
|
|
39
|
+
Requires-Dist: rich<16,>=10; extra == 'cli'
|
|
38
40
|
Provides-Extra: http2
|
|
39
41
|
Requires-Dist: h2<5,>=3; extra == 'http2'
|
|
40
42
|
Provides-Extra: socks
|
|
@@ -52,7 +54,7 @@ Description-Content-Type: text/markdown
|
|
|
52
54
|
<a href="https://pypi.org/project/httpx2/"><img src="https://badge.fury.io/py/httpx2.svg" alt="Package version"></a>
|
|
53
55
|
</p>
|
|
54
56
|
|
|
55
|
-
HTTPX2 is a fully featured HTTP client library for Python
|
|
57
|
+
HTTPX2 is a fully featured HTTP client library for Python. It includes **an integrated command line client**, has support for both **HTTP/1.1 and HTTP/2**, and provides both **sync and async APIs**.
|
|
56
58
|
|
|
57
59
|
> [!NOTE]
|
|
58
60
|
> HTTPX2 is a continuation of the wonderful work started by [@lovelydinosaur](https://github.com/lovelydinosaur) and the broader HTTPX community. We're enormously grateful for everything that has gone into HTTPX over the years - it has been a foundational piece of the modern Python ecosystem, and this project would not exist without it.
|
|
@@ -139,8 +141,6 @@ Or, to include the optional HTTP/2 support, use:
|
|
|
139
141
|
pip install httpx2[http2]
|
|
140
142
|
```
|
|
141
143
|
|
|
142
|
-
HTTPX2 requires Python 3.10+.
|
|
143
|
-
|
|
144
144
|
## Documentation
|
|
145
145
|
|
|
146
146
|
Project documentation is available at [https://httpx2.pydantic.dev/](https://httpx2.pydantic.dev/).
|
|
@@ -164,7 +164,7 @@ The HTTPX2 project relies on these excellent libraries:
|
|
|
164
164
|
* `httpcore2` - The underlying transport implementation for `httpx2`.
|
|
165
165
|
* `h11` - HTTP/1.1 support.
|
|
166
166
|
* `anyio` - Structured concurrency primitives, used to support both `asyncio` and `trio`.
|
|
167
|
-
* `
|
|
167
|
+
* `truststore` - SSL verification using the operating system's trust store.
|
|
168
168
|
* `idna` - Internationalized domain name support.
|
|
169
169
|
|
|
170
170
|
As well as these optional installs:
|
|
@@ -174,7 +174,7 @@ As well as these optional installs:
|
|
|
174
174
|
* `rich` - Rich terminal support. *(Optional, with `httpx2[cli]`)*
|
|
175
175
|
* `click` - Command line client support. *(Optional, with `httpx2[cli]`)*
|
|
176
176
|
* `brotli` or `brotlicffi` - Decoding for "brotli" compressed responses. *(Optional, with `httpx2[brotli]`)*
|
|
177
|
-
* `zstandard` - Decoding for "zstd" compressed responses on Python 3.13 and below. *(Optional, with `httpx2[zstd]`. On Python 3.14+, `zstd` is supported
|
|
177
|
+
* `zstandard` - Decoding for "zstd" compressed responses on Python 3.13 and below. *(Optional, with `httpx2[zstd]`. On Python 3.14+, `zstd` is supported via the stdlib [`compression.zstd`](https://docs.python.org/3/library/compression.zstd.html) module when it is available; the `httpx2[zstd]` extra installs nothing there, so decoding falls back to `zstandard` only on 3.13 and below.)*
|
|
178
178
|
|
|
179
179
|
A huge amount of credit is due to `requests` for the API layout that
|
|
180
180
|
much of this work follows, as well as to `urllib3` for plenty of design
|
|
@@ -186,13 +186,19 @@ inspiration around the lower-level networking details.
|
|
|
186
186
|
|
|
187
187
|
## Release Information
|
|
188
188
|
|
|
189
|
-
###
|
|
189
|
+
### Added
|
|
190
190
|
|
|
191
|
-
*
|
|
191
|
+
* Add `HTTPXDeprecationWarning`, a `UserWarning` subclass shown by default so deprecations are visible without enabling warnings. ([#1029](https://github.com/pydantic/httpx2/pull/1029))
|
|
192
192
|
|
|
193
193
|
### Changed
|
|
194
194
|
|
|
195
|
-
*
|
|
195
|
+
* Limit the number of chained `Content-Encoding` decoders to 5. ([#1027](https://github.com/pydantic/httpx2/pull/1027))
|
|
196
|
+
* Allow version 15 of `rich` in the `cli` extra. ([#1015](https://github.com/pydantic/httpx2/pull/1015))
|
|
197
|
+
|
|
198
|
+
### Fixed
|
|
199
|
+
|
|
200
|
+
* Parse an empty `Digest` auth realm without crashing. ([#1023](https://github.com/pydantic/httpx2/pull/1023))
|
|
201
|
+
* Decode IDNA labels in non-leading host positions. ([#1018](https://github.com/pydantic/httpx2/pull/1018))
|
|
196
202
|
|
|
197
203
|
|
|
198
204
|
---
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
<a href="https://pypi.org/project/httpx2/"><img src="https://badge.fury.io/py/httpx2.svg" alt="Package version"></a>
|
|
8
8
|
</p>
|
|
9
9
|
|
|
10
|
-
HTTPX2 is a fully featured HTTP client library for Python
|
|
10
|
+
HTTPX2 is a fully featured HTTP client library for Python. It includes **an integrated command line client**, has support for both **HTTP/1.1 and HTTP/2**, and provides both **sync and async APIs**.
|
|
11
11
|
|
|
12
12
|
> [!NOTE]
|
|
13
13
|
> HTTPX2 is a continuation of the wonderful work started by [@lovelydinosaur](https://github.com/lovelydinosaur) and the broader HTTPX community. We're enormously grateful for everything that has gone into HTTPX over the years - it has been a foundational piece of the modern Python ecosystem, and this project would not exist without it.
|
|
@@ -94,8 +94,6 @@ Or, to include the optional HTTP/2 support, use:
|
|
|
94
94
|
pip install httpx2[http2]
|
|
95
95
|
```
|
|
96
96
|
|
|
97
|
-
HTTPX2 requires Python 3.10+.
|
|
98
|
-
|
|
99
97
|
## Documentation
|
|
100
98
|
|
|
101
99
|
Project documentation is available at [https://httpx2.pydantic.dev/](https://httpx2.pydantic.dev/).
|
|
@@ -119,7 +117,7 @@ The HTTPX2 project relies on these excellent libraries:
|
|
|
119
117
|
* `httpcore2` - The underlying transport implementation for `httpx2`.
|
|
120
118
|
* `h11` - HTTP/1.1 support.
|
|
121
119
|
* `anyio` - Structured concurrency primitives, used to support both `asyncio` and `trio`.
|
|
122
|
-
* `
|
|
120
|
+
* `truststore` - SSL verification using the operating system's trust store.
|
|
123
121
|
* `idna` - Internationalized domain name support.
|
|
124
122
|
|
|
125
123
|
As well as these optional installs:
|
|
@@ -129,7 +127,7 @@ As well as these optional installs:
|
|
|
129
127
|
* `rich` - Rich terminal support. *(Optional, with `httpx2[cli]`)*
|
|
130
128
|
* `click` - Command line client support. *(Optional, with `httpx2[cli]`)*
|
|
131
129
|
* `brotli` or `brotlicffi` - Decoding for "brotli" compressed responses. *(Optional, with `httpx2[brotli]`)*
|
|
132
|
-
* `zstandard` - Decoding for "zstd" compressed responses on Python 3.13 and below. *(Optional, with `httpx2[zstd]`. On Python 3.14+, `zstd` is supported
|
|
130
|
+
* `zstandard` - Decoding for "zstd" compressed responses on Python 3.13 and below. *(Optional, with `httpx2[zstd]`. On Python 3.14+, `zstd` is supported via the stdlib [`compression.zstd`](https://docs.python.org/3/library/compression.zstd.html) module when it is available; the `httpx2[zstd]` extra installs nothing there, so decoding falls back to `zstandard` only on 3.13 and below.)*
|
|
133
131
|
|
|
134
132
|
A huge amount of credit is due to `requests` for the API layout that
|
|
135
133
|
much of this work follows, as well as to `urllib3` for plenty of design
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import typing
|
|
4
|
+
from collections.abc import Generator
|
|
4
5
|
from contextlib import contextmanager
|
|
5
6
|
|
|
6
7
|
from ._client import Client
|
|
@@ -54,41 +55,28 @@ def request(
|
|
|
54
55
|
verify: ssl.SSLContext | str | bool = True,
|
|
55
56
|
trust_env: bool = True,
|
|
56
57
|
) -> Response:
|
|
57
|
-
"""
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
* **auth** - *(optional)* An authentication class to use when sending the
|
|
80
|
-
request.
|
|
81
|
-
* **proxy** - *(optional)* A proxy URL where all the traffic should be routed.
|
|
82
|
-
* **timeout** - *(optional)* The timeout configuration to use when sending
|
|
83
|
-
the request.
|
|
84
|
-
* **follow_redirects** - *(optional)* Enables or disables HTTP redirects.
|
|
85
|
-
* **verify** - *(optional)* Either `True` to use an SSL context with the
|
|
86
|
-
default CA bundle, `False` to disable verification, or an instance of
|
|
87
|
-
`ssl.SSLContext` to use a custom context.
|
|
88
|
-
* **trust_env** - *(optional)* Enables or disables usage of environment
|
|
89
|
-
variables for configuration.
|
|
90
|
-
|
|
91
|
-
**Returns:** `Response`
|
|
58
|
+
"""Sends an HTTP request.
|
|
59
|
+
|
|
60
|
+
Parameters:
|
|
61
|
+
method: HTTP method for the new `Request` object: `GET`, `OPTIONS`, `HEAD`, `POST`, `PUT`, `PATCH`, or `DELETE`.
|
|
62
|
+
url: URL for the new `Request` object.
|
|
63
|
+
params: *(optional)* Query parameters to include in the URL, as a string, dictionary, or sequence of two-tuples.
|
|
64
|
+
content: *(optional)* Binary content to include in the body of the request, as bytes or a byte iterator.
|
|
65
|
+
data: *(optional)* Form data to include in the body of the request, as a dictionary.
|
|
66
|
+
files: *(optional)* A dictionary of upload files to include in the body of the request.
|
|
67
|
+
json: *(optional)* A JSON serializable object to include in the body of the request.
|
|
68
|
+
headers: *(optional)* Dictionary of HTTP headers to include in the request.
|
|
69
|
+
cookies: *(optional)* Dictionary of Cookie items to include in the request.
|
|
70
|
+
auth: *(optional)* An authentication class to use when sending the request.
|
|
71
|
+
proxy: *(optional)* A proxy URL where all the traffic should be routed.
|
|
72
|
+
timeout: *(optional)* The timeout configuration to use when sending the request.
|
|
73
|
+
follow_redirects: *(optional)* Enables or disables HTTP redirects.
|
|
74
|
+
verify: *(optional)* Either `True` to use an SSL context with the default CA bundle, `False` to disable
|
|
75
|
+
verification, or an instance of `ssl.SSLContext` to use a custom context.
|
|
76
|
+
trust_env: *(optional)* Enables or disables usage of environment variables for configuration.
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
The `Response` object.
|
|
92
80
|
|
|
93
81
|
Usage:
|
|
94
82
|
|
|
@@ -138,7 +126,7 @@ def stream(
|
|
|
138
126
|
follow_redirects: bool = False,
|
|
139
127
|
verify: ssl.SSLContext | str | bool = True,
|
|
140
128
|
trust_env: bool = True,
|
|
141
|
-
) ->
|
|
129
|
+
) -> Generator[Response]:
|
|
142
130
|
"""
|
|
143
131
|
Alternative to `httpx2.request()` that streams the response body
|
|
144
132
|
instead of loading it into memory at once.
|
|
@@ -10,9 +10,9 @@ from urllib.request import parse_http_list
|
|
|
10
10
|
|
|
11
11
|
from ._exceptions import ProtocolError
|
|
12
12
|
from ._models import Cookies, Request, Response
|
|
13
|
-
from ._utils import to_bytes, to_str
|
|
13
|
+
from ._utils import to_bytes, to_str
|
|
14
14
|
|
|
15
|
-
if typing.TYPE_CHECKING:
|
|
15
|
+
if typing.TYPE_CHECKING:
|
|
16
16
|
from hashlib import _Hash
|
|
17
17
|
|
|
18
18
|
|
|
@@ -225,7 +225,7 @@ class DigestAuth(Auth):
|
|
|
225
225
|
header_dict: dict[str, str] = {}
|
|
226
226
|
for field in parse_http_list(fields):
|
|
227
227
|
key, value = field.strip().split("=", 1)
|
|
228
|
-
header_dict[key] =
|
|
228
|
+
header_dict[key] = value.strip('"')
|
|
229
229
|
|
|
230
230
|
try:
|
|
231
231
|
realm = header_dict["realm"].encode()
|
|
@@ -6,6 +6,7 @@ import logging
|
|
|
6
6
|
import time
|
|
7
7
|
import typing
|
|
8
8
|
import warnings
|
|
9
|
+
from collections.abc import AsyncGenerator, Generator
|
|
9
10
|
from contextlib import asynccontextmanager, contextmanager
|
|
10
11
|
from types import TracebackType
|
|
11
12
|
|
|
@@ -132,43 +133,42 @@ class ClientState(enum.Enum):
|
|
|
132
133
|
|
|
133
134
|
class BoundSyncStream(SyncByteStream):
|
|
134
135
|
"""
|
|
135
|
-
A byte stream that
|
|
136
|
-
|
|
136
|
+
A byte stream that tracks elapsed time for a response. Once closed, the
|
|
137
|
+
elapsed time is available via the `elapsed` attribute, and the response
|
|
138
|
+
can read it back from `response.stream.elapsed`.
|
|
137
139
|
"""
|
|
138
140
|
|
|
139
|
-
def __init__(self, stream: SyncByteStream,
|
|
141
|
+
def __init__(self, stream: SyncByteStream, start: float) -> None:
|
|
140
142
|
self._stream = stream
|
|
141
|
-
self._response = response
|
|
142
143
|
self._start = start
|
|
144
|
+
self.elapsed: datetime.timedelta | None = None
|
|
143
145
|
|
|
144
146
|
def __iter__(self) -> typing.Iterator[bytes]:
|
|
145
|
-
|
|
146
|
-
yield chunk
|
|
147
|
+
yield from self._stream
|
|
147
148
|
|
|
148
149
|
def close(self) -> None:
|
|
149
|
-
elapsed = time.perf_counter() - self._start
|
|
150
|
-
self._response.elapsed = datetime.timedelta(seconds=elapsed)
|
|
150
|
+
self.elapsed = datetime.timedelta(seconds=time.perf_counter() - self._start)
|
|
151
151
|
self._stream.close()
|
|
152
152
|
|
|
153
153
|
|
|
154
154
|
class BoundAsyncStream(AsyncByteStream):
|
|
155
155
|
"""
|
|
156
|
-
An async byte stream that
|
|
157
|
-
|
|
156
|
+
An async byte stream that tracks elapsed time for a response. Once closed,
|
|
157
|
+
the elapsed time is available via the `elapsed` attribute, and the response
|
|
158
|
+
can read it back from `response.stream.elapsed`.
|
|
158
159
|
"""
|
|
159
160
|
|
|
160
|
-
def __init__(self, stream: AsyncByteStream,
|
|
161
|
+
def __init__(self, stream: AsyncByteStream, start: float) -> None:
|
|
161
162
|
self._stream = stream
|
|
162
|
-
self._response = response
|
|
163
163
|
self._start = start
|
|
164
|
+
self.elapsed: datetime.timedelta | None = None
|
|
164
165
|
|
|
165
166
|
async def __aiter__(self) -> typing.AsyncIterator[bytes]:
|
|
166
167
|
async for chunk in self._stream:
|
|
167
168
|
yield chunk
|
|
168
169
|
|
|
169
170
|
async def aclose(self) -> None:
|
|
170
|
-
elapsed = time.perf_counter() - self._start
|
|
171
|
-
self._response.elapsed = datetime.timedelta(seconds=elapsed)
|
|
171
|
+
self.elapsed = datetime.timedelta(seconds=time.perf_counter() - self._start)
|
|
172
172
|
await self._stream.aclose()
|
|
173
173
|
|
|
174
174
|
|
|
@@ -189,7 +189,7 @@ class BaseClient:
|
|
|
189
189
|
event_hooks: None | (typing.Mapping[str, list[EventHook]]) = None,
|
|
190
190
|
base_url: URL | str = "",
|
|
191
191
|
trust_env: bool = True,
|
|
192
|
-
default_encoding: str | typing.Callable[[bytes], str] = "utf-8",
|
|
192
|
+
default_encoding: str | typing.Callable[[bytes], str | None] = "utf-8",
|
|
193
193
|
) -> None:
|
|
194
194
|
event_hooks = {} if event_hooks is None else event_hooks
|
|
195
195
|
|
|
@@ -628,7 +628,7 @@ class Client(BaseClient):
|
|
|
628
628
|
event_hooks: None | (typing.Mapping[str, list[EventHook]]) = None,
|
|
629
629
|
base_url: URL | str = "",
|
|
630
630
|
transport: BaseTransport | None = None,
|
|
631
|
-
default_encoding: str | typing.Callable[[bytes], str] = "utf-8",
|
|
631
|
+
default_encoding: str | typing.Callable[[bytes], str | None] = "utf-8",
|
|
632
632
|
) -> None:
|
|
633
633
|
super().__init__(
|
|
634
634
|
auth=auth,
|
|
@@ -810,7 +810,7 @@ class Client(BaseClient):
|
|
|
810
810
|
follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
|
|
811
811
|
timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
|
|
812
812
|
extensions: RequestExtensions | None = None,
|
|
813
|
-
) ->
|
|
813
|
+
) -> Generator[Response]:
|
|
814
814
|
"""
|
|
815
815
|
Alternative to `httpx2.request()` that streams the response body
|
|
816
816
|
instead of loading it into memory at once.
|
|
@@ -977,7 +977,7 @@ class Client(BaseClient):
|
|
|
977
977
|
assert isinstance(response.stream, SyncByteStream)
|
|
978
978
|
|
|
979
979
|
response.request = request
|
|
980
|
-
response.stream = BoundSyncStream(response.stream,
|
|
980
|
+
response.stream = BoundSyncStream(response.stream, start=start)
|
|
981
981
|
self.cookies.extract_cookies(response)
|
|
982
982
|
response.default_encoding = self._default_encoding
|
|
983
983
|
|
|
@@ -1330,7 +1330,7 @@ class AsyncClient(BaseClient):
|
|
|
1330
1330
|
base_url: URL | str = "",
|
|
1331
1331
|
transport: AsyncBaseTransport | None = None,
|
|
1332
1332
|
trust_env: bool = True,
|
|
1333
|
-
default_encoding: str | typing.Callable[[bytes], str] = "utf-8",
|
|
1333
|
+
default_encoding: str | typing.Callable[[bytes], str | None] = "utf-8",
|
|
1334
1334
|
) -> None:
|
|
1335
1335
|
super().__init__(
|
|
1336
1336
|
auth=auth,
|
|
@@ -1513,7 +1513,7 @@ class AsyncClient(BaseClient):
|
|
|
1513
1513
|
follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
|
|
1514
1514
|
timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
|
|
1515
1515
|
extensions: RequestExtensions | None = None,
|
|
1516
|
-
) ->
|
|
1516
|
+
) -> AsyncGenerator[Response]:
|
|
1517
1517
|
"""
|
|
1518
1518
|
Alternative to `httpx2.request()` that streams the response body
|
|
1519
1519
|
instead of loading it into memory at once.
|
|
@@ -1680,7 +1680,7 @@ class AsyncClient(BaseClient):
|
|
|
1680
1680
|
|
|
1681
1681
|
assert isinstance(response.stream, AsyncByteStream)
|
|
1682
1682
|
response.request = request
|
|
1683
|
-
response.stream = BoundAsyncStream(response.stream,
|
|
1683
|
+
response.stream = BoundAsyncStream(response.stream, start=start)
|
|
1684
1684
|
self.cookies.extract_cookies(response)
|
|
1685
1685
|
response.default_encoding = self._default_encoding
|
|
1686
1686
|
|
|
@@ -28,34 +28,40 @@ def create_ssl_context(
|
|
|
28
28
|
import ssl
|
|
29
29
|
import warnings
|
|
30
30
|
|
|
31
|
-
import
|
|
31
|
+
import truststore
|
|
32
32
|
|
|
33
33
|
if verify is True:
|
|
34
|
-
if trust_env and os.environ.get("SSL_CERT_FILE"): # pragma:
|
|
34
|
+
if trust_env and os.environ.get("SSL_CERT_FILE"): # pragma: no cover
|
|
35
35
|
ctx = ssl.create_default_context(cafile=os.environ["SSL_CERT_FILE"])
|
|
36
|
-
elif trust_env and os.environ.get("SSL_CERT_DIR"): # pragma:
|
|
36
|
+
elif trust_env and os.environ.get("SSL_CERT_DIR"): # pragma: no cover
|
|
37
37
|
ctx = ssl.create_default_context(capath=os.environ["SSL_CERT_DIR"])
|
|
38
38
|
else:
|
|
39
|
-
# Default case
|
|
40
|
-
ctx =
|
|
39
|
+
# Default case: rely on the system trust store via `truststore`.
|
|
40
|
+
ctx = truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
|
|
41
41
|
elif verify is False:
|
|
42
42
|
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
|
|
43
43
|
ctx.check_hostname = False
|
|
44
44
|
ctx.verify_mode = ssl.CERT_NONE
|
|
45
|
-
elif isinstance(verify, str):
|
|
45
|
+
elif isinstance(verify, str):
|
|
46
|
+
if cert:
|
|
47
|
+
raise TypeError(
|
|
48
|
+
"`verify=<str>` cannot be combined with `cert=...`. "
|
|
49
|
+
"Build an `ssl.SSLContext` and pass it as `verify=<ctx>`, "
|
|
50
|
+
"using `.load_cert_chain()` to configure the certificate chain."
|
|
51
|
+
)
|
|
46
52
|
message = (
|
|
47
53
|
"`verify=<str>` is deprecated. "
|
|
48
54
|
"Use `verify=ssl.create_default_context(cafile=...)` "
|
|
49
55
|
"or `verify=ssl.create_default_context(capath=...)` instead."
|
|
50
56
|
)
|
|
51
57
|
warnings.warn(message, DeprecationWarning)
|
|
52
|
-
if os.path.isdir(verify):
|
|
58
|
+
if os.path.isdir(verify): # pragma: no cover
|
|
53
59
|
return ssl.create_default_context(capath=verify)
|
|
54
60
|
return ssl.create_default_context(cafile=verify)
|
|
55
61
|
else:
|
|
56
62
|
ctx = verify
|
|
57
63
|
|
|
58
|
-
if cert: # pragma:
|
|
64
|
+
if cert: # pragma: no cover
|
|
59
65
|
message = (
|
|
60
66
|
"`cert=...` is deprecated. Use `verify=<ssl_context>` instead,"
|
|
61
67
|
"with `.load_cert_chain()` to configure the certificate chain."
|
|
@@ -83,6 +89,11 @@ class Timeout:
|
|
|
83
89
|
# 5s timeout elsewhere.
|
|
84
90
|
"""
|
|
85
91
|
|
|
92
|
+
connect: float | None
|
|
93
|
+
read: float | None
|
|
94
|
+
write: float | None
|
|
95
|
+
pool: float | None
|
|
96
|
+
|
|
86
97
|
def __init__(
|
|
87
98
|
self,
|
|
88
99
|
timeout: TimeoutTypes | UnsetType = UNSET,
|
|
@@ -98,10 +109,10 @@ class Timeout:
|
|
|
98
109
|
assert read is UNSET
|
|
99
110
|
assert write is UNSET
|
|
100
111
|
assert pool is UNSET
|
|
101
|
-
self.connect = timeout.connect
|
|
102
|
-
self.read = timeout.read
|
|
103
|
-
self.write = timeout.write
|
|
104
|
-
self.pool = timeout.pool
|
|
112
|
+
self.connect = timeout.connect
|
|
113
|
+
self.read = timeout.read
|
|
114
|
+
self.write = timeout.write
|
|
115
|
+
self.pool = timeout.pool
|
|
105
116
|
elif isinstance(timeout, tuple):
|
|
106
117
|
# Passed as a tuple.
|
|
107
118
|
self.connect = timeout[0]
|
|
@@ -2,14 +2,10 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import inspect
|
|
4
4
|
import warnings
|
|
5
|
+
from collections.abc import AsyncIterable, AsyncIterator, Iterable, Iterator, Mapping
|
|
5
6
|
from json import dumps as json_dumps
|
|
6
7
|
from typing import (
|
|
7
8
|
Any,
|
|
8
|
-
AsyncIterable,
|
|
9
|
-
AsyncIterator,
|
|
10
|
-
Iterable,
|
|
11
|
-
Iterator,
|
|
12
|
-
Mapping,
|
|
13
9
|
)
|
|
14
10
|
from urllib.parse import urlencode
|
|
15
11
|
|
|
@@ -60,8 +56,7 @@ class IteratorByteStream(SyncByteStream):
|
|
|
60
56
|
chunk = self._stream.read(self.CHUNK_SIZE)
|
|
61
57
|
else:
|
|
62
58
|
# Otherwise iterate.
|
|
63
|
-
|
|
64
|
-
yield part
|
|
59
|
+
yield from self._stream
|
|
65
60
|
|
|
66
61
|
|
|
67
62
|
class AsyncIteratorByteStream(AsyncByteStream):
|
|
@@ -133,10 +128,8 @@ def encode_content(
|
|
|
133
128
|
raise TypeError(f"Unexpected type for 'content', {type(content)!r}")
|
|
134
129
|
|
|
135
130
|
|
|
136
|
-
def encode_urlencoded_data(
|
|
137
|
-
|
|
138
|
-
) -> tuple[dict[str, str], ByteStream]:
|
|
139
|
-
plain_data = []
|
|
131
|
+
def encode_urlencoded_data(data: RequestData) -> tuple[dict[str, str], ByteStream]:
|
|
132
|
+
plain_data: list[tuple[str, str]] = []
|
|
140
133
|
for key, value in data.items():
|
|
141
134
|
if isinstance(value, (list, tuple)):
|
|
142
135
|
plain_data.extend([(key, primitive_value_to_str(item)) for item in value])
|
|
@@ -29,9 +29,9 @@ except ImportError: # pragma: no cover
|
|
|
29
29
|
|
|
30
30
|
|
|
31
31
|
# Zstandard support is optional on Python <= 3.13.
|
|
32
|
-
# On Python 3.14
|
|
33
|
-
# it without an extra dependency.
|
|
32
|
+
# On Python 3.14+, the stdlib includes an optional built-in zstd implementation.
|
|
34
33
|
if typing.TYPE_CHECKING:
|
|
34
|
+
# We keep checking Python version in the type checker path because try..except doesn't help type checkers.
|
|
35
35
|
if sys.version_info >= (3, 14):
|
|
36
36
|
from compression.zstd import ZstdDecompressor, ZstdError
|
|
37
37
|
else:
|
|
@@ -39,20 +39,22 @@ if typing.TYPE_CHECKING:
|
|
|
39
39
|
|
|
40
40
|
ZstdDecompressor = functools.partial(_ZstdDecompressor().decompressobj)
|
|
41
41
|
|
|
42
|
-
_zstandard_installed: bool =
|
|
42
|
+
_zstandard_installed: bool = False
|
|
43
43
|
else: # pragma: no cover
|
|
44
|
-
|
|
44
|
+
_zstandard_installed = False
|
|
45
|
+
try:
|
|
45
46
|
from compression.zstd import ZstdDecompressor, ZstdError
|
|
46
47
|
|
|
47
48
|
_zstandard_installed = True
|
|
48
|
-
|
|
49
|
+
# Either Python <3.14 or the distro doesn't have `compression.zstd`.
|
|
50
|
+
except ImportError:
|
|
49
51
|
try:
|
|
50
52
|
from zstandard import ZstdDecompressor as _ZstdDecompressor, ZstdError
|
|
51
53
|
|
|
52
54
|
ZstdDecompressor = functools.partial(_ZstdDecompressor().decompressobj)
|
|
53
55
|
_zstandard_installed = True
|
|
54
56
|
except ImportError:
|
|
55
|
-
|
|
57
|
+
pass
|
|
56
58
|
|
|
57
59
|
|
|
58
60
|
class ContentDecoder:
|
|
@@ -181,11 +183,10 @@ class BrotliDecoder(ContentDecoder):
|
|
|
181
183
|
|
|
182
184
|
|
|
183
185
|
class ZStandardDecoder(ContentDecoder):
|
|
184
|
-
"""
|
|
185
|
-
Handle 'zstd' RFC 8878 decoding.
|
|
186
|
+
"""Handle 'zstd' RFC 8878 decoding.
|
|
186
187
|
|
|
187
|
-
|
|
188
|
-
|
|
188
|
+
If running on Python 3.14+ or a distro that doesn't have the `compression.zstd` stdlib module, requires either:
|
|
189
|
+
`pip install zstandard` or `pip install httpx2[zstd]`.
|
|
189
190
|
"""
|
|
190
191
|
|
|
191
192
|
# inspired by the ZstdDecoder implementation in urllib3
|
|
@@ -229,13 +230,18 @@ class MultiDecoder(ContentDecoder):
|
|
|
229
230
|
Handle the case where multiple encodings have been applied.
|
|
230
231
|
"""
|
|
231
232
|
|
|
232
|
-
|
|
233
|
+
max_decode_links: typing.ClassVar[int] = 5
|
|
234
|
+
|
|
235
|
+
def __init__(self, encodings: typing.Sequence[str]) -> None:
|
|
233
236
|
"""
|
|
234
|
-
'
|
|
237
|
+
'encodings' should be the content codings in the order in which
|
|
235
238
|
each was applied.
|
|
236
239
|
"""
|
|
240
|
+
codings = [encoding for encoding in encodings if encoding in SUPPORTED_DECODERS]
|
|
241
|
+
if len(codings) > self.max_decode_links:
|
|
242
|
+
raise DecodingError(f"Cannot apply more than {self.max_decode_links} content encodings.")
|
|
237
243
|
# Note that we reverse the order for decoding.
|
|
238
|
-
self.children =
|
|
244
|
+
self.children: list[ContentDecoder] = [SUPPORTED_DECODERS[coding]() for coding in reversed(codings)]
|
|
239
245
|
|
|
240
246
|
def decode(self, data: bytes) -> bytes:
|
|
241
247
|
for child in self.children:
|
|
@@ -396,7 +402,7 @@ class LineDecoder:
|
|
|
396
402
|
return lines
|
|
397
403
|
|
|
398
404
|
|
|
399
|
-
SUPPORTED_DECODERS = {
|
|
405
|
+
SUPPORTED_DECODERS: dict[str, type[ContentDecoder]] = {
|
|
400
406
|
"identity": IdentityDecoder,
|
|
401
407
|
"gzip": GZipDecoder,
|
|
402
408
|
"deflate": DeflateDecoder,
|