httpcore2 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 (38) hide show
  1. {httpcore2-2.3.0 → httpcore2-2.4.0}/CHANGELOG.md +8 -0
  2. {httpcore2-2.3.0 → httpcore2-2.4.0}/PKG-INFO +11 -2
  3. {httpcore2-2.3.0 → httpcore2-2.4.0}/httpcore2/_api.py +2 -1
  4. {httpcore2-2.3.0 → httpcore2-2.4.0}/httpcore2/_async/http11.py +1 -5
  5. {httpcore2-2.3.0 → httpcore2-2.4.0}/httpcore2/_async/http2.py +5 -8
  6. {httpcore2-2.3.0 → httpcore2-2.4.0}/httpcore2/_async/http_proxy.py +4 -4
  7. {httpcore2-2.3.0 → httpcore2-2.4.0}/httpcore2/_backends/anyio.py +7 -5
  8. {httpcore2-2.3.0 → httpcore2-2.4.0}/httpcore2/_backends/base.py +1 -5
  9. {httpcore2-2.3.0 → httpcore2-2.4.0}/httpcore2/_exceptions.py +5 -2
  10. {httpcore2-2.3.0 → httpcore2-2.4.0}/httpcore2/_models.py +6 -7
  11. {httpcore2-2.3.0 → httpcore2-2.4.0}/httpcore2/_sync/http11.py +1 -5
  12. {httpcore2-2.3.0 → httpcore2-2.4.0}/httpcore2/_sync/http2.py +5 -8
  13. {httpcore2-2.3.0 → httpcore2-2.4.0}/httpcore2/_sync/http_proxy.py +4 -4
  14. {httpcore2-2.3.0 → httpcore2-2.4.0}/httpcore2/_synchronization.py +1 -1
  15. {httpcore2-2.3.0 → httpcore2-2.4.0}/pyproject.toml +2 -1
  16. {httpcore2-2.3.0 → httpcore2-2.4.0}/.gitignore +0 -0
  17. {httpcore2-2.3.0 → httpcore2-2.4.0}/LICENSE.md +0 -0
  18. {httpcore2-2.3.0 → httpcore2-2.4.0}/README.md +0 -0
  19. {httpcore2-2.3.0 → httpcore2-2.4.0}/httpcore2/__init__.py +0 -0
  20. {httpcore2-2.3.0 → httpcore2-2.4.0}/httpcore2/_async/__init__.py +0 -0
  21. {httpcore2-2.3.0 → httpcore2-2.4.0}/httpcore2/_async/connection.py +0 -0
  22. {httpcore2-2.3.0 → httpcore2-2.4.0}/httpcore2/_async/connection_pool.py +0 -0
  23. {httpcore2-2.3.0 → httpcore2-2.4.0}/httpcore2/_async/interfaces.py +0 -0
  24. {httpcore2-2.3.0 → httpcore2-2.4.0}/httpcore2/_async/socks_proxy.py +0 -0
  25. {httpcore2-2.3.0 → httpcore2-2.4.0}/httpcore2/_backends/__init__.py +0 -0
  26. {httpcore2-2.3.0 → httpcore2-2.4.0}/httpcore2/_backends/auto.py +0 -0
  27. {httpcore2-2.3.0 → httpcore2-2.4.0}/httpcore2/_backends/mock.py +0 -0
  28. {httpcore2-2.3.0 → httpcore2-2.4.0}/httpcore2/_backends/sync.py +0 -0
  29. {httpcore2-2.3.0 → httpcore2-2.4.0}/httpcore2/_backends/trio.py +0 -0
  30. {httpcore2-2.3.0 → httpcore2-2.4.0}/httpcore2/_ssl.py +0 -0
  31. {httpcore2-2.3.0 → httpcore2-2.4.0}/httpcore2/_sync/__init__.py +0 -0
  32. {httpcore2-2.3.0 → httpcore2-2.4.0}/httpcore2/_sync/connection.py +0 -0
  33. {httpcore2-2.3.0 → httpcore2-2.4.0}/httpcore2/_sync/connection_pool.py +0 -0
  34. {httpcore2-2.3.0 → httpcore2-2.4.0}/httpcore2/_sync/interfaces.py +0 -0
  35. {httpcore2-2.3.0 → httpcore2-2.4.0}/httpcore2/_sync/socks_proxy.py +0 -0
  36. {httpcore2-2.3.0 → httpcore2-2.4.0}/httpcore2/_trace.py +0 -0
  37. {httpcore2-2.3.0 → httpcore2-2.4.0}/httpcore2/_utils.py +0 -0
  38. {httpcore2-2.3.0 → httpcore2-2.4.0}/httpcore2/py.typed +0 -0
