httpx2 2.2.0__tar.gz → 2.3.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.3.0}/.gitignore +1 -0
- {httpx2-2.2.0 → httpx2-2.3.0}/CHANGELOG.md +15 -2
- {httpx2-2.2.0 → httpx2-2.3.0}/PKG-INFO +12 -11
- {httpx2-2.2.0 → httpx2-2.3.0}/README.md +2 -4
- {httpx2-2.2.0 → httpx2-2.3.0}/httpx2/_api.py +22 -35
- {httpx2-2.2.0 → httpx2-2.3.0}/httpx2/_auth.py +1 -1
- {httpx2-2.2.0 → httpx2-2.3.0}/httpx2/_client.py +17 -17
- {httpx2-2.2.0 → httpx2-2.3.0}/httpx2/_config.py +14 -8
- {httpx2-2.2.0 → httpx2-2.3.0}/httpx2/_decoders.py +11 -10
- {httpx2-2.2.0 → httpx2-2.3.0}/httpx2/_main.py +1 -2
- {httpx2-2.2.0 → httpx2-2.3.0}/httpx2/_models.py +4 -1
- {httpx2-2.2.0 → httpx2-2.3.0}/httpx2/_multipart.py +1 -1
- httpx2-2.3.0/httpx2/_transports/__init__.py +15 -0
- {httpx2-2.2.0 → httpx2-2.3.0}/httpx2/_transports/asgi.py +10 -15
- {httpx2-2.2.0 → httpx2-2.3.0}/httpx2/_transports/base.py +1 -0
- {httpx2-2.2.0 → httpx2-2.3.0}/httpx2/_transports/mock.py +1 -4
- {httpx2-2.2.0 → httpx2-2.3.0}/httpx2/_transports/wsgi.py +6 -8
- {httpx2-2.2.0 → httpx2-2.3.0}/httpx2/_types.py +1 -1
- {httpx2-2.2.0 → httpx2-2.3.0}/httpx2/_urlparse.py +2 -2
- {httpx2-2.2.0 → httpx2-2.3.0}/httpx2/_urls.py +1 -1
- {httpx2-2.2.0 → httpx2-2.3.0}/httpx2/_utils.py +1 -1
- {httpx2-2.2.0 → httpx2-2.3.0}/pyproject.toml +3 -2
- httpx2-2.2.0/httpx2/_transports/__init__.py +0 -15
- {httpx2-2.2.0 → httpx2-2.3.0}/LICENSE.md +0 -0
- {httpx2-2.2.0 → httpx2-2.3.0}/httpx2/__init__.py +0 -0
- {httpx2-2.2.0 → httpx2-2.3.0}/httpx2/__version__.py +0 -0
- {httpx2-2.2.0 → httpx2-2.3.0}/httpx2/_content.py +0 -0
- {httpx2-2.2.0 → httpx2-2.3.0}/httpx2/_exceptions.py +0 -0
- {httpx2-2.2.0 → httpx2-2.3.0}/httpx2/_status_codes.py +0 -0
- {httpx2-2.2.0 → httpx2-2.3.0}/httpx2/_transports/default.py +0 -0
- {httpx2-2.2.0 → httpx2-2.3.0}/httpx2/py.typed +0 -0
|
@@ -4,6 +4,19 @@ 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.3.0 (June 1st, 2026)
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
* Use `truststore` instead of `certifi` for default SSL verification, loading the operating system's trust store. ([#209](https://github.com/pydantic/httpx2/pull/209))
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
* 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))
|
|
16
|
+
* Allow a `default_encoding` callable to return `None`. ([#951](https://github.com/pydantic/httpx2/pull/951))
|
|
17
|
+
* Make the `zstd` import optional on Python 3.14. ([#1000](https://github.com/pydantic/httpx2/pull/1000))
|
|
18
|
+
* Store elapsed time on the stream wrapper to avoid reference cycles. ([#948](https://github.com/pydantic/httpx2/pull/948))
|
|
19
|
+
|
|
7
20
|
## 2.2.0 (May 16th, 2026)
|
|
8
21
|
|
|
9
22
|
### Fixed
|
|
@@ -57,7 +70,7 @@ Historical entries below are from upstream `encode/httpx`.
|
|
|
57
70
|
|
|
58
71
|
## 0.28.0 (28th November, 2024)
|
|
59
72
|
|
|
60
|
-
Be aware that the default *JSON request bodies now use a more compact representation*. This is generally considered a
|
|
73
|
+
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
74
|
|
|
62
75
|
The 0.28 release includes a limited set of deprecations...
|
|
63
76
|
|
|
@@ -192,7 +205,7 @@ Our revised [SSL documentation](docs/advanced/ssl.md) covers how to implement th
|
|
|
192
205
|
|
|
193
206
|
### Removed
|
|
194
207
|
|
|
195
|
-
* The `rfc3986`
|
|
208
|
+
* The `rfc3986` dependency has been removed. (#2252)
|
|
196
209
|
|
|
197
210
|
## 0.23.3 (4th January, 2023)
|
|
198
211
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: httpx2
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.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
|
|
@@ -25,9 +25,9 @@ Classifier: Programming Language :: Python :: 3.14
|
|
|
25
25
|
Classifier: Topic :: Internet :: WWW/HTTP
|
|
26
26
|
Requires-Python: >=3.10
|
|
27
27
|
Requires-Dist: anyio
|
|
28
|
-
Requires-Dist:
|
|
29
|
-
Requires-Dist: httpcore2==2.2.0
|
|
28
|
+
Requires-Dist: httpcore2==2.3.0
|
|
30
29
|
Requires-Dist: idna
|
|
30
|
+
Requires-Dist: truststore>=0.10
|
|
31
31
|
Provides-Extra: brotli
|
|
32
32
|
Requires-Dist: brotli; (platform_python_implementation == 'CPython') and extra == 'brotli'
|
|
33
33
|
Requires-Dist: brotlicffi; (platform_python_implementation != 'CPython') and extra == 'brotli'
|
|
@@ -139,8 +139,6 @@ Or, to include the optional HTTP/2 support, use:
|
|
|
139
139
|
pip install httpx2[http2]
|
|
140
140
|
```
|
|
141
141
|
|
|
142
|
-
HTTPX2 requires Python 3.10+.
|
|
143
|
-
|
|
144
142
|
## Documentation
|
|
145
143
|
|
|
146
144
|
Project documentation is available at [https://httpx2.pydantic.dev/](https://httpx2.pydantic.dev/).
|
|
@@ -164,7 +162,7 @@ The HTTPX2 project relies on these excellent libraries:
|
|
|
164
162
|
* `httpcore2` - The underlying transport implementation for `httpx2`.
|
|
165
163
|
* `h11` - HTTP/1.1 support.
|
|
166
164
|
* `anyio` - Structured concurrency primitives, used to support both `asyncio` and `trio`.
|
|
167
|
-
* `
|
|
165
|
+
* `truststore` - SSL verification using the operating system's trust store.
|
|
168
166
|
* `idna` - Internationalized domain name support.
|
|
169
167
|
|
|
170
168
|
As well as these optional installs:
|
|
@@ -174,7 +172,7 @@ As well as these optional installs:
|
|
|
174
172
|
* `rich` - Rich terminal support. *(Optional, with `httpx2[cli]`)*
|
|
175
173
|
* `click` - Command line client support. *(Optional, with `httpx2[cli]`)*
|
|
176
174
|
* `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
|
|
175
|
+
* `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
176
|
|
|
179
177
|
A huge amount of credit is due to `requests` for the API layout that
|
|
180
178
|
much of this work follows, as well as to `urllib3` for plenty of design
|
|
@@ -186,13 +184,16 @@ inspiration around the lower-level networking details.
|
|
|
186
184
|
|
|
187
185
|
## Release Information
|
|
188
186
|
|
|
189
|
-
###
|
|
187
|
+
### Changed
|
|
190
188
|
|
|
191
|
-
*
|
|
189
|
+
* Use `truststore` instead of `certifi` for default SSL verification, loading the operating system's trust store. ([#209](https://github.com/pydantic/httpx2/pull/209))
|
|
192
190
|
|
|
193
|
-
###
|
|
191
|
+
### Fixed
|
|
194
192
|
|
|
195
|
-
*
|
|
193
|
+
* 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))
|
|
194
|
+
* Allow a `default_encoding` callable to return `None`. ([#951](https://github.com/pydantic/httpx2/pull/951))
|
|
195
|
+
* Make the `zstd` import optional on Python 3.14. ([#1000](https://github.com/pydantic/httpx2/pull/1000))
|
|
196
|
+
* Store elapsed time on the stream wrapper to avoid reference cycles. ([#948](https://github.com/pydantic/httpx2/pull/948))
|
|
196
197
|
|
|
197
198
|
|
|
198
199
|
---
|
|
@@ -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
|
|
@@ -54,41 +54,28 @@ def request(
|
|
|
54
54
|
verify: ssl.SSLContext | str | bool = True,
|
|
55
55
|
trust_env: bool = True,
|
|
56
56
|
) -> 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`
|
|
57
|
+
"""Sends an HTTP request.
|
|
58
|
+
|
|
59
|
+
Parameters:
|
|
60
|
+
method: HTTP method for the new `Request` object: `GET`, `OPTIONS`, `HEAD`, `POST`, `PUT`, `PATCH`, or `DELETE`.
|
|
61
|
+
url: URL for the new `Request` object.
|
|
62
|
+
params: *(optional)* Query parameters to include in the URL, as a string, dictionary, or sequence of two-tuples.
|
|
63
|
+
content: *(optional)* Binary content to include in the body of the request, as bytes or a byte iterator.
|
|
64
|
+
data: *(optional)* Form data to include in the body of the request, as a dictionary.
|
|
65
|
+
files: *(optional)* A dictionary of upload files to include in the body of the request.
|
|
66
|
+
json: *(optional)* A JSON serializable object to include in the body of the request.
|
|
67
|
+
headers: *(optional)* Dictionary of HTTP headers to include in the request.
|
|
68
|
+
cookies: *(optional)* Dictionary of Cookie items to include in the request.
|
|
69
|
+
auth: *(optional)* An authentication class to use when sending the request.
|
|
70
|
+
proxy: *(optional)* A proxy URL where all the traffic should be routed.
|
|
71
|
+
timeout: *(optional)* The timeout configuration to use when sending the request.
|
|
72
|
+
follow_redirects: *(optional)* Enables or disables HTTP redirects.
|
|
73
|
+
verify: *(optional)* Either `True` to use an SSL context with the default CA bundle, `False` to disable
|
|
74
|
+
verification, or an instance of `ssl.SSLContext` to use a custom context.
|
|
75
|
+
trust_env: *(optional)* Enables or disables usage of environment variables for configuration.
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
The `Response` object.
|
|
92
79
|
|
|
93
80
|
Usage:
|
|
94
81
|
|
|
@@ -132,43 +132,43 @@ class ClientState(enum.Enum):
|
|
|
132
132
|
|
|
133
133
|
class BoundSyncStream(SyncByteStream):
|
|
134
134
|
"""
|
|
135
|
-
A byte stream that
|
|
136
|
-
|
|
135
|
+
A byte stream that tracks elapsed time for a response. Once closed, the
|
|
136
|
+
elapsed time is available via the `elapsed` attribute, and the response
|
|
137
|
+
can read it back from `response.stream.elapsed`.
|
|
137
138
|
"""
|
|
138
139
|
|
|
139
|
-
def __init__(self, stream: SyncByteStream,
|
|
140
|
+
def __init__(self, stream: SyncByteStream, start: float) -> None:
|
|
140
141
|
self._stream = stream
|
|
141
|
-
self._response = response
|
|
142
142
|
self._start = start
|
|
143
|
+
self.elapsed: datetime.timedelta | None = None
|
|
143
144
|
|
|
144
145
|
def __iter__(self) -> typing.Iterator[bytes]:
|
|
145
146
|
for chunk in self._stream:
|
|
146
147
|
yield chunk
|
|
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,
|
|
@@ -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,
|
|
@@ -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."
|
|
@@ -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
|
|
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
|
|
@@ -180,8 +180,7 @@ def format_certificate(cert: _PeerCertRetDictType) -> str: # pragma: no cover
|
|
|
180
180
|
lines.append(f"* {key}:")
|
|
181
181
|
for item in value:
|
|
182
182
|
if key in ("subject", "issuer"):
|
|
183
|
-
for sub_item in item
|
|
184
|
-
lines.append(f"* {sub_item[0]}: {sub_item[1]!r}")
|
|
183
|
+
lines.extend(f"* {sub_item[0]}: {sub_item[1]!r}" for sub_item in item)
|
|
185
184
|
elif isinstance(item, tuple) and len(item) == 2:
|
|
186
185
|
lines.append(f"* {item[0]}: {item[1]!r}")
|
|
187
186
|
else:
|
|
@@ -507,7 +507,7 @@ class Response:
|
|
|
507
507
|
request: Request | None = None,
|
|
508
508
|
extensions: ResponseExtensions | None = None,
|
|
509
509
|
history: list[Response] | None = None,
|
|
510
|
-
default_encoding: str | typing.Callable[[bytes], str] = "utf-8",
|
|
510
|
+
default_encoding: str | typing.Callable[[bytes], str | None] = "utf-8",
|
|
511
511
|
) -> None:
|
|
512
512
|
self.status_code = status_code
|
|
513
513
|
self.headers = Headers(headers)
|
|
@@ -563,6 +563,9 @@ class Response:
|
|
|
563
563
|
cycle to complete.
|
|
564
564
|
"""
|
|
565
565
|
if not hasattr(self, "_elapsed"):
|
|
566
|
+
stream_elapsed: datetime.timedelta | None = getattr(self.stream, "elapsed", None)
|
|
567
|
+
if stream_elapsed is not None:
|
|
568
|
+
return stream_elapsed
|
|
566
569
|
raise RuntimeError("'.elapsed' may only be accessed after the response has been read or closed.")
|
|
567
570
|
return self._elapsed
|
|
568
571
|
|
|
@@ -117,7 +117,7 @@ class FileField:
|
|
|
117
117
|
# This large tuple based API largely mirror's requests' API
|
|
118
118
|
# It would be good to think of better APIs for this that we could
|
|
119
119
|
# include in httpx 2.0 since variable length tuples(especially of 4 elements)
|
|
120
|
-
# are quite
|
|
120
|
+
# are quite unwieldy
|
|
121
121
|
if isinstance(value, tuple):
|
|
122
122
|
if len(value) == 2:
|
|
123
123
|
# neither the 3rd parameter (content_type) nor the 4th (headers)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from .asgi import ASGITransport
|
|
2
|
+
from .base import AsyncBaseTransport, BaseTransport
|
|
3
|
+
from .default import AsyncHTTPTransport, HTTPTransport
|
|
4
|
+
from .mock import MockTransport
|
|
5
|
+
from .wsgi import WSGITransport
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"ASGITransport",
|
|
9
|
+
"AsyncBaseTransport",
|
|
10
|
+
"BaseTransport",
|
|
11
|
+
"AsyncHTTPTransport",
|
|
12
|
+
"HTTPTransport",
|
|
13
|
+
"MockTransport",
|
|
14
|
+
"WSGITransport",
|
|
15
|
+
]
|
|
@@ -6,7 +6,7 @@ from .._models import Request, Response
|
|
|
6
6
|
from .._types import AsyncByteStream
|
|
7
7
|
from .base import AsyncBaseTransport
|
|
8
8
|
|
|
9
|
-
if typing.TYPE_CHECKING:
|
|
9
|
+
if typing.TYPE_CHECKING:
|
|
10
10
|
import asyncio
|
|
11
11
|
|
|
12
12
|
import trio
|
|
@@ -31,7 +31,7 @@ def is_running_trio() -> bool:
|
|
|
31
31
|
|
|
32
32
|
if sniffio.current_async_library() == "trio":
|
|
33
33
|
return True
|
|
34
|
-
except ImportError: # pragma:
|
|
34
|
+
except ImportError: # pragma: no cover
|
|
35
35
|
pass
|
|
36
36
|
|
|
37
37
|
return False
|
|
@@ -70,14 +70,12 @@ class ASGITransport(AsyncBaseTransport):
|
|
|
70
70
|
```
|
|
71
71
|
|
|
72
72
|
Arguments:
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
* `client` - A two-tuple indicating the client IP and port of incoming requests.
|
|
80
|
-
```
|
|
73
|
+
app: The ASGI application.
|
|
74
|
+
raise_app_exceptions: Boolean indicating if exceptions in the application
|
|
75
|
+
should be raised. Default to `True`. Can be set to `False` for use cases
|
|
76
|
+
such as testing the content of a client 500 response.
|
|
77
|
+
root_path: The root path on which the ASGI application should be mounted.
|
|
78
|
+
client: A two-tuple indicating the client IP and port of incoming requests.
|
|
81
79
|
"""
|
|
82
80
|
|
|
83
81
|
def __init__(
|
|
@@ -92,10 +90,7 @@ class ASGITransport(AsyncBaseTransport):
|
|
|
92
90
|
self.root_path = root_path
|
|
93
91
|
self.client = client
|
|
94
92
|
|
|
95
|
-
async def handle_async_request(
|
|
96
|
-
self,
|
|
97
|
-
request: Request,
|
|
98
|
-
) -> Response:
|
|
93
|
+
async def handle_async_request(self, request: Request) -> Response:
|
|
99
94
|
assert isinstance(request.stream, AsyncByteStream)
|
|
100
95
|
|
|
101
96
|
# ASGI scope.
|
|
@@ -121,7 +116,7 @@ class ASGITransport(AsyncBaseTransport):
|
|
|
121
116
|
# Response.
|
|
122
117
|
status_code = None
|
|
123
118
|
response_headers = None
|
|
124
|
-
body_parts = []
|
|
119
|
+
body_parts: list[bytes] = []
|
|
125
120
|
response_started = False
|
|
126
121
|
response_complete = create_event()
|
|
127
122
|
|
|
@@ -5,6 +5,7 @@ from types import TracebackType
|
|
|
5
5
|
|
|
6
6
|
from .._models import Request, Response
|
|
7
7
|
|
|
8
|
+
# TODO(Marcelo): When Python 3.10 reaches EOF, we can use `typing.Self` instead of defining those two.
|
|
8
9
|
T = typing.TypeVar("T", bound="BaseTransport")
|
|
9
10
|
A = typing.TypeVar("A", bound="AsyncBaseTransport")
|
|
10
11
|
|
|
@@ -26,10 +26,7 @@ class MockTransport(AsyncBaseTransport, BaseTransport):
|
|
|
26
26
|
raise TypeError("Cannot use an async handler in a sync Client")
|
|
27
27
|
return response
|
|
28
28
|
|
|
29
|
-
async def handle_async_request(
|
|
30
|
-
self,
|
|
31
|
-
request: Request,
|
|
32
|
-
) -> Response:
|
|
29
|
+
async def handle_async_request(self, request: Request) -> Response:
|
|
33
30
|
await request.aread()
|
|
34
31
|
response = self.handler(request)
|
|
35
32
|
|
|
@@ -64,14 +64,12 @@ class WSGITransport(BaseTransport):
|
|
|
64
64
|
```
|
|
65
65
|
|
|
66
66
|
Arguments:
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
* `remote_addr` - A string indicating the client IP of incoming requests.
|
|
74
|
-
```
|
|
67
|
+
app: The WSGI application.
|
|
68
|
+
raise_app_exceptions: Boolean indicating if exceptions in the application
|
|
69
|
+
should be raised. Default to `True`. Can be set to `False` for use cases
|
|
70
|
+
such as testing the content of a client 500 response.
|
|
71
|
+
script_name: The root path on which the WSGI application should be mounted.
|
|
72
|
+
remote_addr: A string indicating the client IP of incoming requests.
|
|
75
73
|
"""
|
|
76
74
|
|
|
77
75
|
def __init__(
|
|
@@ -236,8 +236,8 @@ def urlparse(url: str = "", **kwargs: str | None) -> ParseResult:
|
|
|
236
236
|
# Replace "raw_path" with "path" and "query".
|
|
237
237
|
if "raw_path" in kwargs:
|
|
238
238
|
raw_path = kwargs.pop("raw_path") or ""
|
|
239
|
-
kwargs["path"],
|
|
240
|
-
if not
|
|
239
|
+
kwargs["path"], separator, kwargs["query"] = raw_path.partition("?")
|
|
240
|
+
if not separator:
|
|
241
241
|
kwargs["query"] = None
|
|
242
242
|
|
|
243
243
|
# Ensure that IPv6 "host" addresses are always escaped with "[...]".
|
|
@@ -398,7 +398,7 @@ class URL:
|
|
|
398
398
|
return f"{self.__class__.__name__}({url!r})"
|
|
399
399
|
|
|
400
400
|
@property
|
|
401
|
-
def raw(self) -> tuple[bytes, bytes, int, bytes]: # pragma:
|
|
401
|
+
def raw(self) -> tuple[bytes, bytes, int, bytes]: # pragma: no cover
|
|
402
402
|
import collections
|
|
403
403
|
import warnings
|
|
404
404
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
[build-system]
|
|
2
|
-
requires = ["hatchling", "hatch-fancy-pypi-readme", "uv-dynamic-versioning>=0.
|
|
2
|
+
requires = ["hatchling", "hatch-fancy-pypi-readme", "uv-dynamic-versioning>=0.8.0"]
|
|
3
3
|
build-backend = "hatchling.build"
|
|
4
4
|
|
|
5
5
|
[tool.hatch.version]
|
|
@@ -9,6 +9,7 @@ source = "uv-dynamic-versioning"
|
|
|
9
9
|
vcs = "git"
|
|
10
10
|
style = "pep440"
|
|
11
11
|
bump = true
|
|
12
|
+
fallback-version = "0.0.0"
|
|
12
13
|
|
|
13
14
|
[project]
|
|
14
15
|
name = "httpx2"
|
|
@@ -42,7 +43,7 @@ dynamic = ["readme", "version", "dependencies"]
|
|
|
42
43
|
|
|
43
44
|
[tool.hatch.metadata.hooks.uv-dynamic-versioning]
|
|
44
45
|
dependencies = [
|
|
45
|
-
"
|
|
46
|
+
"truststore>=0.10",
|
|
46
47
|
"httpcore2=={{ version }}",
|
|
47
48
|
"anyio",
|
|
48
49
|
"idna",
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
from .asgi import *
|
|
2
|
-
from .base import *
|
|
3
|
-
from .default import *
|
|
4
|
-
from .mock import *
|
|
5
|
-
from .wsgi import *
|
|
6
|
-
|
|
7
|
-
__all__ = [
|
|
8
|
-
"ASGITransport",
|
|
9
|
-
"AsyncBaseTransport",
|
|
10
|
-
"BaseTransport",
|
|
11
|
-
"AsyncHTTPTransport",
|
|
12
|
-
"HTTPTransport",
|
|
13
|
-
"MockTransport",
|
|
14
|
-
"WSGITransport",
|
|
15
|
-
]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|