httpx2 2.0.0__tar.gz → 2.1.0__tar.gz

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