boring-http 2.0.1__tar.gz → 2.0.3__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 (29) hide show
  1. {boring_http-2.0.1 → boring_http-2.0.3}/PKG-INFO +4 -4
  2. {boring_http-2.0.1 → boring_http-2.0.3}/README.md +3 -3
  3. {boring_http-2.0.1 → boring_http-2.0.3}/pyproject.toml +1 -1
  4. {boring_http-2.0.1 → boring_http-2.0.3}/pyproject.toml.orig +1 -1
  5. {boring_http-2.0.1 → boring_http-2.0.3}/src/boring_http/cookies.py +15 -0
  6. {boring_http-2.0.1 → boring_http-2.0.3}/src/boring_http/pool.py +2 -1
  7. boring_http-2.0.3/src/boring_http/public_suffixes.py +10407 -0
  8. {boring_http-2.0.1 → boring_http-2.0.3}/src/boring_http/timing.py +3 -0
  9. {boring_http-2.0.1 → boring_http-2.0.3}/src/boring_http/websocket.py +2 -0
  10. {boring_http-2.0.1 → boring_http-2.0.3}/src/boring_http/__init__.py +0 -0
  11. {boring_http-2.0.1 → boring_http-2.0.3}/src/boring_http/client.py +0 -0
  12. {boring_http-2.0.1 → boring_http-2.0.3}/src/boring_http/codings.py +0 -0
  13. {boring_http-2.0.1 → boring_http-2.0.3}/src/boring_http/errors.py +0 -0
  14. {boring_http-2.0.1 → boring_http-2.0.3}/src/boring_http/hpack.py +0 -0
  15. {boring_http-2.0.1 → boring_http-2.0.3}/src/boring_http/hpack_tables.py +0 -0
  16. {boring_http-2.0.1 → boring_http-2.0.3}/src/boring_http/http1.py +0 -0
  17. {boring_http-2.0.1 → boring_http-2.0.3}/src/boring_http/http2.py +0 -0
  18. {boring_http-2.0.1 → boring_http-2.0.3}/src/boring_http/http3.py +0 -0
  19. {boring_http-2.0.1 → boring_http-2.0.3}/src/boring_http/message.py +0 -0
  20. {boring_http-2.0.1 → boring_http-2.0.3}/src/boring_http/py.typed +0 -0
  21. {boring_http-2.0.1 → boring_http-2.0.3}/src/boring_http/qpack.py +0 -0
  22. {boring_http-2.0.1 → boring_http-2.0.3}/src/boring_http/qpack_tables.py +0 -0
  23. {boring_http-2.0.1 → boring_http-2.0.3}/src/boring_http/quic/__init__.py +0 -0
  24. {boring_http-2.0.1 → boring_http-2.0.3}/src/boring_http/quic/connection.py +0 -0
  25. {boring_http-2.0.1 → boring_http-2.0.3}/src/boring_http/quic/packet.py +0 -0
  26. {boring_http-2.0.1 → boring_http-2.0.3}/src/boring_http/quic/protocol.py +0 -0
  27. {boring_http-2.0.1 → boring_http-2.0.3}/src/boring_http/quic/recovery.py +0 -0
  28. {boring_http-2.0.1 → boring_http-2.0.3}/src/boring_http/quic/streams.py +0 -0
  29. {boring_http-2.0.1 → boring_http-2.0.3}/src/boring_http/revocation.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: boring-http
3
- Version: 2.0.1
3
+ Version: 2.0.3
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
@@ -70,7 +70,7 @@ async def main() -> None:
70
70
  asyncio.run(main())
71
71
  ```
72
72
 
73
- `response.cookies` holds only the cookies this response set; `client.cookies` is the jar that follows every request. `location`, `content_type`, `charset`, `content_length`, `content_encoding`, `retry_after`, `etag`, `last_modified` and `date` are parsed, `response.headers` keeps the raw fields in order, `raise_for_status()` raises `HTTPStatusError`.
73
+ `response.cookies` holds only the cookies this response set; `client.cookies` is the jar that follows every request. A `Domain` that is a public suffix on the bundled Public Suffix List, such as `co.uk` or `github.io`, is refused unless it is the host itself, which then keeps the cookie to that host. `location`, `content_type`, `charset`, `content_length`, `content_encoding`, `retry_after`, `etag`, `last_modified` and `date` are parsed, `response.headers` keeps the raw fields in order, `raise_for_status()` raises `HTTPStatusError`.
74
74
 
75
75
  ## Streaming and timeouts
76
76
 
@@ -98,7 +98,7 @@ async def main() -> None:
98
98
  asyncio.run(main())
99
99
  ```
100
100
 
101
- A single number sets every phase and the total, so the request gives up after that many seconds whatever it was doing. With a `Timeout` the phases are separate: `read` is the longest wait for the next bytes, `total` caps the whole exchange including redirects, and the error names whichever ran out.
101
+ A single number sets every phase and the total, so the request gives up after that many seconds whatever it was doing. With a `Timeout` the phases are separate: `connect` covers opening a connection, the proxy, TCP and the TLS or QUIC handshake included, `read` is the longest wait for the next bytes, `total` caps the whole exchange including redirects, and the error names whichever ran out.
102
102
 
103
103
  ## HTTP/3
104
104
 
@@ -453,7 +453,7 @@ async def main() -> None:
453
453
  asyncio.run(main())
454
454
  ```
455
455
 
456
- 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.
456
+ 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.
457
457
 
458
458
  ## uvloop
459
459
 
@@ -60,7 +60,7 @@ async def main() -> None:
60
60
  asyncio.run(main())
61
61
  ```
62
62
 
63
- `response.cookies` holds only the cookies this response set; `client.cookies` is the jar that follows every request. `location`, `content_type`, `charset`, `content_length`, `content_encoding`, `retry_after`, `etag`, `last_modified` and `date` are parsed, `response.headers` keeps the raw fields in order, `raise_for_status()` raises `HTTPStatusError`.
63
+ `response.cookies` holds only the cookies this response set; `client.cookies` is the jar that follows every request. A `Domain` that is a public suffix on the bundled Public Suffix List, such as `co.uk` or `github.io`, is refused unless it is the host itself, which then keeps the cookie to that host. `location`, `content_type`, `charset`, `content_length`, `content_encoding`, `retry_after`, `etag`, `last_modified` and `date` are parsed, `response.headers` keeps the raw fields in order, `raise_for_status()` raises `HTTPStatusError`.
64
64
 
65
65
  ## Streaming and timeouts
66
66
 
@@ -88,7 +88,7 @@ async def main() -> None:
88
88
  asyncio.run(main())
89
89
  ```
90
90
 
91
- A single number sets every phase and the total, so the request gives up after that many seconds whatever it was doing. With a `Timeout` the phases are separate: `read` is the longest wait for the next bytes, `total` caps the whole exchange including redirects, and the error names whichever ran out.
91
+ A single number sets every phase and the total, so the request gives up after that many seconds whatever it was doing. With a `Timeout` the phases are separate: `connect` covers opening a connection, the proxy, TCP and the TLS or QUIC handshake included, `read` is the longest wait for the next bytes, `total` caps the whole exchange including redirects, and the error names whichever ran out.
92
92
 
93
93
  ## HTTP/3
94
94
 
@@ -443,7 +443,7 @@ async def main() -> None:
443
443
  asyncio.run(main())
444
444
  ```
445
445
 
446
- 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.
446
+ 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.
447
447
 
448
448
  ## uvloop
449
449
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "boring-http"
3
- version = "2.0.1"
3
+ version = "2.0.3"
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.0.1"
3
+ version = "2.0.3"
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"
@@ -4,6 +4,16 @@ from dataclasses import dataclass
4
4
  from email.utils import parsedate_to_datetime
5
5
  from typing import Self
6
6
 
7
+ from boring_http.public_suffixes import EXCEPTIONS, RULES
8
+
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
+
7
17
 
8
18
  @dataclass(frozen=True, slots=True)
9
19
  class Cookie:
@@ -46,6 +56,11 @@ class Cookie:
46
56
  pass
47
57
  if domain and not (host == domain or host.endswith("." + domain)):
48
58
  return None
59
+ # 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 is_public_suffix(domain):
61
+ if domain != host:
62
+ return None
63
+ domain = ""
49
64
  if not path.startswith("/"):
50
65
  path = request_path[: request_path.rfind("/")] or "/"
51
66
  return cls(
@@ -68,7 +68,8 @@ class Pool:
68
68
  await limit.acquire()
69
69
  await self._total.acquire()
70
70
  try:
71
- connection = await self._connector(url, proxy, quic)
71
+ async with deadline.phase("connect"):
72
+ connection = await self._connector(url, proxy, quic)
72
73
  except BaseException:
73
74
  limit.release()
74
75
  self._total.release()