httpcore2 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 (39) hide show
  1. {httpcore2-2.2.0 → httpcore2-2.4.0}/CHANGELOG.md +21 -4
  2. {httpcore2-2.2.0 → httpcore2-2.4.0}/PKG-INFO +30 -16
  3. {httpcore2-2.2.0 → httpcore2-2.4.0}/README.md +1 -5
  4. {httpcore2-2.2.0 → httpcore2-2.4.0}/httpcore2/__init__.py +2 -2
  5. {httpcore2-2.2.0 → httpcore2-2.4.0}/httpcore2/_api.py +2 -1
  6. {httpcore2-2.2.0 → httpcore2-2.4.0}/httpcore2/_async/__init__.py +2 -2
  7. {httpcore2-2.2.0 → httpcore2-2.4.0}/httpcore2/_async/connection_pool.py +56 -43
  8. {httpcore2-2.2.0 → httpcore2-2.4.0}/httpcore2/_async/http11.py +3 -7
  9. {httpcore2-2.2.0 → httpcore2-2.4.0}/httpcore2/_async/http2.py +12 -15
  10. {httpcore2-2.2.0 → httpcore2-2.4.0}/httpcore2/_async/http_proxy.py +5 -5
  11. {httpcore2-2.2.0 → httpcore2-2.4.0}/httpcore2/_async/interfaces.py +8 -8
  12. {httpcore2-2.2.0 → httpcore2-2.4.0}/httpcore2/_async/socks_proxy.py +8 -8
  13. {httpcore2-2.2.0 → httpcore2-2.4.0}/httpcore2/_backends/anyio.py +12 -10
  14. {httpcore2-2.2.0 → httpcore2-2.4.0}/httpcore2/_backends/auto.py +2 -2
  15. {httpcore2-2.2.0 → httpcore2-2.4.0}/httpcore2/_backends/base.py +17 -21
  16. {httpcore2-2.2.0 → httpcore2-2.4.0}/httpcore2/_backends/sync.py +10 -8
  17. {httpcore2-2.2.0 → httpcore2-2.4.0}/httpcore2/_backends/trio.py +3 -3
  18. {httpcore2-2.2.0 → httpcore2-2.4.0}/httpcore2/_exceptions.py +6 -3
  19. {httpcore2-2.2.0 → httpcore2-2.4.0}/httpcore2/_models.py +17 -18
  20. httpcore2-2.4.0/httpcore2/_ssl.py +12 -0
  21. {httpcore2-2.2.0 → httpcore2-2.4.0}/httpcore2/_sync/__init__.py +2 -2
  22. {httpcore2-2.2.0 → httpcore2-2.4.0}/httpcore2/_sync/connection_pool.py +56 -43
  23. {httpcore2-2.2.0 → httpcore2-2.4.0}/httpcore2/_sync/http11.py +3 -7
  24. {httpcore2-2.2.0 → httpcore2-2.4.0}/httpcore2/_sync/http2.py +12 -15
  25. {httpcore2-2.2.0 → httpcore2-2.4.0}/httpcore2/_sync/http_proxy.py +5 -5
  26. {httpcore2-2.2.0 → httpcore2-2.4.0}/httpcore2/_sync/interfaces.py +8 -8
  27. {httpcore2-2.2.0 → httpcore2-2.4.0}/httpcore2/_sync/socks_proxy.py +8 -8
  28. {httpcore2-2.2.0 → httpcore2-2.4.0}/httpcore2/_synchronization.py +11 -11
  29. {httpcore2-2.2.0 → httpcore2-2.4.0}/httpcore2/_utils.py +3 -3
  30. {httpcore2-2.2.0 → httpcore2-2.4.0}/pyproject.toml +9 -7
  31. httpcore2-2.2.0/httpcore2/_ssl.py +0 -9
  32. {httpcore2-2.2.0 → httpcore2-2.4.0}/.gitignore +0 -0
  33. {httpcore2-2.2.0 → httpcore2-2.4.0}/LICENSE.md +0 -0
  34. {httpcore2-2.2.0 → httpcore2-2.4.0}/httpcore2/_async/connection.py +0 -0
  35. {httpcore2-2.2.0 → httpcore2-2.4.0}/httpcore2/_backends/__init__.py +0 -0
  36. {httpcore2-2.2.0 → httpcore2-2.4.0}/httpcore2/_backends/mock.py +0 -0
  37. {httpcore2-2.2.0 → httpcore2-2.4.0}/httpcore2/_sync/connection.py +0 -0
  38. {httpcore2-2.2.0 → httpcore2-2.4.0}/httpcore2/_trace.py +0 -0
  39. {httpcore2-2.2.0 → httpcore2-2.4.0}/httpcore2/py.typed +0 -0
@@ -4,6 +4,23 @@ 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
+
15
+ ## 2.3.0 (June 1st, 2026)
16
+
17
+ ### Changed
18
+
19
+ * Use `truststore` instead of `certifi` for default SSL verification, loading the operating system's trust store. ([#1002](https://github.com/pydantic/httpx2/pull/1002))
20
+ * Rewrite `_assign_requests_to_connections` as a single-pass loop. ([#974](https://github.com/pydantic/httpx2/pull/974))
21
+ * Use `anyio`'s `fast_acquire` for `Lock` and `Semaphore`. ([#970](https://github.com/pydantic/httpx2/pull/970))
22
+ * Use `memoryview` in `write()` to avoid copies. ([#954](https://github.com/pydantic/httpx2/pull/954))
23
+
7
24
  ## 2.2.0 (May 16th, 2026)
8
25
 
9
26
  No changes since `2.1.0`. Version bumped to stay in lockstep with `httpx2`.
@@ -64,7 +81,7 @@ Historical entries below are from upstream `encode/httpcore`.
64
81
  ## 1.0.5 (March 27th, 2024)
65
82
 
66
83
  - Handle `EndOfStream` exception for anyio backend. (#899)
67
- - Allow trio `0.25.*` series in package dependancies. (#903)
84
+ - Allow trio `0.25.*` series in package dependencies. (#903)
68
85
 
69
86
  ## 1.0.4 (February 21st, 2024)
70
87
 
@@ -142,7 +159,7 @@ The project versioning policy is now explicitly governed by SEMVER. See https://
142
159
  - Allow `ws` and `wss` schemes. Allows us to properly support websocket upgrade connections. (#625)
143
160
  - Forwarding HTTP proxies use a connection-per-remote-host. Required by some proxy implementations. (#637)
144
161
  - Don't raise `RuntimeError` when closing a connection pool with active connections. Removes some error cases when cancellations are used. (#631)
145
- - Lazy import `anyio`, so that it's no longer a hard dependancy, and isn't imported if unused. (#639)
162
+ - Lazy import `anyio`, so that it's no longer a hard dependency, and isn't imported if unused. (#639)
146
163
 
147
164
  ## 0.16.2 (November 25th, 2022)
148
165
 
@@ -200,7 +217,7 @@ The project versioning policy is now explicitly governed by SEMVER. See https://
200
217
  ## 0.14.1 (November 12th, 2021)
201
218
 
202
219
  - `max_connections` becomes optional. (Pull #429)
203
- - `certifi` is now included in the install dependancies. (Pull #428)
220
+ - `certifi` is now included in the install dependencies. (Pull #428)
204
221
  - `h2` is now strictly optional. (Pull #428)
205
222
 
206
223
  ## 0.14.0 (November 11th, 2021)
@@ -265,7 +282,7 @@ Note that `curio` support is not currently available in 0.14.0. If you're using
265
282
 
266
283
  ### Fixed
267
284
 
268
- - More resiliant testing for closed connections. (Pull #311)
285
+ - More resilient testing for closed connections. (Pull #311)
269
286
  - Don't raise exceptions on ungraceful connection closes. (Pull #310)
270
287
 
271
288
  ## 0.13.0 (April 21st, 2021)
@@ -1,16 +1,17 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: httpcore2
3
- Version: 2.2.0
3
+ Version: 2.4.0
4
4
  Summary: A minimal low-level HTTP client.
5
- Project-URL: Documentation, https://www.encode.io/httpcore
6
- Project-URL: Homepage, https://www.encode.io/httpcore/
7
- Project-URL: Source, https://github.com/encode/httpcore
5
+ Project-URL: Changelog, https://github.com/pydantic/httpx2/blob/main/src/httpcore2/CHANGELOG.md
6
+ Project-URL: Homepage, https://github.com/pydantic/httpx2
7
+ Project-URL: Source, https://github.com/pydantic/httpx2/blob/main/src/httpcore2
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
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
@@ -25,10 +26,10 @@ Classifier: Programming Language :: Python :: 3.13
25
26
  Classifier: Programming Language :: Python :: 3.14
26
27
  Classifier: Topic :: Internet :: WWW/HTTP
27
28
  Requires-Python: >=3.10
28
- Requires-Dist: certifi
29
29
  Requires-Dist: h11>=0.16
30
+ Requires-Dist: truststore>=0.10
30
31
  Provides-Extra: asyncio
31
- Requires-Dist: anyio<5.0,>=4.0; extra == 'asyncio'
32
+ Requires-Dist: anyio<5.0,>=4.5.0; extra == 'asyncio'
32
33
  Provides-Extra: http2
33
34
  Requires-Dist: h2<5,>=3; extra == 'http2'
34
35
  Provides-Extra: socks
@@ -62,10 +63,6 @@ Some things HTTP Core does do:
62
63
  * Provides both sync and async interfaces.
63
64
  * Async backend support for `asyncio` and `trio`.
64
65
 
65
- ## Requirements
66
-
67
- Python 3.8+
68
-
69
66
  ## Installation
70
67
 
71
68
  For HTTP/1.1 only support, install with:
@@ -128,7 +125,7 @@ The motivation for `httpcore` is:
128
125
  The `httpcore` package has the following dependencies...
129
126
 
130
127
  * `h11`
131
- * `certifi`
128
+ * `truststore`
132
129
 
133
130
  And the following optional extras...
134
131
 
@@ -154,6 +151,23 @@ All notable changes to this project will be documented in this file.
154
151
 
155
152
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
156
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
+
162
+ ## 2.3.0 (June 1st, 2026)
163
+
164
+ ### Changed
165
+
166
+ * Use `truststore` instead of `certifi` for default SSL verification, loading the operating system's trust store. ([#1002](https://github.com/pydantic/httpx2/pull/1002))
167
+ * Rewrite `_assign_requests_to_connections` as a single-pass loop. ([#974](https://github.com/pydantic/httpx2/pull/974))
168
+ * Use `anyio`'s `fast_acquire` for `Lock` and `Semaphore`. ([#970](https://github.com/pydantic/httpx2/pull/970))
169
+ * Use `memoryview` in `write()` to avoid copies. ([#954](https://github.com/pydantic/httpx2/pull/954))
170
+
157
171
  ## 2.2.0 (May 16th, 2026)
158
172
 
159
173
  No changes since `2.1.0`. Version bumped to stay in lockstep with `httpx2`.
@@ -214,7 +228,7 @@ Historical entries below are from upstream `encode/httpcore`.
214
228
  ## 1.0.5 (March 27th, 2024)
215
229
 
216
230
  - Handle `EndOfStream` exception for anyio backend. (#899)
217
- - Allow trio `0.25.*` series in package dependancies. (#903)
231
+ - Allow trio `0.25.*` series in package dependencies. (#903)
218
232
 
219
233
  ## 1.0.4 (February 21st, 2024)
220
234
 
@@ -292,7 +306,7 @@ The project versioning policy is now explicitly governed by SEMVER. See https://
292
306
  - Allow `ws` and `wss` schemes. Allows us to properly support websocket upgrade connections. (#625)
293
307
  - Forwarding HTTP proxies use a connection-per-remote-host. Required by some proxy implementations. (#637)
294
308
  - Don't raise `RuntimeError` when closing a connection pool with active connections. Removes some error cases when cancellations are used. (#631)
295
- - Lazy import `anyio`, so that it's no longer a hard dependancy, and isn't imported if unused. (#639)
309
+ - Lazy import `anyio`, so that it's no longer a hard dependency, and isn't imported if unused. (#639)
296
310
 
297
311
  ## 0.16.2 (November 25th, 2022)
298
312
 
@@ -350,7 +364,7 @@ The project versioning policy is now explicitly governed by SEMVER. See https://
350
364
  ## 0.14.1 (November 12th, 2021)
351
365
 
352
366
  - `max_connections` becomes optional. (Pull #429)
353
- - `certifi` is now included in the install dependancies. (Pull #428)
367
+ - `certifi` is now included in the install dependencies. (Pull #428)
354
368
  - `h2` is now strictly optional. (Pull #428)
355
369
 
356
370
  ## 0.14.0 (November 11th, 2021)
@@ -415,7 +429,7 @@ Note that `curio` support is not currently available in 0.14.0. If you're using
415
429
 
416
430
  ### Fixed
417
431
 
418
- - More resiliant testing for closed connections. (Pull #311)
432
+ - More resilient testing for closed connections. (Pull #311)
419
433
  - Don't raise exceptions on ungraceful connection closes. (Pull #310)
420
434
 
421
435
  ## 0.13.0 (April 21st, 2021)
@@ -23,10 +23,6 @@ Some things HTTP Core does do:
23
23
  * Provides both sync and async interfaces.
24
24
  * Async backend support for `asyncio` and `trio`.
25
25
 
26
- ## Requirements
27
-
28
- Python 3.8+
29
-
30
26
  ## Installation
31
27
 
32
28
  For HTTP/1.1 only support, install with:
@@ -89,7 +85,7 @@ The motivation for `httpcore` is:
89
85
  The `httpcore` package has the following dependencies...
90
86
 
91
87
  * `h11`
92
- * `certifi`
88
+ * `truststore`
93
89
 
94
90
  And the following optional extras...
95
91
 
@@ -51,7 +51,7 @@ from ._sync import (
51
51
  # The 'httpcore2.AnyIOBackend' class is conditional on 'anyio' being installed.
52
52
  try:
53
53
  from ._backends.anyio import AnyIOBackend
54
- except ImportError: # pragma: nocover
54
+ except ImportError: # pragma: no cover
55
55
 
56
56
  class AnyIOBackend: # type: ignore
57
57
  def __init__(self, *args, **kwargs): # type: ignore
@@ -62,7 +62,7 @@ except ImportError: # pragma: nocover
62
62
  # The 'httpcore2.TrioBackend' class is conditional on 'trio' being installed.
63
63
  try:
64
64
  from ._backends.trio import TrioBackend
65
- except ImportError: # pragma: nocover
65
+ except ImportError: # pragma: no cover
66
66
 
67
67
  class TrioBackend: # type: ignore
68
68
  def __init__(self, *args, **kwargs): # type: ignore
@@ -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
 
@@ -6,7 +6,7 @@ from .interfaces import AsyncConnectionInterface
6
6
 
7
7
  try:
8
8
  from .http2 import AsyncHTTP2Connection
9
- except ImportError: # pragma: nocover
9
+ except ImportError: # pragma: no cover
10
10
 
11
11
  class AsyncHTTP2Connection: # type: ignore
12
12
  def __init__(self, *args, **kwargs) -> None: # type: ignore
@@ -18,7 +18,7 @@ except ImportError: # pragma: nocover
18
18
 
19
19
  try:
20
20
  from .socks_proxy import AsyncSOCKSProxy
21
- except ImportError: # pragma: nocover
21
+ except ImportError: # pragma: no cover
22
22
 
23
23
  class AsyncSOCKSProxy: # type: ignore
24
24
  def __init__(self, *args, **kwargs) -> None: # type: ignore
@@ -229,7 +229,7 @@ class AsyncConnectionPool(AsyncRequestInterface):
229
229
  # In this case we clear the connection and try again.
230
230
  pool_request.clear_connection()
231
231
  else:
232
- break # pragma: nocover
232
+ break # pragma: no cover
233
233
 
234
234
  except BaseException as exc:
235
235
  with self._optional_thread_lock:
@@ -259,38 +259,49 @@ class AsyncConnectionPool(AsyncRequestInterface):
259
259
  Called whenever a new request is added or removed from the pool.
260
260
 
261
261
  Any closing connections are returned, allowing the I/O for closing
262
- those connections to be handled seperately.
262
+ those connections to be handled separately.
263
263
  """
264
- closing_connections = []
264
+ closing_connections: list[AsyncConnectionInterface] = []
265
+ retained_connections: list[AsyncConnectionInterface] = []
265
266
 
266
- # First we handle cleaning up any connections that are closed,
267
- # have expired their keep-alive, or surplus idle connections.
268
- for connection in list(self._connections):
267
+ # First we handle cleaning up any connections that are closed
268
+ # or have expired their keep-alive, in a single pass.
269
+ for connection in self._connections:
269
270
  if connection.is_closed():
270
- # log: "removing closed connection"
271
- self._connections.remove(connection)
271
+ continue
272
272
  elif connection.has_expired():
273
- # log: "closing expired connection"
274
- self._connections.remove(connection)
275
- closing_connections.append(connection)
276
- elif (
277
- connection.is_idle()
278
- and sum(connection.is_idle() for connection in self._connections) > self._max_keepalive_connections
279
- ):
280
- # log: "closing idle connection"
281
- self._connections.remove(connection)
282
273
  closing_connections.append(connection)
274
+ else:
275
+ retained_connections.append(connection)
276
+
277
+ # Then we close any surplus idle connections, to enforce the
278
+ # max_keepalive_connections setting.
279
+ idle_surplus = (
280
+ sum(connection.is_idle() for connection in retained_connections) - self._max_keepalive_connections
281
+ )
282
+ if idle_surplus > 0:
283
+ kept: list[AsyncConnectionInterface] = []
284
+ for connection in retained_connections:
285
+ if idle_surplus > 0 and connection.is_idle():
286
+ closing_connections.append(connection)
287
+ idle_surplus -= 1
288
+ else:
289
+ kept.append(connection)
290
+ retained_connections = kept
291
+
292
+ self._connections = retained_connections
293
+
294
+ # Snapshot the set of reusable connections once, rather than rebuilding
295
+ # it per queued request — this is what brings the loop from O(N*M) to
296
+ # O(N+M) in the common case.
297
+ available_connections = [connection for connection in self._connections if connection.is_available()]
298
+ new_connection_budget = self._max_connections - len(self._connections)
283
299
 
284
300
  # Assign queued requests to connections.
285
- queued_requests = [request for request in self._requests if request.is_queued()]
286
- for pool_request in queued_requests:
301
+ for pool_request in self._requests:
302
+ if not pool_request.is_queued():
303
+ continue
287
304
  origin = pool_request.request.url.origin
288
- available_connections = [
289
- connection
290
- for connection in self._connections
291
- if connection.can_handle_request(origin) and connection.is_available()
292
- ]
293
- idle_connections = [connection for connection in self._connections if connection.is_idle()]
294
305
 
295
306
  # There are three cases for how we may be able to handle the request:
296
307
  #
@@ -298,24 +309,26 @@ class AsyncConnectionPool(AsyncRequestInterface):
298
309
  # 2. We can create a new connection to handle the request.
299
310
  # 3. We can close an idle connection and then create a new connection
300
311
  # to handle the request.
301
- if available_connections:
302
- # log: "reusing existing connection"
303
- connection = available_connections[0]
304
- pool_request.assign_to_connection(connection)
305
- elif len(self._connections) < self._max_connections:
306
- # log: "creating new connection"
307
- connection = self.create_connection(origin)
308
- self._connections.append(connection)
309
- pool_request.assign_to_connection(connection)
310
- elif idle_connections:
311
- # log: "closing idle connection"
312
- connection = idle_connections[0]
313
- self._connections.remove(connection)
314
- closing_connections.append(connection)
315
- # log: "creating new connection"
316
- connection = self.create_connection(origin)
317
- self._connections.append(connection)
318
- pool_request.assign_to_connection(connection)
312
+ for connection in available_connections:
313
+ if connection.can_handle_request(origin):
314
+ pool_request.assign_to_connection(connection)
315
+ break
316
+ else:
317
+ if new_connection_budget > 0:
318
+ connection = self.create_connection(origin)
319
+ self._connections.append(connection)
320
+ pool_request.assign_to_connection(connection)
321
+ new_connection_budget -= 1
322
+ continue
323
+ for idx, connection in enumerate(available_connections):
324
+ if connection.is_idle():
325
+ del available_connections[idx]
326
+ self._connections.remove(connection)
327
+ closing_connections.append(connection)
328
+ connection = self.create_connection(origin)
329
+ self._connections.append(connection)
330
+ pool_request.assign_to_connection(connection)
331
+ break
319
332
 
320
333
  return closing_connections
321
334
 
@@ -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):
@@ -85,9 +81,9 @@ class AsyncHTTP11Connection(AsyncConnectionInterface):
85
81
  await self._send_request_body(**kwargs)
86
82
  except WriteError:
87
83
  # If we get a write error while we're writing the request,
88
- # then we supress this error and move on to attempting to
84
+ # then we suppress this error and move on to attempting to
89
85
  # read the response. Servers can sometimes close the request
90
- # pre-emptively and then respond with a well formed HTTP
86
+ # preemptively and then respond with a well formed HTTP
91
87
  # error response.
92
88
  pass
93
89
 
@@ -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):
@@ -120,9 +116,10 @@ class AsyncHTTP2Connection(AsyncConnectionInterface):
120
116
  try:
121
117
  stream_id = self._h2_state.get_next_available_stream_id()
122
118
  self._events[stream_id] = []
123
- except h2.exceptions.NoAvailableStreamIDError: # pragma: nocover
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:
@@ -161,11 +158,11 @@ class AsyncHTTP2Connection(AsyncConnectionInterface):
161
158
  #
162
159
  # In this case we'll have stored the event, and should raise
163
160
  # it as a RemoteProtocolError.
164
- if self._connection_terminated: # pragma: nocover
161
+ if self._connection_terminated: # pragma: no cover
165
162
  raise RemoteProtocolError(self._connection_terminated)
166
163
  # If h2 raises a protocol error in some other state then we
167
164
  # must somehow have made a protocol violation.
168
- raise LocalProtocolError(exc) # pragma: nocover
165
+ raise LocalProtocolError(exc) # pragma: no cover
169
166
 
170
167
  raise exc
171
168
 
@@ -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
 
@@ -387,7 +384,7 @@ class AsyncHTTP2Connection(AsyncConnectionInterface):
387
384
  if self._keepalive_expiry is not None:
388
385
  now = time.monotonic()
389
386
  self._expire_at = now + self._keepalive_expiry
390
- if self._used_all_stream_ids: # pragma: nocover
387
+ if self._used_all_stream_ids: # pragma: no cover
391
388
  await self.aclose()
392
389
 
393
390
  async def aclose(self) -> None:
@@ -404,7 +401,7 @@ class AsyncHTTP2Connection(AsyncConnectionInterface):
404
401
  timeout = timeouts.get("read", None)
405
402
 
406
403
  if self._read_exception is not None:
407
- raise self._read_exception # pragma: nocover
404
+ raise self._read_exception # pragma: no cover
408
405
 
409
406
  try:
410
407
  data = await self._network_stream.read(self.READ_NUM_BYTES, timeout)
@@ -435,11 +432,11 @@ class AsyncHTTP2Connection(AsyncConnectionInterface):
435
432
  data_to_send = self._h2_state.data_to_send()
436
433
 
437
434
  if self._write_exception is not None:
438
- raise self._write_exception # pragma: nocover
435
+ raise self._write_exception # pragma: no cover
439
436
 
440
437
  try:
441
438
  await self._network_stream.write(data_to_send, timeout)
442
- except Exception as exc: # pragma: nocover
439
+ except Exception as exc: # pragma: no cover
443
440
  # If we get a network error we should:
444
441
  #
445
442
  # 1. Save the exception and just raise it immediately on any future write.
@@ -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,12 +42,12 @@ 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
 
49
49
 
50
- class AsyncHTTPProxy(AsyncConnectionPool): # pragma: nocover
50
+ class AsyncHTTPProxy(AsyncConnectionPool): # pragma: no cover
51
51
  """
52
52
  A connection pool that sends requests via an HTTP proxy.
53
53
  """
@@ -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
 
@@ -82,18 +82,18 @@ class AsyncRequestInterface:
82
82
  await response.aclose()
83
83
 
84
84
  async def handle_async_request(self, request: Request) -> Response:
85
- raise NotImplementedError() # pragma: nocover
85
+ raise NotImplementedError() # pragma: no cover
86
86
 
87
87
 
88
88
  class AsyncConnectionInterface(AsyncRequestInterface):
89
89
  async def aclose(self) -> None:
90
- raise NotImplementedError() # pragma: nocover
90
+ raise NotImplementedError() # pragma: no cover
91
91
 
92
92
  def info(self) -> str:
93
- raise NotImplementedError() # pragma: nocover
93
+ raise NotImplementedError() # pragma: no cover
94
94
 
95
95
  def can_handle_request(self, origin: Origin) -> bool:
96
- raise NotImplementedError() # pragma: nocover
96
+ raise NotImplementedError() # pragma: no cover
97
97
 
98
98
  def is_available(self) -> bool:
99
99
  """
@@ -111,7 +111,7 @@ class AsyncConnectionInterface(AsyncRequestInterface):
111
111
  required exceptions if multiple requests are attempted over a connection
112
112
  that ends up being established as HTTP/1.1.
113
113
  """
114
- raise NotImplementedError() # pragma: nocover
114
+ raise NotImplementedError() # pragma: no cover
115
115
 
116
116
  def has_expired(self) -> bool:
117
117
  """
@@ -120,13 +120,13 @@ class AsyncConnectionInterface(AsyncRequestInterface):
120
120
  This either means that the connection is idle and it has passed the
121
121
  expiry time on its keep-alive, or that server has sent an EOF.
122
122
  """
123
- raise NotImplementedError() # pragma: nocover
123
+ raise NotImplementedError() # pragma: no cover
124
124
 
125
125
  def is_idle(self) -> bool:
126
126
  """
127
127
  Return `True` if the connection is currently idle.
128
128
  """
129
- raise NotImplementedError() # pragma: nocover
129
+ raise NotImplementedError() # pragma: no cover
130
130
 
131
131
  def is_closed(self) -> bool:
132
132
  """
@@ -135,4 +135,4 @@ class AsyncConnectionInterface(AsyncRequestInterface):
135
135
  Used when a response is closed to determine if the connection may be
136
136
  returned to the connection pool or not.
137
137
  """
138
- raise NotImplementedError() # pragma: nocover
138
+ raise NotImplementedError() # pragma: no cover
@@ -96,7 +96,7 @@ async def _init_socks5_connection(
96
96
  raise ProxyError(f"Proxy Server could not connect: {reply_code}.")
97
97
 
98
98
 
99
- class AsyncSOCKSProxy(AsyncConnectionPool): # pragma: nocover
99
+ class AsyncSOCKSProxy(AsyncConnectionPool): # pragma: no cover
100
100
  """
101
101
  A connection pool that sends requests via an HTTP proxy.
102
102
  """
@@ -254,7 +254,7 @@ class AsyncSocks5Connection(AsyncConnectionInterface):
254
254
  http2_negotiated = ssl_object is not None and ssl_object.selected_alpn_protocol() == "h2"
255
255
 
256
256
  # Create the HTTP/1.1 or HTTP/2 connection
257
- if http2_negotiated or (self._http2 and not self._http1): # pragma: nocover
257
+ if http2_negotiated or (self._http2 and not self._http1): # pragma: no cover
258
258
  from .http2 import AsyncHTTP2Connection
259
259
 
260
260
  self._connection = AsyncHTTP2Connection(
@@ -271,7 +271,7 @@ class AsyncSocks5Connection(AsyncConnectionInterface):
271
271
  except Exception as exc:
272
272
  self._connect_failed = True
273
273
  raise exc
274
- elif not self._connection.is_available(): # pragma: nocover
274
+ elif not self._connection.is_available(): # pragma: no cover
275
275
  raise ConnectionNotAvailable()
276
276
 
277
277
  return await self._connection.handle_async_request(request)
@@ -284,7 +284,7 @@ class AsyncSocks5Connection(AsyncConnectionInterface):
284
284
  await self._connection.aclose()
285
285
 
286
286
  def is_available(self) -> bool:
287
- if self._connection is None: # pragma: nocover
287
+ if self._connection is None: # pragma: no cover
288
288
  # If HTTP/2 support is enabled, and the resulting connection could
289
289
  # end up as HTTP/2 then we should indicate the connection as being
290
290
  # available to service multiple requests.
@@ -294,22 +294,22 @@ class AsyncSocks5Connection(AsyncConnectionInterface):
294
294
  return self._connection.is_available()
295
295
 
296
296
  def has_expired(self) -> bool:
297
- if self._connection is None: # pragma: nocover
297
+ if self._connection is None: # pragma: no cover
298
298
  return self._connect_failed
299
299
  return self._connection.has_expired()
300
300
 
301
301
  def is_idle(self) -> bool:
302
- if self._connection is None: # pragma: nocover
302
+ if self._connection is None: # pragma: no cover
303
303
  return self._connect_failed
304
304
  return self._connection.is_idle()
305
305
 
306
306
  def is_closed(self) -> bool:
307
- if self._connection is None: # pragma: nocover
307
+ if self._connection is None: # pragma: no cover
308
308
  return self._connect_failed
309
309
  return self._connection.is_closed()
310
310
 
311
311
  def info(self) -> str:
312
- if self._connection is None: # pragma: nocover
312
+ if self._connection is None: # pragma: no cover
313
313
  return "CONNECTION FAILED" if self._connect_failed else "CONNECTING"
314
314
  return self._connection.info()
315
315