@@ -4,6 +4,14 @@ 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
+ ### Fixed
10
+
11
+ * Move HTTP/2 stream events cleanup inside `_state_lock`. ([#1013](https://github.com/pydantic/httpx2/pull/1013))
12
+ * Release the HTTP/2 semaphore permit on `NoAvailableStreamIDError`. ([#1012](https://github.com/pydantic/httpx2/pull/1012))
13
+ * Use `RLock` instead of `Lock` to prevent a thread deadlock. ([#1008](https://github.com/pydantic/httpx2/pull/1008))
14
+
7
15
  ## 2.3.0 (June 1st, 2026)
8
16
 
9
17
  ### Changed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: httpcore2
3
- Version: 2.3.0
3
+ Version: 2.4.0
4
4
  Summary: A minimal low-level HTTP client.
5
5
  Project-URL: Changelog, https://github.com/pydantic/httpx2/blob/main/src/httpcore2/CHANGELOG.md
6
6
  Project-URL: Homepage, https://github.com/pydantic/httpx2
@@ -9,8 +9,9 @@ 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
11
  License-File: LICENSE.md
12
- Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Development Status :: 5 - Production/Stable
13
13
  Classifier: Environment :: Web Environment
14
+ Classifier: Framework :: AnyIO
14
15
  Classifier: Framework :: AsyncIO
15
16
  Classifier: Framework :: Trio
16
17
  Classifier: Intended Audience :: Developers
@@ -150,6 +151,14 @@ All notable changes to this project will be documented in this file.
150
151
 
151
152
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
152
153
 
154
+ ## 2.4.0 (June 11th, 2026)
155
+
156
+ ### Fixed
157
+
158
+ * Move HTTP/2 stream events cleanup inside `_state_lock`. ([#1013](https://github.com/pydantic/httpx2/pull/1013))
159
+ * Release the HTTP/2 semaphore permit on `NoAvailableStreamIDError`. ([#1012](https://github.com/pydantic/httpx2/pull/1012))
160
+ * Use `RLock` instead of `Lock` to prevent a thread deadlock. ([#1008](https://github.com/pydantic/httpx2/pull/1008))
161
+
153
162
  ## 2.3.0 (June 1st, 2026)
154
163
 
155
164
  ### Changed
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import contextlib
4
4
  import typing
5
+ from collections.abc import Generator
5
6
 
6
7
  from ._models import URL, Extensions, HeaderTypes, Response
7
8
  from ._sync.connection_pool import ConnectionPool
@@ -55,7 +56,7 @@ def stream(
55
56
  headers: HeaderTypes = None,
56
57
  content: bytes | typing.Iterator[bytes] | None = None,
57
58
  extensions: Extensions | None = None,
58
- ) -> typing.Iterator[Response]:
59
+ ) -> Generator[Response]:
59
60
  """
60
61
  Sends an HTTP request, returning the response within a content manager.
61
62
 
@@ -28,11 +28,7 @@ logger = logging.getLogger("httpcore2.http11")
28
28
 
29
29
 
30
30
  # A subset of `h11.Event` types supported by `_send_event`
31
- H11SendEvent = typing.Union[
32
- h11.Request,
33
- h11.Data,
34
- h11.EndOfMessage,
35
- ]
31
+ H11SendEvent = h11.Request | h11.Data | h11.EndOfMessage
36
32
 
37
33
 
38
34
  class HTTPConnectionState(enum.IntEnum):
@@ -14,11 +14,7 @@ import h2.exceptions
14
14
  import h2.settings
15
15
 
16
16
  from .._backends.base import AsyncNetworkStream
17
- from .._exceptions import (
18
- ConnectionNotAvailable,
19
- LocalProtocolError,
20
- RemoteProtocolError,
21
- )
17
+ from .._exceptions import ConnectionNotAvailable, LocalProtocolError, RemoteProtocolError
22
18
  from .._models import Origin, Request, Response
23
19
  from .._synchronization import AsyncLock, AsyncSemaphore, AsyncShieldCancellation
24
20
  from .._trace import Trace
@@ -29,7 +25,7 @@ logger = logging.getLogger("httpcore2.http2")
29
25
 
30
26
 
31
27
  def has_body_headers(request: Request) -> bool:
32
- return any(k.lower() == b"content-length" or k.lower() == b"transfer-encoding" for k, v in request.headers)
28
+ return any(k.lower() == b"content-length" or k.lower() == b"transfer-encoding" for k, _v in request.headers)
33
29
 
34
30
 
35
31
  class HTTPConnectionState(enum.IntEnum):
@@ -123,6 +119,7 @@ class AsyncHTTP2Connection(AsyncConnectionInterface):
123
119
  except h2.exceptions.NoAvailableStreamIDError: # pragma: no cover
124
120
  self._used_all_stream_ids = True
125
121
  self._request_count -= 1
122
+ await self._max_streams_semaphore.release()
126
123
  raise ConnectionNotAvailable()
127
124
 
128
125
  try:
@@ -275,7 +272,7 @@ class AsyncHTTP2Connection(AsyncConnectionInterface):
275
272
  break
276
273
 
277
274
  status_code = 200
278
- headers = []
275
+ headers: list[tuple[bytes, bytes]] = []
279
276
  assert event.headers is not None
280
277
  for k, v in event.headers:
281
278
  if k == b":status":
@@ -377,8 +374,8 @@ class AsyncHTTP2Connection(AsyncConnectionInterface):
377
374
 
378
375
  async def _response_closed(self, stream_id: int) -> None:
379
376
  await self._max_streams_semaphore.release()
380
- del self._events[stream_id]
381
377
  async with self._state_lock:
378
+ del self._events[stream_id]
382
379
  if self._connection_terminated and not self._events:
383
380
  await self.aclose()
384
381
 
@@ -24,8 +24,8 @@ from .connection_pool import AsyncConnectionPool
24
24
  from .http11 import AsyncHTTP11Connection
25
25
  from .interfaces import AsyncConnectionInterface
26
26
 
27
- ByteOrStr = typing.Union[bytes, str]
28
- HeadersAsSequence = typing.Sequence[typing.Tuple[ByteOrStr, ByteOrStr]]
27
+ ByteOrStr = bytes | str
28
+ HeadersAsSequence = typing.Sequence[tuple[ByteOrStr, ByteOrStr]]
29
29
  HeadersAsMapping = typing.Mapping[ByteOrStr, ByteOrStr]
30
30
 
31
31
 
@@ -42,7 +42,7 @@ def merge_headers(
42
42
  """
43
43
  default_headers = [] if default_headers is None else list(default_headers)
44
44
  override_headers = [] if override_headers is None else list(override_headers)
45
- has_override = set(key.lower() for key, value in override_headers)
45
+ has_override = set(key.lower() for key, _value in override_headers)
46
46
  default_headers = [(key, value) for key, value in default_headers if key.lower() not in has_override]
47
47
  return default_headers + override_headers
48
48
 
@@ -278,7 +278,7 @@ class AsyncTunnelHTTPConnection(AsyncConnectionInterface):
278
278
  if connect_response.status < 200 or connect_response.status > 299:
279
279
  reason_bytes = connect_response.extensions.get("reason_phrase", b"")
280
280
  reason_str = reason_bytes.decode("ascii", errors="ignore")
281
- msg = "%d %s" % (connect_response.status, reason_str)
281
+ msg = f"{connect_response.status} {reason_str}"
282
282
  await self._connection.aclose()
283
283
  raise ProxyError(msg)
284
284
 
@@ -4,6 +4,8 @@ import ssl
4
4
  import typing
5
5
 
6
6
  import anyio
7
+ import anyio.abc
8
+ import anyio.streams.tls
7
9
 
8
10
  from .._exceptions import (
9
11
  ConnectError,
@@ -23,7 +25,7 @@ class AnyIOStream(AsyncNetworkStream):
23
25
  self._stream = stream
24
26
 
25
27
  async def read(self, max_bytes: int, timeout: float | None = None) -> bytes:
26
- exc_map = {
28
+ exc_map: dict[type[Exception], type[Exception]] = {
27
29
  TimeoutError: ReadTimeout,
28
30
  anyio.BrokenResourceError: ReadError,
29
31
  anyio.ClosedResourceError: ReadError,
@@ -40,7 +42,7 @@ class AnyIOStream(AsyncNetworkStream):
40
42
  if not buffer:
41
43
  return
42
44
 
43
- exc_map = {
45
+ exc_map: dict[type[Exception], type[Exception]] = {
44
46
  TimeoutError: WriteTimeout,
45
47
  anyio.BrokenResourceError: WriteError,
46
48
  anyio.ClosedResourceError: WriteError,
@@ -58,7 +60,7 @@ class AnyIOStream(AsyncNetworkStream):
58
60
  server_hostname: str | None = None,
59
61
  timeout: float | None = None,
60
62
  ) -> AsyncNetworkStream:
61
- exc_map = {
63
+ exc_map: dict[type[Exception], type[Exception]] = {
62
64
  TimeoutError: ConnectTimeout,
63
65
  anyio.BrokenResourceError: ConnectError,
64
66
  anyio.EndOfStream: ConnectError,
@@ -105,7 +107,7 @@ class AnyIOBackend(AsyncNetworkBackend):
105
107
  ) -> AsyncNetworkStream: # pragma: no cover
106
108
  if socket_options is None:
107
109
  socket_options = []
108
- exc_map = {
110
+ exc_map: dict[type[Exception], type[Exception]] = {
109
111
  TimeoutError: ConnectTimeout,
110
112
  OSError: ConnectError,
111
113
  anyio.BrokenResourceError: ConnectError,
@@ -130,7 +132,7 @@ class AnyIOBackend(AsyncNetworkBackend):
130
132
  ) -> AsyncNetworkStream: # pragma: no cover
131
133
  if socket_options is None:
132
134
  socket_options = []
133
- exc_map = {
135
+ exc_map: dict[type[Exception], type[Exception]] = {
134
136
  TimeoutError: ConnectTimeout,
135
137
  OSError: ConnectError,
136
138
  anyio.BrokenResourceError: ConnectError,
@@ -4,11 +4,7 @@ import ssl
4
4
  import time
5
5
  import typing
6
6
 
7
- SOCKET_OPTION = typing.Union[
8
- typing.Tuple[int, int, int],
9
- typing.Tuple[int, int, typing.Union[bytes, bytearray]],
10
- typing.Tuple[int, int, None, int],
11
- ]
7
+ SOCKET_OPTION = tuple[int, int, int] | tuple[int, int, bytes | bytearray] | tuple[int, int, None, int]
12
8
 
13
9
 
14
10
  class NetworkStream:
@@ -1,11 +1,14 @@
1
+ from __future__ import annotations
2
+
1
3
  import contextlib
2
4
  import typing
5
+ from collections.abc import Generator
3
6
 
4
- ExceptionMapping = typing.Mapping[typing.Type[Exception], typing.Type[Exception]]
7
+ ExceptionMapping = typing.Mapping[type[Exception], type[Exception]]
5
8
 
6
9
 
7
10
  @contextlib.contextmanager
8
- def map_exceptions(map: ExceptionMapping) -> typing.Iterator[None]:
11
+ def map_exceptions(map: ExceptionMapping) -> Generator[None]:
9
12
  try:
10
13
  yield
11
14
  except Exception as exc: # noqa: PIE786
@@ -11,10 +11,10 @@ from ._utils import safe_async_iterate
11
11
  # Functions for typechecking...
12
12
 
13
13
 
14
- ByteOrStr = typing.Union[bytes, str]
15
- HeadersAsSequence = typing.Sequence[typing.Tuple[ByteOrStr, ByteOrStr]]
14
+ ByteOrStr = bytes | str
15
+ HeadersAsSequence = typing.Sequence[tuple[ByteOrStr, ByteOrStr]]
16
16
  HeadersAsMapping = typing.Mapping[ByteOrStr, ByteOrStr]
17
- HeaderTypes = typing.Union[HeadersAsSequence, HeadersAsMapping, None]
17
+ HeaderTypes = HeadersAsSequence | HeadersAsMapping | None
18
18
 
19
19
  Extensions = typing.MutableMapping[str, typing.Any]
20
20
 
@@ -110,10 +110,10 @@ DEFAULT_PORTS = {
110
110
  def include_request_headers(
111
111
  headers: list[tuple[bytes, bytes]],
112
112
  *,
113
- url: "URL",
113
+ url: URL,
114
114
  content: None | bytes | typing.Iterable[bytes] | typing.AsyncIterable[bytes],
115
115
  ) -> list[tuple[bytes, bytes]]:
116
- headers_set = {k.lower() for k, v in headers}
116
+ headers_set = {k.lower() for k, _v in headers}
117
117
 
118
118
  if b"host" not in headers_set:
119
119
  default_port = DEFAULT_PORTS.get(url.scheme)
@@ -417,8 +417,7 @@ class Response:
417
417
  if self._stream_consumed:
418
418
  raise RuntimeError("Attempted to call 'for ... in response.iter_stream()' more than once.")
419
419
  self._stream_consumed = True
420
- for chunk in self.stream:
421
- yield chunk
420
+ yield from self.stream
422
421
 
423
422
  def close(self) -> None:
424
423
  if not isinstance(self.stream, typing.Iterable): # pragma: no cover
@@ -28,11 +28,7 @@ logger = logging.getLogger("httpcore2.http11")
28
28
 
29
29
 
30
30
  # A subset of `h11.Event` types supported by `_send_event`
31
- H11SendEvent = typing.Union[
32
- h11.Request,
33
- h11.Data,
34
- h11.EndOfMessage,
35
- ]
31
+ H11SendEvent = h11.Request | h11.Data | h11.EndOfMessage
36
32
 
37
33
 
38
34
  class HTTPConnectionState(enum.IntEnum):
@@ -14,11 +14,7 @@ import h2.exceptions
14
14
  import h2.settings
15
15
 
16
16
  from .._backends.base import NetworkStream
17
- from .._exceptions import (
18
- ConnectionNotAvailable,
19
- LocalProtocolError,
20
- RemoteProtocolError,
21
- )
17
+ from .._exceptions import ConnectionNotAvailable, LocalProtocolError, RemoteProtocolError
22
18
  from .._models import Origin, Request, Response
23
19
  from .._synchronization import Lock, Semaphore, ShieldCancellation
24
20
  from .._trace import Trace
@@ -29,7 +25,7 @@ logger = logging.getLogger("httpcore2.http2")
29
25
 
30
26
 
31
27
  def has_body_headers(request: Request) -> bool:
32
- return any(k.lower() == b"content-length" or k.lower() == b"transfer-encoding" for k, v in request.headers)
28
+ return any(k.lower() == b"content-length" or k.lower() == b"transfer-encoding" for k, _v in request.headers)
33
29
 
34
30
 
35
31
  class HTTPConnectionState(enum.IntEnum):
@@ -123,6 +119,7 @@ class HTTP2Connection(ConnectionInterface):
123
119
  except h2.exceptions.NoAvailableStreamIDError: # pragma: no cover
124
120
  self._used_all_stream_ids = True
125
121
  self._request_count -= 1
122
+ self._max_streams_semaphore.release()
126
123
  raise ConnectionNotAvailable()
127
124
 
128
125
  try:
@@ -275,7 +272,7 @@ class HTTP2Connection(ConnectionInterface):
275
272
  break
276
273
 
277
274
  status_code = 200
278
- headers = []
275
+ headers: list[tuple[bytes, bytes]] = []
279
276
  assert event.headers is not None
280
277
  for k, v in event.headers:
281
278
  if k == b":status":
@@ -377,8 +374,8 @@ class HTTP2Connection(ConnectionInterface):
377
374
 
378
375
  def _response_closed(self, stream_id: int) -> None:
379
376
  self._max_streams_semaphore.release()
380
- del self._events[stream_id]
381
377
  with self._state_lock:
378
+ del self._events[stream_id]
382
379
  if self._connection_terminated and not self._events:
383
380
  self.close()
384
381
 
@@ -24,8 +24,8 @@ from .connection_pool import ConnectionPool
24
24
  from .http11 import HTTP11Connection
25
25
  from .interfaces import ConnectionInterface
26
26
 
27
- ByteOrStr = typing.Union[bytes, str]
28
- HeadersAsSequence = typing.Sequence[typing.Tuple[ByteOrStr, ByteOrStr]]
27
+ ByteOrStr = bytes | str
28
+ HeadersAsSequence = typing.Sequence[tuple[ByteOrStr, ByteOrStr]]
29
29
  HeadersAsMapping = typing.Mapping[ByteOrStr, ByteOrStr]
30
30
 
31
31
 
@@ -42,7 +42,7 @@ def merge_headers(
42
42
  """
43
43
  default_headers = [] if default_headers is None else list(default_headers)
44
44
  override_headers = [] if override_headers is None else list(override_headers)
45
- has_override = set(key.lower() for key, value in override_headers)
45
+ has_override = set(key.lower() for key, _value in override_headers)
46
46
  default_headers = [(key, value) for key, value in default_headers if key.lower() not in has_override]
47
47
  return default_headers + override_headers
48
48
 
@@ -278,7 +278,7 @@ class TunnelHTTPConnection(ConnectionInterface):
278
278
  if connect_response.status < 200 or connect_response.status > 299:
279
279
  reason_bytes = connect_response.extensions.get("reason_phrase", b"")
280
280
  reason_str = reason_bytes.decode("ascii", errors="ignore")
281
- msg = "%d %s" % (connect_response.status, reason_str)
281
+ msg = f"{connect_response.status} {reason_str}"
282
282
  self._connection.close()
283
283
  raise ProxyError(msg)
284
284
 
@@ -254,7 +254,7 @@ class ThreadLock:
254
254
  """
255
255
 
256
256
  def __init__(self) -> None:
257
- self._lock = threading.Lock()
257
+ self._lock = threading.RLock()
258
258
 
259
259
  def __enter__(self) -> ThreadLock:
260
260
  self._lock.acquire()
@@ -26,8 +26,9 @@ maintainers = [
26
26
  { name = "Pydantic Services Inc.", email = "engineering@pydantic.dev" },
27
27
  ]
28
28
  classifiers = [
29
- "Development Status :: 3 - Alpha",
29
+ "Development Status :: 5 - Production/Stable",
30
30
  "Environment :: Web Environment",
31
+ "Framework :: AnyIO",
31
32
  "Framework :: AsyncIO",
32
33
  "Framework :: Trio",
33
34
  "Intended Audience :: Developers",
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes