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.
Files changed (32) hide show
  1. {httpx2-2.2.0 → httpx2-2.4.0}/.gitignore +1 -0
  2. {httpx2-2.2.0 → httpx2-2.4.0}/CHANGELOG.md +31 -2
  3. {httpx2-2.2.0 → httpx2-2.4.0}/PKG-INFO +20 -14
  4. {httpx2-2.2.0 → httpx2-2.4.0}/README.md +3 -5
  5. {httpx2-2.2.0 → httpx2-2.4.0}/httpx2/_api.py +24 -36
  6. {httpx2-2.2.0 → httpx2-2.4.0}/httpx2/_auth.py +3 -3
  7. {httpx2-2.2.0 → httpx2-2.4.0}/httpx2/_client.py +21 -21
  8. {httpx2-2.2.0 → httpx2-2.4.0}/httpx2/_config.py +23 -12
  9. {httpx2-2.2.0 → httpx2-2.4.0}/httpx2/_content.py +4 -11
  10. {httpx2-2.2.0 → httpx2-2.4.0}/httpx2/_decoders.py +20 -14
  11. {httpx2-2.2.0 → httpx2-2.4.0}/httpx2/_exceptions.py +12 -3
  12. {httpx2-2.2.0 → httpx2-2.4.0}/httpx2/_main.py +6 -7
  13. {httpx2-2.2.0 → httpx2-2.4.0}/httpx2/_models.py +17 -22
  14. {httpx2-2.2.0 → httpx2-2.4.0}/httpx2/_multipart.py +4 -5
  15. httpx2-2.4.0/httpx2/_transports/__init__.py +15 -0
  16. {httpx2-2.2.0 → httpx2-2.4.0}/httpx2/_transports/asgi.py +11 -16
  17. {httpx2-2.2.0 → httpx2-2.4.0}/httpx2/_transports/base.py +1 -0
  18. {httpx2-2.2.0 → httpx2-2.4.0}/httpx2/_transports/default.py +7 -11
  19. {httpx2-2.2.0 → httpx2-2.4.0}/httpx2/_transports/mock.py +1 -4
  20. {httpx2-2.2.0 → httpx2-2.4.0}/httpx2/_transports/wsgi.py +7 -10
  21. httpx2-2.4.0/httpx2/_types.py +86 -0
  22. {httpx2-2.2.0 → httpx2-2.4.0}/httpx2/_urlparse.py +3 -3
  23. {httpx2-2.2.0 → httpx2-2.4.0}/httpx2/_urls.py +11 -5
  24. {httpx2-2.2.0 → httpx2-2.4.0}/httpx2/_utils.py +1 -5
  25. {httpx2-2.2.0 → httpx2-2.4.0}/pyproject.toml +8 -5
  26. httpx2-2.2.0/httpx2/_transports/__init__.py +0 -15
  27. httpx2-2.2.0/httpx2/_types.py +0 -110
  28. {httpx2-2.2.0 → httpx2-2.4.0}/LICENSE.md +0 -0
  29. {httpx2-2.2.0 → httpx2-2.4.0}/httpx2/__init__.py +0 -0
  30. {httpx2-2.2.0 → httpx2-2.4.0}/httpx2/__version__.py +0 -0
  31. {httpx2-2.2.0 → httpx2-2.4.0}/httpx2/_status_codes.py +0 -0
  32. {httpx2-2.2.0 → httpx2-2.4.0}/httpx2/py.typed +0 -0
@@ -5,6 +5,7 @@
5
5
  __pycache__/
6
6
  htmlcov/
7
7
  site/
8
+ .wrangler/
8
9
  *.egg-info/
9
10
  venv*/
10
11
  .python-version
@@ -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 prefered style, tho may require updates to test suites.
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` dependancy has been removed. (#2252)
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.2.0
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 :: 4 - Beta
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: certifi
29
- Requires-Dist: httpcore2==2.2.0
30
- Requires-Dist: idna
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<15,>=10; extra == 'cli'
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 3. It includes **an integrated command line client**, has support for both **HTTP/1.1 and HTTP/2**, and provides both **sync and async APIs**.
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
- * `certifi` - SSL certificates.
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 natively via the stdlib [`compression.zstd`](https://docs.python.org/3/library/compression.zstd.html) module.)*
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
- ### Fixed
189
+ ### Added
190
190
 
191
- * Handle multi-frame zstd streams split across chunks. ([#946](https://github.com/pydantic/httpx2/pull/946))
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
- * Lazily import the `_main` CLI module to speed up `import httpx2`. ([#947](https://github.com/pydantic/httpx2/pull/947))
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 3. It includes **an integrated command line client**, has support for both **HTTP/1.1 and HTTP/2**, and provides both **sync and async APIs**.
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
- * `certifi` - SSL certificates.
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 natively via the stdlib [`compression.zstd`](https://docs.python.org/3/library/compression.zstd.html) module.)*
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
- Sends an HTTP request.
59
-
60
- **Parameters:**
61
-
62
- * **method** - HTTP method for the new `Request` object: `GET`, `OPTIONS`,
63
- `HEAD`, `POST`, `PUT`, `PATCH`, or `DELETE`.
64
- * **url** - URL for the new `Request` object.
65
- * **params** - *(optional)* Query parameters to include in the URL, as a
66
- string, dictionary, or sequence of two-tuples.
67
- * **content** - *(optional)* Binary content to include in the body of the
68
- request, as bytes or a byte iterator.
69
- * **data** - *(optional)* Form data to include in the body of the request,
70
- as a dictionary.
71
- * **files** - *(optional)* A dictionary of upload files to include in the
72
- body of the request.
73
- * **json** - *(optional)* A JSON serializable object to include in the body
74
- of the request.
75
- * **headers** - *(optional)* Dictionary of HTTP headers to include in the
76
- request.
77
- * **cookies** - *(optional)* Dictionary of Cookie items to include in the
78
- request.
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
- ) -> typing.Iterator[Response]:
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, unquote
13
+ from ._utils import to_bytes, to_str
14
14
 
15
- if typing.TYPE_CHECKING: # pragma: no cover
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] = unquote(value)
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 is bound to a given response instance, and that
136
- ensures the `response.elapsed` is set once the response is closed.
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, response: Response, start: float) -> None:
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
- for chunk in self._stream:
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 is bound to a given response instance, and that
157
- ensures the `response.elapsed` is set once the response is closed.
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, response: Response, start: float) -> None:
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
- ) -> typing.Iterator[Response]:
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, response=response, start=start)
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
- ) -> typing.AsyncIterator[Response]:
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, response=response, start=start)
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 certifi
31
+ import truststore
32
32
 
33
33
  if verify is True:
34
- if trust_env and os.environ.get("SSL_CERT_FILE"): # pragma: nocover
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: nocover
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 = ssl.create_default_context(cafile=certifi.where())
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): # pragma: nocover
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: nocover
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 # type: typing.Optional[float]
102
- self.read = timeout.read # type: typing.Optional[float]
103
- self.write = timeout.write # type: typing.Optional[float]
104
- self.pool = timeout.pool # type: typing.Optional[float]
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
- for part in self._stream:
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
- data: RequestData,
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, the stdlib includes a built-in zstd implementation, so we can support
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 = True
42
+ _zstandard_installed: bool = False
43
43
  else: # pragma: no cover
44
- if sys.version_info >= (3, 14):
44
+ _zstandard_installed = False
45
+ try:
45
46
  from compression.zstd import ZstdDecompressor, ZstdError
46
47
 
47
48
  _zstandard_installed = True
48
- else:
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
- _zstandard_installed = False
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
- Requires `pip install zstandard`.
188
- Can be installed as a dependency of httpx using `pip install httpx[zstd]`.
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
- def __init__(self, children: typing.Sequence[ContentDecoder]) -> None:
233
+ max_decode_links: typing.ClassVar[int] = 5
234
+
235
+ def __init__(self, encodings: typing.Sequence[str]) -> None:
233
236
  """
234
- 'children' should be a sequence of decoders in the order in which
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 = list(reversed(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,