httpx2 2.3.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 (31) hide show
  1. {httpx2-2.3.0 → httpx2-2.4.0}/CHANGELOG.md +16 -0
  2. {httpx2-2.3.0 → httpx2-2.4.0}/PKG-INFO +16 -11
  3. {httpx2-2.3.0 → httpx2-2.4.0}/README.md +1 -1
  4. {httpx2-2.3.0 → httpx2-2.4.0}/httpx2/_api.py +2 -1
  5. {httpx2-2.3.0 → httpx2-2.4.0}/httpx2/_auth.py +2 -2
  6. {httpx2-2.3.0 → httpx2-2.4.0}/httpx2/_client.py +4 -4
  7. {httpx2-2.3.0 → httpx2-2.4.0}/httpx2/_config.py +9 -4
  8. {httpx2-2.3.0 → httpx2-2.4.0}/httpx2/_content.py +4 -11
  9. {httpx2-2.3.0 → httpx2-2.4.0}/httpx2/_decoders.py +10 -5
  10. {httpx2-2.3.0 → httpx2-2.4.0}/httpx2/_exceptions.py +12 -3
  11. {httpx2-2.3.0 → httpx2-2.4.0}/httpx2/_main.py +5 -5
  12. {httpx2-2.3.0 → httpx2-2.4.0}/httpx2/_models.py +13 -21
  13. {httpx2-2.3.0 → httpx2-2.4.0}/httpx2/_multipart.py +3 -4
  14. {httpx2-2.3.0 → httpx2-2.4.0}/httpx2/_transports/asgi.py +1 -1
  15. {httpx2-2.3.0 → httpx2-2.4.0}/httpx2/_transports/default.py +7 -11
  16. {httpx2-2.3.0 → httpx2-2.4.0}/httpx2/_transports/wsgi.py +1 -2
  17. httpx2-2.4.0/httpx2/_types.py +86 -0
  18. {httpx2-2.3.0 → httpx2-2.4.0}/httpx2/_urlparse.py +1 -1
  19. {httpx2-2.3.0 → httpx2-2.4.0}/httpx2/_urls.py +10 -4
  20. {httpx2-2.3.0 → httpx2-2.4.0}/httpx2/_utils.py +0 -4
  21. {httpx2-2.3.0 → httpx2-2.4.0}/pyproject.toml +5 -3
  22. httpx2-2.3.0/httpx2/_types.py +0 -110
  23. {httpx2-2.3.0 → httpx2-2.4.0}/.gitignore +0 -0
  24. {httpx2-2.3.0 → httpx2-2.4.0}/LICENSE.md +0 -0
  25. {httpx2-2.3.0 → httpx2-2.4.0}/httpx2/__init__.py +0 -0
  26. {httpx2-2.3.0 → httpx2-2.4.0}/httpx2/__version__.py +0 -0
  27. {httpx2-2.3.0 → httpx2-2.4.0}/httpx2/_status_codes.py +0 -0
  28. {httpx2-2.3.0 → httpx2-2.4.0}/httpx2/_transports/__init__.py +0 -0
  29. {httpx2-2.3.0 → httpx2-2.4.0}/httpx2/_transports/base.py +0 -0
  30. {httpx2-2.3.0 → httpx2-2.4.0}/httpx2/_transports/mock.py +0 -0
  31. {httpx2-2.3.0 → httpx2-2.4.0}/httpx2/py.typed +0 -0
@@ -4,6 +4,22 @@ 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
+
7
23
  ## 2.3.0 (June 1st, 2026)
8
24
 
9
25
  ### Changed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: httpx2
3
- Version: 2.3.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: httpcore2==2.3.0
29
- Requires-Dist: idna
29
+ Requires-Dist: httpcore2==2.4.0
30
+ Requires-Dist: idna>=3.18
30
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.
@@ -184,16 +186,19 @@ inspiration around the lower-level networking details.
184
186
 
185
187
  ## Release Information
