boring-http 2.1.0__tar.gz → 2.2.1__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.
- {boring_http-2.1.0 → boring_http-2.2.1}/PKG-INFO +3 -3
- {boring_http-2.1.0 → boring_http-2.2.1}/README.md +2 -2
- {boring_http-2.1.0 → boring_http-2.2.1}/pyproject.toml +1 -1
- {boring_http-2.1.0 → boring_http-2.2.1}/pyproject.toml.orig +1 -1
- {boring_http-2.1.0 → boring_http-2.2.1}/src/boring_http/client.py +28 -8
- {boring_http-2.1.0 → boring_http-2.2.1}/src/boring_http/cookies.py +9 -9
- {boring_http-2.1.0 → boring_http-2.2.1}/src/boring_http/http2.py +5 -1
- {boring_http-2.1.0 → boring_http-2.2.1}/src/boring_http/http3.py +11 -3
- {boring_http-2.1.0 → boring_http-2.2.1}/src/boring_http/pool.py +31 -3
- {boring_http-2.1.0 → boring_http-2.2.1}/src/boring_http/__init__.py +0 -0
- {boring_http-2.1.0 → boring_http-2.2.1}/src/boring_http/codings.py +0 -0
- {boring_http-2.1.0 → boring_http-2.2.1}/src/boring_http/errors.py +0 -0
- {boring_http-2.1.0 → boring_http-2.2.1}/src/boring_http/hpack.py +0 -0
- {boring_http-2.1.0 → boring_http-2.2.1}/src/boring_http/hpack_tables.py +0 -0
- {boring_http-2.1.0 → boring_http-2.2.1}/src/boring_http/http1.py +0 -0
- {boring_http-2.1.0 → boring_http-2.2.1}/src/boring_http/message.py +0 -0
- {boring_http-2.1.0 → boring_http-2.2.1}/src/boring_http/public_suffixes.py +0 -0
- {boring_http-2.1.0 → boring_http-2.2.1}/src/boring_http/py.typed +0 -0
- {boring_http-2.1.0 → boring_http-2.2.1}/src/boring_http/qpack.py +0 -0
- {boring_http-2.1.0 → boring_http-2.2.1}/src/boring_http/qpack_tables.py +0 -0
- {boring_http-2.1.0 → boring_http-2.2.1}/src/boring_http/quic/__init__.py +0 -0
- {boring_http-2.1.0 → boring_http-2.2.1}/src/boring_http/quic/connection.py +0 -0
- {boring_http-2.1.0 → boring_http-2.2.1}/src/boring_http/quic/packet.py +0 -0
- {boring_http-2.1.0 → boring_http-2.2.1}/src/boring_http/quic/protocol.py +0 -0
- {boring_http-2.1.0 → boring_http-2.2.1}/src/boring_http/quic/recovery.py +0 -0
- {boring_http-2.1.0 → boring_http-2.2.1}/src/boring_http/quic/streams.py +0 -0
- {boring_http-2.1.0 → boring_http-2.2.1}/src/boring_http/revocation.py +0 -0
- {boring_http-2.1.0 → boring_http-2.2.1}/src/boring_http/timing.py +0 -0
- {boring_http-2.1.0 → boring_http-2.2.1}/src/boring_http/websocket.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: boring-http
|
|
3
|
-
Version: 2.1
|
|
3
|
+
Version: 2.2.1
|
|
4
4
|
Summary: Asyncio HTTP client on BoringSSL: HTTP/1.1, HTTP/2 and HTTP/3 over QUIC v1 and v2, WebSocket on all three, HTTP and SOCKS proxies, headers in your order
|
|
5
5
|
Requires-Dist: boring-ssl2>=1.2.0
|
|
6
6
|
Requires-Dist: ssl2>=2.0.1
|
|
@@ -42,7 +42,7 @@ async def main() -> None:
|
|
|
42
42
|
asyncio.run(main())
|
|
43
43
|
```
|
|
44
44
|
|
|
45
|
-
Certificates are checked against the system trust store, or against `trust_store=TrustStore.from_file(...)` from `ssl2`; `verify=False` turns the check off for a test server. Revocation is checked against the CRL the certificate names: fetched over HTTP, verified with the issuer from the chain, cached until its next update, and a listed certificate is refused with `RevocationError`. `revocation=Revocation.STRICT` also refuses when no valid CRL can be had, `Revocation.NONE` skips the check, and the default `Revocation.SOFT` lets such a connection through, as browsers do. The check covers the origin's certificate and an HTTPS proxy's own certificate on a plain-HTTP request through it; the proxy's certificate on a TLS tunnel is verified but not checked for revocation. Headers go on the wire in the order given, client headers first and then the request's own. `Host` leads an HTTP/1.1 request; HTTP/2 and HTTP/3 lowercase the names and drop the connection-specific ones, nothing else changes. `params`, `json`, `data`, `content`, `files`, `auth` and `cookies` are accepted by `get`, `post`, `put`, `patch`, `delete`, `head`, `options` and `request`.
|
|
45
|
+
Certificates are checked against the system trust store, or against `trust_store=TrustStore.from_file(...)` from `ssl2`; `verify=False` turns the check off for a test server. Revocation is checked against the CRL the certificate names: fetched over HTTP, verified with the issuer from the chain, cached until its next update, and a listed certificate is refused with `RevocationError`. `revocation=Revocation.STRICT` also refuses when no valid CRL can be had, `Revocation.NONE` skips the check, and the default `Revocation.SOFT` lets such a connection through, as browsers do. The check covers the origin's certificate and an HTTPS proxy's own certificate on a plain-HTTP request through it; the proxy's certificate on a TLS tunnel is verified but not checked for revocation. `certificate_chain=SigningChain(...)` from `ssl2` presents a client certificate over TCP and QUIC. `check_peer=` is awaited with the peer's chain in DER, leaf first, and its authority after the same checks; raising from it refuses the connection, which is where a key pin goes. Connections are pooled per origin and proxy under `max_connections` and `max_connections_per_host`; one that has served nothing for `max_idle_time` seconds, 90 by default, is closed instead of reused, and `None` keeps it until the server closes it. Headers go on the wire in the order given, client headers first and then the request's own. `Host` leads an HTTP/1.1 request; HTTP/2 and HTTP/3 lowercase the names and drop the connection-specific ones, nothing else changes. `params`, `json`, `data`, `content`, `files`, `auth` and `cookies` are accepted by `get`, `post`, `put`, `patch`, `delete`, `head`, `options` and `request`.
|
|
46
46
|
|
|
47
47
|
## Responses
|
|
48
48
|
|
|
@@ -455,7 +455,7 @@ async def main() -> None:
|
|
|
455
455
|
asyncio.run(main())
|
|
456
456
|
```
|
|
457
457
|
|
|
458
|
-
Over HTTP/1.1 this is the Upgrade handshake. Over HTTP/2 and HTTP/3 it is Extended CONNECT when the server advertises `SETTINGS_ENABLE_CONNECT_PROTOCOL`, otherwise the client opens an HTTP/1.1 connection for it. `subprotocols=[...]` offers subprotocols; `websocket.subprotocol` is the one chosen. The `total` of `timeout` covers the handshake only; afterwards `read` and `write` still limit each wait, so `Timeout(read=None)` lets `receive()` wait through any silence.
|
|
458
|
+
Over HTTP/1.1 this is the Upgrade handshake. Over HTTP/2 and HTTP/3 it is Extended CONNECT when the server advertises `SETTINGS_ENABLE_CONNECT_PROTOCOL`, otherwise the client opens an HTTP/1.1 connection for it. Each WebSocket has a connection of its own, which `close()` closes after the closing handshake. `subprotocols=[...]` offers subprotocols; `websocket.subprotocol` is the one chosen. The `total` of `timeout` covers the handshake only; afterwards `read` and `write` still limit each wait, so `Timeout(read=None)` lets `receive()` wait through any silence.
|
|
459
459
|
|
|
460
460
|
## uvloop
|
|
461
461
|
|
|
@@ -32,7 +32,7 @@ async def main() -> None:
|
|
|
32
32
|
asyncio.run(main())
|
|
33
33
|
```
|
|
34
34
|
|
|
35
|
-
Certificates are checked against the system trust store, or against `trust_store=TrustStore.from_file(...)` from `ssl2`; `verify=False` turns the check off for a test server. Revocation is checked against the CRL the certificate names: fetched over HTTP, verified with the issuer from the chain, cached until its next update, and a listed certificate is refused with `RevocationError`. `revocation=Revocation.STRICT` also refuses when no valid CRL can be had, `Revocation.NONE` skips the check, and the default `Revocation.SOFT` lets such a connection through, as browsers do. The check covers the origin's certificate and an HTTPS proxy's own certificate on a plain-HTTP request through it; the proxy's certificate on a TLS tunnel is verified but not checked for revocation. Headers go on the wire in the order given, client headers first and then the request's own. `Host` leads an HTTP/1.1 request; HTTP/2 and HTTP/3 lowercase the names and drop the connection-specific ones, nothing else changes. `params`, `json`, `data`, `content`, `files`, `auth` and `cookies` are accepted by `get`, `post`, `put`, `patch`, `delete`, `head`, `options` and `request`.
|
|
35
|
+
Certificates are checked against the system trust store, or against `trust_store=TrustStore.from_file(...)` from `ssl2`; `verify=False` turns the check off for a test server. Revocation is checked against the CRL the certificate names: fetched over HTTP, verified with the issuer from the chain, cached until its next update, and a listed certificate is refused with `RevocationError`. `revocation=Revocation.STRICT` also refuses when no valid CRL can be had, `Revocation.NONE` skips the check, and the default `Revocation.SOFT` lets such a connection through, as browsers do. The check covers the origin's certificate and an HTTPS proxy's own certificate on a plain-HTTP request through it; the proxy's certificate on a TLS tunnel is verified but not checked for revocation. `certificate_chain=SigningChain(...)` from `ssl2` presents a client certificate over TCP and QUIC. `check_peer=` is awaited with the peer's chain in DER, leaf first, and its authority after the same checks; raising from it refuses the connection, which is where a key pin goes. Connections are pooled per origin and proxy under `max_connections` and `max_connections_per_host`; one that has served nothing for `max_idle_time` seconds, 90 by default, is closed instead of reused, and `None` keeps it until the server closes it. Headers go on the wire in the order given, client headers first and then the request's own. `Host` leads an HTTP/1.1 request; HTTP/2 and HTTP/3 lowercase the names and drop the connection-specific ones, nothing else changes. `params`, `json`, `data`, `content`, `files`, `auth` and `cookies` are accepted by `get`, `post`, `put`, `patch`, `delete`, `head`, `options` and `request`.
|
|
36
36
|
|
|
37
37
|
## Responses
|
|
38
38
|
|
|
@@ -445,7 +445,7 @@ async def main() -> None:
|
|
|
445
445
|
asyncio.run(main())
|
|
446
446
|
```
|
|
447
447
|
|
|
448
|
-
Over HTTP/1.1 this is the Upgrade handshake. Over HTTP/2 and HTTP/3 it is Extended CONNECT when the server advertises `SETTINGS_ENABLE_CONNECT_PROTOCOL`, otherwise the client opens an HTTP/1.1 connection for it. `subprotocols=[...]` offers subprotocols; `websocket.subprotocol` is the one chosen. The `total` of `timeout` covers the handshake only; afterwards `read` and `write` still limit each wait, so `Timeout(read=None)` lets `receive()` wait through any silence.
|
|
448
|
+
Over HTTP/1.1 this is the Upgrade handshake. Over HTTP/2 and HTTP/3 it is Extended CONNECT when the server advertises `SETTINGS_ENABLE_CONNECT_PROTOCOL`, otherwise the client opens an HTTP/1.1 connection for it. Each WebSocket has a connection of its own, which `close()` closes after the closing handshake. `subprotocols=[...]` offers subprotocols; `websocket.subprotocol` is the one chosen. The `total` of `timeout` covers the handshake only; afterwards `read` and `write` still limit each wait, so `Timeout(read=None)` lets `receive()` wait through any silence.
|
|
449
449
|
|
|
450
450
|
## uvloop
|
|
451
451
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "boring-http"
|
|
3
|
-
version = "2.1
|
|
3
|
+
version = "2.2.1"
|
|
4
4
|
description = "Asyncio HTTP client on BoringSSL: HTTP/1.1, HTTP/2 and HTTP/3 over QUIC v1 and v2, WebSocket on all three, HTTP and SOCKS proxies, headers in your order"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.14"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "boring-http"
|
|
3
|
-
version = "2.1
|
|
3
|
+
version = "2.2.1"
|
|
4
4
|
description = "Asyncio HTTP client on BoringSSL: HTTP/1.1, HTTP/2 and HTTP/3 over QUIC v1 and v2, WebSocket on all three, HTTP and SOCKS proxies, headers in your order"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.14"
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import base64
|
|
2
|
-
from collections.abc import
|
|
2
|
+
from collections.abc import (
|
|
3
|
+
AsyncGenerator,
|
|
4
|
+
AsyncIterable,
|
|
5
|
+
Awaitable,
|
|
6
|
+
Callable,
|
|
7
|
+
Mapping,
|
|
8
|
+
Sequence,
|
|
9
|
+
)
|
|
3
10
|
from contextlib import asynccontextmanager
|
|
4
11
|
from dataclasses import replace
|
|
5
12
|
from time import monotonic
|
|
@@ -16,6 +23,7 @@ from ssl2 import (
|
|
|
16
23
|
ProxyError,
|
|
17
24
|
ProxyScheme,
|
|
18
25
|
SessionCache,
|
|
26
|
+
SigningChain,
|
|
19
27
|
SocketTransport,
|
|
20
28
|
TLSClientConfiguration,
|
|
21
29
|
TLSError,
|
|
@@ -107,10 +115,13 @@ class Client:
|
|
|
107
115
|
trust_store: TrustStore | None = None,
|
|
108
116
|
verify: bool = True,
|
|
109
117
|
revocation: Revocation = Revocation.SOFT,
|
|
118
|
+
certificate_chain: SigningChain | None = None,
|
|
119
|
+
check_peer: Callable[[tuple[bytes, ...], str], Awaitable[None]] | None = None,
|
|
110
120
|
cookies: CookieJar | None = None,
|
|
111
121
|
base_url: str | None = None,
|
|
112
122
|
max_connections: int = 100,
|
|
113
123
|
max_connections_per_host: int = 10,
|
|
124
|
+
max_idle_time: float | None = 90.0,
|
|
114
125
|
max_redirects: int = 20,
|
|
115
126
|
max_message_size: int = 16 * 1024 * 1024,
|
|
116
127
|
decode: bool = True,
|
|
@@ -138,13 +149,17 @@ class Client:
|
|
|
138
149
|
self._trust_store = trust_store
|
|
139
150
|
self._verify = verify
|
|
140
151
|
self._revocations = Revocations(revocation, self._fetch_crl)
|
|
152
|
+
self._certificate_chain = certificate_chain
|
|
153
|
+
self._check_peer = check_peer
|
|
141
154
|
self._base_url = base_url
|
|
142
155
|
self._max_redirects = max_redirects
|
|
143
156
|
self._max_message_size = max_message_size
|
|
144
157
|
self._decode = decode
|
|
145
158
|
self._contexts: dict[tuple[NextProtocol, ...] | None, ClientContext] = {}
|
|
146
159
|
self._memory: dict[tuple[str, int], OriginMemory] = {}
|
|
147
|
-
self._pool = Pool(
|
|
160
|
+
self._pool = Pool(
|
|
161
|
+
self._connect, max_connections, max_connections_per_host, max_idle_time
|
|
162
|
+
)
|
|
148
163
|
self.cookies = CookieJar() if cookies is None else cookies
|
|
149
164
|
|
|
150
165
|
async def __aenter__(self) -> Self:
|
|
@@ -442,12 +457,14 @@ class Client:
|
|
|
442
457
|
ALPN[version] for version in self._versions if version in ALPN
|
|
443
458
|
)
|
|
444
459
|
configuration = TLSClientConfiguration(
|
|
460
|
+
certificate_chain=self._certificate_chain,
|
|
445
461
|
trust_store=self._trust_store,
|
|
446
462
|
inner_protocols=offered,
|
|
447
463
|
verify_certificates=self._verify,
|
|
448
464
|
)
|
|
449
465
|
else:
|
|
450
466
|
configuration = TLSClientConfiguration(
|
|
467
|
+
certificate_chain=self._certificate_chain,
|
|
451
468
|
trust_store=self._trust_store,
|
|
452
469
|
client_hello=hello,
|
|
453
470
|
verify_certificates=self._verify,
|
|
@@ -487,11 +504,16 @@ class Client:
|
|
|
487
504
|
|
|
488
505
|
async def _checked(self, stream: TLSStream, authority: str) -> None:
|
|
489
506
|
try:
|
|
490
|
-
await self.
|
|
491
|
-
except
|
|
507
|
+
await self._verified(stream.buffer.getpeerchain(), authority)
|
|
508
|
+
except BaseException:
|
|
492
509
|
await stream.close()
|
|
493
510
|
raise
|
|
494
511
|
|
|
512
|
+
async def _verified(self, chain: tuple[bytes, ...], authority: str) -> None:
|
|
513
|
+
await self._revocations.check(chain, authority)
|
|
514
|
+
if self._check_peer is not None:
|
|
515
|
+
await self._check_peer(chain, authority)
|
|
516
|
+
|
|
495
517
|
async def _fetch_crl(self, url: str) -> bytes:
|
|
496
518
|
async with Client(
|
|
497
519
|
versions=(HTTPVersion.HTTP_1,), revocation=Revocation.NONE, timeout=10
|
|
@@ -519,10 +541,8 @@ class Client:
|
|
|
519
541
|
memory=memory,
|
|
520
542
|
)
|
|
521
543
|
try:
|
|
522
|
-
await self.
|
|
523
|
-
|
|
524
|
-
)
|
|
525
|
-
except RevocationError:
|
|
544
|
+
await self._verified(client.connection.peer_chain, url.authority)
|
|
545
|
+
except BaseException:
|
|
526
546
|
client.close()
|
|
527
547
|
raise
|
|
528
548
|
connection = Http3Connection(client, self._http3, memory)
|
|
@@ -7,14 +7,6 @@ from typing import Self
|
|
|
7
7
|
from boring_http.public_suffixes import EXCEPTIONS, RULES
|
|
8
8
|
|
|
9
9
|
|
|
10
|
-
def is_public_suffix(domain: str) -> bool:
|
|
11
|
-
# publicsuffix.org's algorithm: an exception rule wins, then a listed rule or a wildcard over the parent, and an unlisted top-level label is one
|
|
12
|
-
if domain in EXCEPTIONS:
|
|
13
|
-
return False
|
|
14
|
-
parent = domain.partition(".")[2]
|
|
15
|
-
return domain in RULES or f"*.{parent}" in RULES or not parent
|
|
16
|
-
|
|
17
|
-
|
|
18
10
|
@dataclass(frozen=True, slots=True)
|
|
19
11
|
class Cookie:
|
|
20
12
|
name: str
|
|
@@ -57,7 +49,7 @@ class Cookie:
|
|
|
57
49
|
if domain and not (host == domain or host.endswith("." + domain)):
|
|
58
50
|
return None
|
|
59
51
|
# RFC 6265 section 5.3 step 5: a public suffix as Domain is refused, unless it is the host itself, which keeps the cookie to that host
|
|
60
|
-
if domain and
|
|
52
|
+
if domain and cls._public_suffix(domain):
|
|
61
53
|
if domain != host:
|
|
62
54
|
return None
|
|
63
55
|
domain = ""
|
|
@@ -92,6 +84,14 @@ class Cookie:
|
|
|
92
84
|
domain_ok and path_ok and (secure or not self.secure) and not self.expired
|
|
93
85
|
)
|
|
94
86
|
|
|
87
|
+
@staticmethod
|
|
88
|
+
def _public_suffix(domain: str) -> bool:
|
|
89
|
+
# publicsuffix.org's algorithm: an exception rule wins, then a listed rule or a wildcard over the parent, and an unlisted top-level label is one
|
|
90
|
+
if domain in EXCEPTIONS:
|
|
91
|
+
return False
|
|
92
|
+
parent = domain.partition(".")[2]
|
|
93
|
+
return domain in RULES or f"*.{parent}" in RULES or not parent
|
|
94
|
+
|
|
95
95
|
@staticmethod
|
|
96
96
|
def _date(text: str) -> float | None:
|
|
97
97
|
try:
|
|
@@ -687,4 +687,8 @@ class Http2StreamTransport:
|
|
|
687
687
|
async def close(self) -> None:
|
|
688
688
|
if not self._closed:
|
|
689
689
|
self._closed = True
|
|
690
|
-
|
|
690
|
+
# Client.websocket opens a connection for the tunnel alone, so the connection ends with it
|
|
691
|
+
try:
|
|
692
|
+
await self._connection.write_data(self._stream, b"", end=True)
|
|
693
|
+
finally:
|
|
694
|
+
await self._connection.close()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import secrets
|
|
3
|
-
from collections.abc import AsyncGenerator, Callable
|
|
3
|
+
from collections.abc import AsyncGenerator, Awaitable, Callable
|
|
4
4
|
from dataclasses import dataclass
|
|
5
5
|
from enum import IntEnum
|
|
6
6
|
|
|
@@ -311,7 +311,7 @@ class Http3Connection:
|
|
|
311
311
|
return (
|
|
312
312
|
status,
|
|
313
313
|
headers,
|
|
314
|
-
Http3StreamTransport(self._quic, stream_id, deadline, frames),
|
|
314
|
+
Http3StreamTransport(self._quic, stream_id, deadline, frames, self.close),
|
|
315
315
|
)
|
|
316
316
|
|
|
317
317
|
async def close(self) -> None:
|
|
@@ -531,12 +531,18 @@ class Http3Connection:
|
|
|
531
531
|
|
|
532
532
|
class Http3StreamTransport:
|
|
533
533
|
def __init__(
|
|
534
|
-
self,
|
|
534
|
+
self,
|
|
535
|
+
quic: QuicClient,
|
|
536
|
+
stream_id: int,
|
|
537
|
+
deadline: Deadline,
|
|
538
|
+
frames: StreamFrames,
|
|
539
|
+
close_connection: Callable[[], Awaitable[None]],
|
|
535
540
|
) -> None:
|
|
536
541
|
self._quic = quic
|
|
537
542
|
self._stream_id = stream_id
|
|
538
543
|
self._frames = frames
|
|
539
544
|
self._deadline = deadline
|
|
545
|
+
self._close_connection = close_connection
|
|
540
546
|
self._pending = b""
|
|
541
547
|
self._closed = False
|
|
542
548
|
|
|
@@ -564,3 +570,5 @@ class Http3StreamTransport:
|
|
|
564
570
|
if not self._closed:
|
|
565
571
|
self._closed = True
|
|
566
572
|
self._quic.send(self._stream_id, b"", end=True)
|
|
573
|
+
# Client.websocket opens a connection for the tunnel alone, so the connection ends with it
|
|
574
|
+
await self._close_connection()
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
from collections.abc import Awaitable, Callable
|
|
3
|
+
from time import monotonic
|
|
3
4
|
from typing import Protocol
|
|
4
5
|
|
|
5
6
|
from ssl2 import Proxy
|
|
@@ -35,15 +36,23 @@ type Connector = Callable[[URL, Proxy | None, bool], Awaitable[Connection]]
|
|
|
35
36
|
|
|
36
37
|
class Pool:
|
|
37
38
|
def __init__(
|
|
38
|
-
self,
|
|
39
|
+
self,
|
|
40
|
+
connector: Connector,
|
|
41
|
+
max_connections: int,
|
|
42
|
+
max_connections_per_host: int,
|
|
43
|
+
max_idle_time: float | None,
|
|
39
44
|
) -> None:
|
|
40
45
|
self._connector = connector
|
|
41
46
|
self._per_host = max_connections_per_host
|
|
42
47
|
self._total = asyncio.Semaphore(max_connections)
|
|
48
|
+
self._max_idle_time = max_idle_time
|
|
43
49
|
self._limits: dict[Key, asyncio.Semaphore] = {}
|
|
44
50
|
self._locks: dict[Key, asyncio.Lock] = {}
|
|
45
51
|
self._changes: dict[Key, asyncio.Event] = {}
|
|
46
52
|
self._connections: dict[Key, list[Connection]] = {}
|
|
53
|
+
# the requests each connection is serving, and when it last became idle
|
|
54
|
+
self._in_use: dict[Connection, int] = {}
|
|
55
|
+
self._idle_since: dict[Connection, float] = {}
|
|
47
56
|
|
|
48
57
|
async def acquire(
|
|
49
58
|
self, url: URL, proxy: Proxy | None, deadline: Deadline, *, quic: bool = False
|
|
@@ -53,10 +62,13 @@ class Pool:
|
|
|
53
62
|
changed = self._changes.setdefault(key, asyncio.Event())
|
|
54
63
|
async with self._locks.setdefault(key, asyncio.Lock()):
|
|
55
64
|
while True:
|
|
56
|
-
for connection in self._connections.get(key, []):
|
|
57
|
-
if connection
|
|
65
|
+
for connection in list(self._connections.get(key, [])):
|
|
66
|
+
if self._expired(connection):
|
|
67
|
+
await connection.close()
|
|
68
|
+
elif connection.available:
|
|
58
69
|
connection.take()
|
|
59
70
|
connection.reused = True
|
|
71
|
+
self._in_use[connection] += 1
|
|
60
72
|
return connection
|
|
61
73
|
if not limit.locked() and not self._total.locked():
|
|
62
74
|
break
|
|
@@ -77,6 +89,7 @@ class Pool:
|
|
|
77
89
|
connection.take()
|
|
78
90
|
connection.released = lambda: self._changed(key, connection, limit)
|
|
79
91
|
self._connections.setdefault(key, []).append(connection)
|
|
92
|
+
self._in_use[connection] = 1
|
|
80
93
|
return connection
|
|
81
94
|
|
|
82
95
|
async def close(self) -> None:
|
|
@@ -91,6 +104,21 @@ class Pool:
|
|
|
91
104
|
connections = self._connections.get(key, [])
|
|
92
105
|
if connection.closed and connection in connections:
|
|
93
106
|
connections.remove(connection)
|
|
107
|
+
del self._in_use[connection]
|
|
108
|
+
self._idle_since.pop(connection, None)
|
|
94
109
|
limit.release()
|
|
95
110
|
self._total.release()
|
|
111
|
+
elif connection in self._in_use:
|
|
112
|
+
self._in_use[connection] = max(self._in_use[connection] - 1, 0)
|
|
113
|
+
if not self._in_use[connection]:
|
|
114
|
+
self._idle_since[connection] = monotonic()
|
|
96
115
|
self._changes[key].set()
|
|
116
|
+
|
|
117
|
+
def _expired(self, connection: Connection) -> bool:
|
|
118
|
+
idle_since = self._idle_since.get(connection)
|
|
119
|
+
return (
|
|
120
|
+
self._max_idle_time is not None
|
|
121
|
+
and idle_since is not None
|
|
122
|
+
and not self._in_use[connection]
|
|
123
|
+
and monotonic() - idle_since > self._max_idle_time
|
|
124
|
+
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|