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.
- {httpcore2-2.2.0 → httpcore2-2.4.0}/CHANGELOG.md +21 -4
- {httpcore2-2.2.0 → httpcore2-2.4.0}/PKG-INFO +30 -16
- {httpcore2-2.2.0 → httpcore2-2.4.0}/README.md +1 -5
- {httpcore2-2.2.0 → httpcore2-2.4.0}/httpcore2/__init__.py +2 -2
- {httpcore2-2.2.0 → httpcore2-2.4.0}/httpcore2/_api.py +2 -1
- {httpcore2-2.2.0 → httpcore2-2.4.0}/httpcore2/_async/__init__.py +2 -2
- {httpcore2-2.2.0 → httpcore2-2.4.0}/httpcore2/_async/connection_pool.py +56 -43
- {httpcore2-2.2.0 → httpcore2-2.4.0}/httpcore2/_async/http11.py +3 -7
- {httpcore2-2.2.0 → httpcore2-2.4.0}/httpcore2/_async/http2.py +12 -15
- {httpcore2-2.2.0 → httpcore2-2.4.0}/httpcore2/_async/http_proxy.py +5 -5
- {httpcore2-2.2.0 → httpcore2-2.4.0}/httpcore2/_async/interfaces.py +8 -8
- {httpcore2-2.2.0 → httpcore2-2.4.0}/httpcore2/_async/socks_proxy.py +8 -8
- {httpcore2-2.2.0 → httpcore2-2.4.0}/httpcore2/_backends/anyio.py +12 -10
- {httpcore2-2.2.0 → httpcore2-2.4.0}/httpcore2/_backends/auto.py +2 -2
- {httpcore2-2.2.0 → httpcore2-2.4.0}/httpcore2/_backends/base.py +17 -21
- {httpcore2-2.2.0 → httpcore2-2.4.0}/httpcore2/_backends/sync.py +10 -8
- {httpcore2-2.2.0 → httpcore2-2.4.0}/httpcore2/_backends/trio.py +3 -3
- {httpcore2-2.2.0 → httpcore2-2.4.0}/httpcore2/_exceptions.py +6 -3
- {httpcore2-2.2.0 → httpcore2-2.4.0}/httpcore2/_models.py +17 -18
- httpcore2-2.4.0/httpcore2/_ssl.py +12 -0
- {httpcore2-2.2.0 → httpcore2-2.4.0}/httpcore2/_sync/__init__.py +2 -2
- {httpcore2-2.2.0 → httpcore2-2.4.0}/httpcore2/_sync/connection_pool.py +56 -43
- {httpcore2-2.2.0 → httpcore2-2.4.0}/httpcore2/_sync/http11.py +3 -7
- {httpcore2-2.2.0 → httpcore2-2.4.0}/httpcore2/_sync/http2.py +12 -15
- {httpcore2-2.2.0 → httpcore2-2.4.0}/httpcore2/_sync/http_proxy.py +5 -5
- {httpcore2-2.2.0 → httpcore2-2.4.0}/httpcore2/_sync/interfaces.py +8 -8
- {httpcore2-2.2.0 → httpcore2-2.4.0}/httpcore2/_sync/socks_proxy.py +8 -8
- {httpcore2-2.2.0 → httpcore2-2.4.0}/httpcore2/_synchronization.py +11 -11
- {httpcore2-2.2.0 → httpcore2-2.4.0}/httpcore2/_utils.py +3 -3
- {httpcore2-2.2.0 → httpcore2-2.4.0}/pyproject.toml +9 -7
- httpcore2-2.2.0/httpcore2/_ssl.py +0 -9
- {httpcore2-2.2.0 → httpcore2-2.4.0}/.gitignore +0 -0
- {httpcore2-2.2.0 → httpcore2-2.4.0}/LICENSE.md +0 -0
- {httpcore2-2.2.0 → httpcore2-2.4.0}/httpcore2/_async/connection.py +0 -0
- {httpcore2-2.2.0 → httpcore2-2.4.0}/httpcore2/_backends/__init__.py +0 -0
- {httpcore2-2.2.0 → httpcore2-2.4.0}/httpcore2/_backends/mock.py +0 -0
- {httpcore2-2.2.0 → httpcore2-2.4.0}/httpcore2/_sync/connection.py +0 -0
- {httpcore2-2.2.0 → httpcore2-2.4.0}/httpcore2/_trace.py +0 -0
- {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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
3
|
+
Version: 2.4.0
|
|
4
4
|
Summary: A minimal low-level HTTP client.
|
|
5
|
-
Project-URL:
|
|
6
|
-
Project-URL: Homepage, https://
|
|
7
|
-
Project-URL: Source, https://github.com/
|
|
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 ::
|
|
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
|
-
* `
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
* `
|
|
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:
|
|
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:
|
|
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
|
-
) ->
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
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,
|
|
268
|
-
for connection in
|
|
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
|
-
|
|
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
|
-
|
|
286
|
-
|
|
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
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
connection
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
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 =
|
|
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
|
|
84
|
+
# then we suppress this error and move on to attempting to
|
|
89
85
|
# read the response. Servers can sometimes close the request
|
|
90
|
-
#
|
|
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,
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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 =
|
|
28
|
-
HeadersAsSequence = typing.Sequence[
|
|
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,
|
|
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:
|
|
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 = "
|
|
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:
|
|
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:
|
|
90
|
+
raise NotImplementedError() # pragma: no cover
|
|
91
91
|
|
|
92
92
|
def info(self) -> str:
|
|
93
|
-
raise NotImplementedError() # pragma:
|
|
93
|
+
raise NotImplementedError() # pragma: no cover
|
|
94
94
|
|
|
95
95
|
def can_handle_request(self, origin: Origin) -> bool:
|
|
96
|
-
raise NotImplementedError() # pragma:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|