186
188
 
189
+ ### Added
190
+
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
+
187
193
  ### Changed
188
194
 
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))
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))
190
197
 
191
198
  ### Fixed
192
199
 
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))
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))
197
202
 
198
203
 
199
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.
@@ -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
@@ -125,7 +126,7 @@ def stream(
125
126
  follow_redirects: bool = False,
126
127
  verify: ssl.SSLContext | str | bool = True,
127
128
  trust_env: bool = True,
128
- ) -> typing.Iterator[Response]:
129
+ ) -> Generator[Response]:
129
130
  """
130
131
  Alternative to `httpx2.request()` that streams the response body
131
132
  instead of loading it into memory at once.
@@ -10,7 +10,7 @@ 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
15
  if typing.TYPE_CHECKING:
16
16
  from hashlib import _Hash
@@ -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
 
@@ -143,8 +144,7 @@ class BoundSyncStream(SyncByteStream):
143
144
  self.elapsed: datetime.timedelta | None = None
144
145
 
145
146
  def __iter__(self) -> typing.Iterator[bytes]:
146
- for chunk in self._stream:
147
- yield chunk
147
+ yield from self._stream
148
148
 
149
149
  def close(self) -> None:
150
150
  self.elapsed = datetime.timedelta(seconds=time.perf_counter() - self._start)
@@ -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.
@@ -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.
@@ -89,6 +89,11 @@ class Timeout:
89
89
  # 5s timeout elsewhere.
90
90
  """
91
91
 
92
+ connect: float | None
93
+ read: float | None
94
+ write: float | None
95
+ pool: float | None
96
+
92
97
  def __init__(
93
98
  self,
94
99
  timeout: TimeoutTypes | UnsetType = UNSET,
@@ -104,10 +109,10 @@ class Timeout:
104
109
  assert read is UNSET
105
110
  assert write is UNSET
106
111
  assert pool is UNSET
107
- self.connect = timeout.connect # type: typing.Optional[float]
108
- self.read = timeout.read # type: typing.Optional[float]
109
- self.write = timeout.write # type: typing.Optional[float]
110
- 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
111
116
  elif isinstance(timeout, tuple):
112
117
  # Passed as a tuple.
113
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])
@@ -39,7 +39,7 @@ if typing.TYPE_CHECKING:
39
39
 
40
40
  ZstdDecompressor = functools.partial(_ZstdDecompressor().decompressobj)
41
41
 
42
- _zstandard_installed: bool
42
+ _zstandard_installed: bool = False
43
43
  else: # pragma: no cover
44
44
  _zstandard_installed = False
45
45
  try:
@@ -230,13 +230,18 @@ class MultiDecoder(ContentDecoder):
230
230
  Handle the case where multiple encodings have been applied.
231
231
  """
232
232
 
233
- 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:
234
236
  """
235
- 'children' should be a sequence of decoders in the order in which
237
+ 'encodings' should be the content codings in the order in which
236
238
  each was applied.
237
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.")
238
243
  # Note that we reverse the order for decoding.
239
- self.children = list(reversed(children))
244
+ self.children: list[ContentDecoder] = [SUPPORTED_DECODERS[coding]() for coding in reversed(codings)]
240
245
 
241
246
  def decode(self, data: bytes) -> bytes:
242
247
  for child in self.children:
@@ -397,7 +402,7 @@ class LineDecoder:
397
402
  return lines
398
403
 
399
404
 
400
- SUPPORTED_DECODERS = {
405
+ SUPPORTED_DECODERS: dict[str, type[ContentDecoder]] = {
401
406
  "identity": IdentityDecoder,
402
407
  "gzip": GZipDecoder,
403
408
  "deflate": DeflateDecoder,
@@ -35,6 +35,7 @@ from __future__ import annotations
35
35
 
36
36
  import contextlib
37
37
  import typing
38
+ from collections.abc import Generator
38
39
 
39
40
  if typing.TYPE_CHECKING:
40
41
  from ._models import Request, Response # pragma: no cover
@@ -71,6 +72,16 @@ __all__ = [
71
72
  ]
72
73
 
73
74
 
75
+ class HTTPXDeprecationWarning(UserWarning):
76
+ """A custom deprecation warning for HTTPX.
77
+
78
+ Unlike the built-in `DeprecationWarning`, this inherits from `UserWarning` to ensure it is visible by default,
79
+ helping users discover deprecated features without needing to enable warnings explicitly.
80
+
81
+ Reference: https://sethmlarson.dev/deprecations-via-warnings-dont-work-for-python-libraries
82
+ """
83
+
84
+
74
85
  class HTTPError(Exception):
75
86
  """
76
87
  Base class for `RequestError` and `HTTPStatusError`.
@@ -356,9 +367,7 @@ class RequestNotRead(StreamError):
356
367
 
357
368
 
358
369
  @contextlib.contextmanager
359
- def request_context(
360
- request: Request | None = None,
361
- ) -> typing.Iterator[None]:
370
+ def request_context(request: Request | None = None) -> Generator[None]:
362
371
  """
363
372
  A context manager that can be used to attach the given request context
364
373
  to any `RequestError` exceptions that are raised within the block.
@@ -168,13 +168,13 @@ def print_response(response: Response) -> None:
168
168
  console.print(f"<{len(response.content)} bytes of binary data>")
169
169
 
170
170
 
171
- _PCTRTT = typing.Tuple[typing.Tuple[str, str], ...]
172
- _PCTRTTT = typing.Tuple[_PCTRTT, ...]
173
- _PeerCertRetDictType = typing.Dict[str, typing.Union[str, _PCTRTTT, _PCTRTT]]
171
+ _PCTRTT = tuple[tuple[str, str], ...]
172
+ _PCTRTTT = tuple[_PCTRTT, ...]
173
+ _PeerCertRetDictType = dict[str, str | _PCTRTTT | _PCTRTT]
174
174
 
175
175
 
176
176
  def format_certificate(cert: _PeerCertRetDictType) -> str: # pragma: no cover
177
- lines = []
177
+ lines: list[str] = []
178
178
  for key, value in cert.items():
179
179
  if isinstance(value, (list, tuple)):
180
180
  lines.append(f"* {key}:")
@@ -462,7 +462,7 @@ def main(
462
462
  params=list(params),
463
463
  content=content,
464
464
  data=dict(data),
465
- files=files, # type: ignore
465
+ files=files, # type: ignore[arg-type]
466
466
  json=json,
467
467
  headers=headers,
468
468
  cookies=dict(cookies),
@@ -12,7 +12,6 @@ from http.cookiejar import Cookie, CookieJar
12
12
 
13
13
  from ._content import ByteStream, UnattachedStream, encode_request, encode_response
14
14
  from ._decoders import (
15
- SUPPORTED_DECODERS,
16
15
  ByteChunker,
17
16
  ContentDecoder,
18
17
  IdentityDecoder,
@@ -146,7 +145,7 @@ class Headers(typing.MutableMapping[str, str]):
146
145
  headers: HeaderTypes | None = None,
147
146
  encoding: str | None = None,
148
147
  ) -> None:
149
- self._list = [] # type: typing.List[typing.Tuple[bytes, bytes, bytes]]
148
+ self._list: list[tuple[bytes, bytes, bytes]] = []
150
149
 
151
150
  if isinstance(headers, Headers):
152
151
  self._list = list(headers._list)
@@ -200,7 +199,7 @@ class Headers(typing.MutableMapping[str, str]):
200
199
  return [(raw_key, value) for raw_key, _, value in self._list]
201
200
 
202
201
  def keys(self) -> typing.KeysView[str]:
203
- return {key.decode(self.encoding): None for _, key, value in self._list}.keys()
202
+ return {key.decode(self.encoding): None for _, key, _value in self._list}.keys()
204
203
 
205
204
  def values(self) -> typing.ValuesView[str]:
206
205
  values_dict: dict[str, str] = {}
@@ -263,7 +262,7 @@ class Headers(typing.MutableMapping[str, str]):
263
262
  if not split_commas:
264
263
  return values
265
264
 
266
- split_values = []
265
+ split_values: list[str] = []
267
266
  for value in values:
268
267
  split_values.extend([item.strip() for item in value.split(",")])
269
268
  return split_values
@@ -679,20 +678,13 @@ class Response:
679
678
  content, depending on the Content-Encoding used in the response.
680
679
  """
681
680
  if not hasattr(self, "_decoder"):
682
- decoders: list[ContentDecoder] = []
683
681
  values = self.headers.get_list("content-encoding", split_commas=True)
684
- for value in values:
685
- value = value.strip().lower()
686
- try:
687
- decoder_cls = SUPPORTED_DECODERS[value]
688
- decoders.append(decoder_cls())
689
- except KeyError:
690
- continue
691
-
692
- if len(decoders) == 1:
693
- self._decoder = decoders[0]
694
- elif len(decoders) > 1:
695
- self._decoder = MultiDecoder(children=decoders)
682
+ encodings = [value.strip().lower() for value in values]
683
+ decoder = MultiDecoder([encoding for encoding in encodings if encoding != "identity"])
684
+ if len(decoder.children) == 1:
685
+ self._decoder = decoder.children[0]
686
+ elif decoder.children:
687
+ self._decoder = decoder
696
688
  else:
697
689
  self._decoder = IdentityDecoder()
698
690
 
@@ -1161,7 +1153,7 @@ class Cookies(typing.MutableMapping[str, str]):
1161
1153
  Delete all cookies. Optionally include a domain and path in
1162
1154
  order to only delete a subset of all the cookies.
1163
1155
  """
1164
- args = []
1156
+ args: list[str] = []
1165
1157
  if domain is not None:
1166
1158
  args.append(domain)
1167
1159
  if path is not None:
@@ -1218,9 +1210,9 @@ class Cookies(typing.MutableMapping[str, str]):
1218
1210
  )
1219
1211
  self.request = request
1220
1212
 
1221
- def add_unredirected_header(self, key: str, value: str) -> None:
1222
- super().add_unredirected_header(key, value)
1223
- self.request.headers[key] = value
1213
+ def add_unredirected_header(self, key: str, val: str) -> None:
1214
+ super().add_unredirected_header(key, val)
1215
+ self.request.headers[key] = val
1224
1216
 
1225
1217
  class _CookieCompatResponse:
1226
1218
  """
@@ -22,7 +22,7 @@ from ._utils import (
22
22
  )
23
23
 
24
24
  _HTML5_FORM_ENCODING_REPLACEMENTS = {'"': "%22", "\\": "\\\\"}
25
- _HTML5_FORM_ENCODING_REPLACEMENTS.update({chr(c): "%{:02X}".format(c) for c in range(0x1F + 1) if c != 0x1B})
25
+ _HTML5_FORM_ENCODING_REPLACEMENTS.update({chr(c): f"%{c:02X}" for c in range(0x1F + 1) if c != 0x1B})
26
26
  _HTML5_FORM_ENCODING_RE = re.compile(r"|".join([re.escape(c) for c in _HTML5_FORM_ENCODING_REPLACEMENTS.keys()]))
27
27
 
28
28
 
@@ -219,7 +219,7 @@ class MultipartStream(SyncByteStream, AsyncByteStream):
219
219
  boundary = os.urandom(16).hex().encode("ascii")
220
220
 
221
221
  self.boundary = boundary
222
- self.content_type = "multipart/form-data; boundary=%s" % boundary.decode("ascii")
222
+ self.content_type = f"multipart/form-data; boundary={boundary.decode('ascii')}"
223
223
  self.fields = list(self._iter_fields(data, files))
224
224
 
225
225
  def _iter_fields(self, data: RequestData, files: RequestFiles) -> typing.Iterator[FileField | DataField]:
@@ -271,8 +271,7 @@ class MultipartStream(SyncByteStream, AsyncByteStream):
271
271
  return {"Content-Length": str(content_length), "Content-Type": content_type}
272
272
 
273
273
  def __iter__(self) -> typing.Iterator[bytes]:
274
- for chunk in self.iter_chunks():
275
- yield chunk
274
+ yield from self.iter_chunks()
276
275
 
277
276
  async def __aiter__(self) -> typing.AsyncIterator[bytes]:
278
277
  for chunk in self.iter_chunks():
@@ -11,7 +11,7 @@ if typing.TYPE_CHECKING:
11
11
 
12
12
  import trio
13
13
 
14
- Event = typing.Union[asyncio.Event, trio.Event]
14
+ Event = asyncio.Event | trio.Event
15
15
 
16
16
 
17
17
  _Message = typing.MutableMapping[str, typing.Any]
@@ -28,12 +28,13 @@ from __future__ import annotations
28
28
 
29
29
  import contextlib
30
30
  import typing
31
+ from collections.abc import Generator
31
32
  from types import TracebackType
32
33
 
33
- if typing.TYPE_CHECKING:
34
- import ssl # pragma: no cover
34
+ if typing.TYPE_CHECKING: # pragma: no cover
35
+ import ssl
35
36
 
36
- import httpx2 # pragma: no cover
37
+ import httpx2
37
38
 
38
39
  from .._config import DEFAULT_LIMITS, Limits, Proxy, create_ssl_context
39
40
  from .._exceptions import (
@@ -60,11 +61,7 @@ from .base import AsyncBaseTransport, BaseTransport
60
61
  T = typing.TypeVar("T", bound="HTTPTransport")
61
62
  A = typing.TypeVar("A", bound="AsyncHTTPTransport")
62
63
 
63
- SOCKET_OPTION = typing.Union[
64
- typing.Tuple[int, int, int],
65
- typing.Tuple[int, int, typing.Union[bytes, bytearray]],
66
- typing.Tuple[int, int, None, int],
67
- ]
64
+ SOCKET_OPTION = tuple[int, int, int] | tuple[int, int, bytes | bytearray] | tuple[int, int, None, int]
68
65
 
69
66
  __all__ = ["AsyncHTTPTransport", "HTTPTransport"]
70
67
 
@@ -93,7 +90,7 @@ def _load_httpcore_exceptions() -> dict[type[Exception], type[httpx2.HTTPError]]
93
90
 
94
91
 
95
92
  @contextlib.contextmanager
96
- def map_httpcore_exceptions() -> typing.Iterator[None]:
93
+ def map_httpcore_exceptions() -> Generator[None]:
97
94
  global HTTPCORE_EXC_MAP
98
95
  if len(HTTPCORE_EXC_MAP) == 0:
99
96
  HTTPCORE_EXC_MAP = _load_httpcore_exceptions()
@@ -124,8 +121,7 @@ class ResponseStream(SyncByteStream):
124
121
 
125
122
  def __iter__(self) -> typing.Iterator[bytes]:
126
123
  with map_httpcore_exceptions():
127
- for part in self._httpcore_stream:
128
- yield part
124
+ yield from self._httpcore_stream
129
125
 
130
126
  def close(self) -> None:
131
127
  if hasattr(self._httpcore_stream, "close"):
@@ -33,8 +33,7 @@ class WSGIByteStream(SyncByteStream):
33
33
  self._result = _skip_leading_empty_chunks(result)
34
34
 
35
35
  def __iter__(self) -> typing.Iterator[bytes]:
36
- for part in self._result:
37
- yield part
36
+ yield from self._result
38
37
 
39
38
  def close(self) -> None:
40
39
  if self._close is not None:
@@ -0,0 +1,86 @@
1
+ """
2
+ Type definitions for type checking purposes.
3
+ """
4
+
5
+ from collections.abc import AsyncIterable, AsyncIterator, Callable, Iterable, Iterator, Mapping, Sequence
6
+ from http.cookiejar import CookieJar
7
+ from typing import IO, TYPE_CHECKING, Any, Union
8
+
9
+ if TYPE_CHECKING:
10
+ from ._auth import Auth # noqa: F401
11
+ from ._config import Proxy, Timeout # noqa: F401
12
+ from ._models import Cookies, Headers, Request # noqa: F401
13
+ from ._urls import URL, QueryParams # noqa: F401
14
+
15
+
16
+ PrimitiveData = str | int | float | bool | None
17
+
18
+ URLTypes = Union["URL", str]
19
+
20
+ QueryParamTypes = Union[
21
+ "QueryParams",
22
+ Mapping[str, PrimitiveData | Sequence[PrimitiveData]],
23
+ list[tuple[str, PrimitiveData]],
24
+ tuple[tuple[str, PrimitiveData], ...],
25
+ str,
26
+ bytes,
27
+ ]
28
+
29
+ HeaderTypes = Union[
30
+ "Headers",
31
+ Mapping[str, str],
32
+ Mapping[bytes, bytes],
33
+ Sequence[tuple[str, str]],
34
+ Sequence[tuple[bytes, bytes]],
35
+ ]
36
+
37
+ CookieTypes = Union["Cookies", CookieJar, dict[str, str], list[tuple[str, str]]]
38
+
39
+ TimeoutTypes = Union[float | None, tuple[float | None, float | None, float | None, float | None], "Timeout"]
40
+ ProxyTypes = Union["URL", str, "Proxy"]
41
+ CertTypes = str | tuple[str, str] | tuple[str, str, str]
42
+
43
+ AuthTypes = Union[tuple[str | bytes, str | bytes], Callable[["Request"], "Request"], "Auth"]
44
+
45
+ RequestContent = str | bytes | Iterable[bytes] | AsyncIterable[bytes]
46
+ ResponseContent = str | bytes | Iterable[bytes] | AsyncIterable[bytes]
47
+ ResponseExtensions = Mapping[str, Any]
48
+
49
+ RequestData = Mapping[str, Any]
50
+
51
+ FileContent = IO[bytes] | bytes | str
52
+ FileTypes = (
53
+ # # file (or bytes)
54
+ FileContent
55
+ # # (filename, file (or bytes))
56
+ | tuple[str | None, FileContent]
57
+ # # (filename, file (or bytes), content_type)
58
+ | tuple[str | None, FileContent, str | None]
59
+ | tuple[str | None, FileContent, str | None, Mapping[str, str]]
60
+ )
61
+ RequestFiles = Mapping[str, FileTypes] | Sequence[tuple[str, FileTypes]]
62
+
63
+ RequestExtensions = Mapping[str, Any]
64
+
65
+ __all__ = ["AsyncByteStream", "SyncByteStream"]
66
+
67
+
68
+ class SyncByteStream:
69
+ def __iter__(self) -> Iterator[bytes]:
70
+ raise NotImplementedError("The '__iter__' method must be implemented.") # pragma: no cover
71
+ yield b"" # pragma: no cover
72
+
73
+ def close(self) -> None:
74
+ """
75
+ Subclasses can override this method to release any network resources
76
+ after a request/response cycle is complete.
77
+ """
78
+
79
+
80
+ class AsyncByteStream:
81
+ async def __aiter__(self) -> AsyncIterator[bytes]:
82
+ raise NotImplementedError("The '__aiter__' method must be implemented.") # pragma: no cover
83
+ yield b"" # pragma: no cover
84
+
85
+ async def aclose(self) -> None:
86
+ pass
@@ -480,7 +480,7 @@ def quote(string: str, safe: str) -> str:
480
480
  need to be escaped. Unreserved characters are always treated as safe.
481
481
  See: https://www.rfc-editor.org/rfc/rfc3986#section-2.3
482
482
  """
483
- parts = []
483
+ parts: list[str] = []
484
484
  current_position = 0
485
485
  for match in re.finditer(PERCENT_ENCODED_REGEX, string):
486
486
  start_position, end_position = match.start(), match.end()
@@ -1,10 +1,17 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import sys
3
4
  import typing
4
5
  from urllib.parse import parse_qs, unquote, urlencode
5
6
 
6
7
  import idna
7
8
 
9
+ if sys.version_info >= (3, 13):
10
+ from warnings import deprecated # pragma: no cover
11
+ else:
12
+ from typing_extensions import deprecated # pragma: no cover
13
+
14
+ from ._exceptions import HTTPXDeprecationWarning
8
15
  from ._types import QueryParamTypes
9
16
  from ._urlparse import urlparse
10
17
  from ._utils import primitive_value_to_str
@@ -184,8 +191,8 @@ class URL:
184
191
  """
185
192
  host: str = self._uri_reference.host
186
193
 
187
- if host.startswith("xn--"):
188
- host = idna.decode(host)
194
+ if "xn--" in host:
195
+ host = idna.decode(host, display=True)
189
196
 
190
197
  return host
191
198
 
@@ -398,11 +405,10 @@ class URL:
398
405
  return f"{self.__class__.__name__}({url!r})"
399
406
 
400
407
  @property
408
+ @deprecated("URL.raw is deprecated.", category=HTTPXDeprecationWarning)
401
409
  def raw(self) -> tuple[bytes, bytes, int, bytes]: # pragma: no cover
402
410
  import collections
403
- import warnings
404
411
 
405
- warnings.warn("URL.raw is deprecated.")
406
412
  RawURL = collections.namedtuple("RawURL", ["raw_scheme", "raw_host", "port", "raw_path"])
407
413
  return RawURL(
408
414
  raw_scheme=self.raw_scheme,
@@ -86,10 +86,6 @@ def to_bytes_or_str(value: str, match_type_of: typing.AnyStr) -> typing.AnyStr:
86
86
  return value if isinstance(match_type_of, str) else value.encode()
87
87
 
88
88
 
89
- def unquote(value: str) -> str:
90
- return value[1:-1] if value[0] == value[-1] == '"' else value
91
-
92
-
93
89
  def peek_filelike_length(stream: typing.Any) -> int | None:
94
90
  """
95
91
  Given a file-like stream object, return its length in number of bytes
@@ -23,8 +23,9 @@ maintainers = [
23
23
  { name = "Pydantic Services Inc.", email = "engineering@pydantic.dev" }
24
24
  ]
25
25
  classifiers = [
26
- "Development Status :: 4 - Beta",
26
+ "Development Status :: 5 - Production/Stable",
27
27
  "Environment :: Web Environment",
28
+ "Framework :: AnyIO",
28
29
  "Framework :: AsyncIO",
29
30
  "Framework :: Trio",
30
31
  "Intended Audience :: Developers",
@@ -46,7 +47,8 @@ dependencies = [
46
47
  "truststore>=0.10",
47
48
  "httpcore2=={{ version }}",
48
49
  "anyio",
49
- "idna",
50
+ "idna>=3.18",
51
+ "typing_extensions>=4.5.0; python_version < '3.13'",
50
52
  ]
51
53
 
52
54
  [project.optional-dependencies]
@@ -57,7 +59,7 @@ brotli = [
57
59
  cli = [
58
60
  "click==8.*",
59
61
  "pygments==2.*",
60
- "rich>=10,<15",
62
+ "rich>=10,<16",
61
63
  ]
62
64
  http2 = [
63
65
  "h2>=3,<5",
@@ -1,110 +0,0 @@
1
- """
2
- Type definitions for type checking purposes.
3
- """
4
-
5
- from http.cookiejar import CookieJar
6
- from typing import (
7
- IO,
8
- TYPE_CHECKING,
9
- Any,
10
- AsyncIterable,
11
- AsyncIterator,
12
- Callable,
13
- Dict,
14
- Iterable,
15
- Iterator,
16
- List,
17
- Mapping,
18
- Optional,
19
- Sequence,
20
- Tuple,
21
- Union,
22
- )
23
-
24
- if TYPE_CHECKING:
25
- from ._auth import Auth # noqa: F401
26
- from ._config import Proxy, Timeout # noqa: F401
27
- from ._models import Cookies, Headers, Request # noqa: F401
28
- from ._urls import URL, QueryParams # noqa: F401
29
-
30
-
31
- PrimitiveData = Optional[Union[str, int, float, bool]]
32
-
33
- URLTypes = Union["URL", str]
34
-
35
- QueryParamTypes = Union[
36
- "QueryParams",
37
- Mapping[str, Union[PrimitiveData, Sequence[PrimitiveData]]],
38
- List[Tuple[str, PrimitiveData]],
39
- Tuple[Tuple[str, PrimitiveData], ...],
40
- str,
41
- bytes,
42
- ]
43
-
44
- HeaderTypes = Union[
45
- "Headers",
46
- Mapping[str, str],
47
- Mapping[bytes, bytes],
48
- Sequence[Tuple[str, str]],
49
- Sequence[Tuple[bytes, bytes]],
50
- ]
51
-
52
- CookieTypes = Union["Cookies", CookieJar, Dict[str, str], List[Tuple[str, str]]]
53
-
54
- TimeoutTypes = Union[
55
- Optional[float],
56
- Tuple[Optional[float], Optional[float], Optional[float], Optional[float]],
57
- "Timeout",
58
- ]
59
- ProxyTypes = Union["URL", str, "Proxy"]
60
- CertTypes = Union[str, Tuple[str, str], Tuple[str, str, str]]
61
-
62
- AuthTypes = Union[
63
- Tuple[Union[str, bytes], Union[str, bytes]],
64
- Callable[["Request"], "Request"],
65
- "Auth",
66
- ]
67
-
68
- RequestContent = Union[str, bytes, Iterable[bytes], AsyncIterable[bytes]]
69
- ResponseContent = Union[str, bytes, Iterable[bytes], AsyncIterable[bytes]]
70
- ResponseExtensions = Mapping[str, Any]
71
-
72
- RequestData = Mapping[str, Any]
73
-
74
- FileContent = Union[IO[bytes], bytes, str]
75
- FileTypes = Union[
76
- # file (or bytes)
77
- FileContent,
78
- # (filename, file (or bytes))
79
- Tuple[Optional[str], FileContent],
80
- # (filename, file (or bytes), content_type)
81
- Tuple[Optional[str], FileContent, Optional[str]],
82
- # (filename, file (or bytes), content_type, headers)
83
- Tuple[Optional[str], FileContent, Optional[str], Mapping[str, str]],
84
- ]
85
- RequestFiles = Union[Mapping[str, FileTypes], Sequence[Tuple[str, FileTypes]]]
86
-
87
- RequestExtensions = Mapping[str, Any]
88
-
89
- __all__ = ["AsyncByteStream", "SyncByteStream"]
90
-
91
-
92
- class SyncByteStream:
93
- def __iter__(self) -> Iterator[bytes]:
94
- raise NotImplementedError("The '__iter__' method must be implemented.") # pragma: no cover
95
- yield b"" # pragma: no cover
96
-
97
- def close(self) -> None:
98
- """
99
- Subclasses can override this method to release any network resources
100
- after a request/response cycle is complete.
101
- """
102
-
103
-
104
- class AsyncByteStream:
105
- async def __aiter__(self) -> AsyncIterator[bytes]:
106
- raise NotImplementedError("The '__aiter__' method must be implemented.") # pragma: no cover
107
- yield b"" # pragma: no cover
108
-
109
- async def aclose(self) -> None:
110
- pass
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes