lkrequest 0.2.3__tar.gz → 0.2.4__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 (134) hide show
  1. {lkrequest-0.2.3 → lkrequest-0.2.4}/.github/workflows/ci.yml +3 -1
  2. {lkrequest-0.2.3 → lkrequest-0.2.4}/CHANGELOG.md +79 -0
  3. {lkrequest-0.2.3 → lkrequest-0.2.4}/CONTRIBUTING.md +1 -1
  4. {lkrequest-0.2.3 → lkrequest-0.2.4}/Cargo.lock +137 -187
  5. {lkrequest-0.2.3 → lkrequest-0.2.4}/Cargo.toml +10 -10
  6. {lkrequest-0.2.3 → lkrequest-0.2.4}/PKG-INFO +207 -31
  7. {lkrequest-0.2.3 → lkrequest-0.2.4}/README-zh.md +202 -27
  8. {lkrequest-0.2.3 → lkrequest-0.2.4}/README.md +203 -27
  9. {lkrequest-0.2.3 → lkrequest-0.2.4}/examples/accept_encoding.py +10 -6
  10. {lkrequest-0.2.3 → lkrequest-0.2.4}/examples/authentication.py +2 -2
  11. {lkrequest-0.2.3 → lkrequest-0.2.4}/examples/basic_requests.py +3 -1
  12. {lkrequest-0.2.3 → lkrequest-0.2.4}/examples/browser_fingerprints.py +5 -1
  13. {lkrequest-0.2.3 → lkrequest-0.2.4}/examples/certificate_config.py +6 -6
  14. {lkrequest-0.2.3 → lkrequest-0.2.4}/examples/client_pool.py +5 -6
  15. {lkrequest-0.2.3 → lkrequest-0.2.4}/examples/connection_prewarming.py +30 -22
  16. {lkrequest-0.2.3 → lkrequest-0.2.4}/examples/cookie_management.py +7 -7
  17. {lkrequest-0.2.3 → lkrequest-0.2.4}/examples/custom_fingerprint.py +37 -13
  18. {lkrequest-0.2.3 → lkrequest-0.2.4}/examples/error_handling.py +31 -13
  19. {lkrequest-0.2.3 → lkrequest-0.2.4}/examples/event_hooks.py +4 -4
  20. {lkrequest-0.2.3 → lkrequest-0.2.4}/examples/fingerprint_validation.py +3 -3
  21. lkrequest-0.2.4/examples/http1_header_case.py +157 -0
  22. lkrequest-0.2.4/examples/logging_config.py +40 -0
  23. {lkrequest-0.2.3 → lkrequest-0.2.4}/examples/metrics.py +4 -4
  24. {lkrequest-0.2.3 → lkrequest-0.2.4}/examples/middleware.py +11 -7
  25. {lkrequest-0.2.3 → lkrequest-0.2.4}/examples/multipart_upload.py +11 -3
  26. {lkrequest-0.2.3 → lkrequest-0.2.4}/examples/pool_stats.py +11 -7
  27. {lkrequest-0.2.3 → lkrequest-0.2.4}/examples/proxy_pool.py +36 -7
  28. {lkrequest-0.2.3 → lkrequest-0.2.4}/examples/redirect_history.py +5 -5
  29. {lkrequest-0.2.3 → lkrequest-0.2.4}/examples/response_inspection.py +6 -4
  30. {lkrequest-0.2.3 → lkrequest-0.2.4}/examples/retry_strategies.py +38 -18
  31. {lkrequest-0.2.3 → lkrequest-0.2.4}/examples/session_pool.py +5 -4
  32. {lkrequest-0.2.3 → lkrequest-0.2.4}/examples/streaming_response.py +4 -4
  33. {lkrequest-0.2.3 → lkrequest-0.2.4}/examples/timeout_config.py +10 -31
  34. {lkrequest-0.2.3 → lkrequest-0.2.4}/examples/websocket.py +8 -2
  35. {lkrequest-0.2.3 → lkrequest-0.2.4}/pyproject.toml +4 -4
  36. {lkrequest-0.2.3 → lkrequest-0.2.4}/python/lkrequest/__init__.py +22 -4
  37. {lkrequest-0.2.3 → lkrequest-0.2.4}/python/lkrequest/_lkrequest.pyi +1016 -102
  38. {lkrequest-0.2.3 → lkrequest-0.2.4}/python/lkrequest/blocking.py +14 -4
  39. {lkrequest-0.2.3 → lkrequest-0.2.4}/src/bridge.rs +15 -10
  40. lkrequest-0.2.4/src/build_info.rs +76 -0
  41. {lkrequest-0.2.3 → lkrequest-0.2.4}/src/client.rs +740 -319
  42. {lkrequest-0.2.3 → lkrequest-0.2.4}/src/client_pool.rs +5 -4
  43. {lkrequest-0.2.3 → lkrequest-0.2.4}/src/dns.rs +15 -3
  44. lkrequest-0.2.4/src/error.rs +425 -0
  45. {lkrequest-0.2.3 → lkrequest-0.2.4}/src/fingerprint.rs +322 -29
  46. lkrequest-0.2.4/src/hsts.rs +180 -0
  47. {lkrequest-0.2.3 → lkrequest-0.2.4}/src/lib.rs +41 -9
  48. lkrequest-0.2.4/src/logging.rs +147 -0
  49. {lkrequest-0.2.3 → lkrequest-0.2.4}/src/masque.rs +21 -2
  50. {lkrequest-0.2.3 → lkrequest-0.2.4}/src/metrics.rs +0 -10
  51. {lkrequest-0.2.3 → lkrequest-0.2.4}/src/middleware.rs +16 -16
  52. lkrequest-0.2.4/src/multipart.rs +133 -0
  53. {lkrequest-0.2.3 → lkrequest-0.2.4}/src/network_partition.rs +3 -3
  54. {lkrequest-0.2.3 → lkrequest-0.2.4}/src/priority.rs +1 -1
  55. lkrequest-0.2.4/src/protocol.rs +664 -0
  56. lkrequest-0.2.4/src/proxy.rs +622 -0
  57. lkrequest-0.2.4/src/proxy_source.rs +122 -0
  58. {lkrequest-0.2.3 → lkrequest-0.2.4}/src/quic.rs +22 -1
  59. {lkrequest-0.2.3 → lkrequest-0.2.4}/src/randomize.rs +3 -3
  60. lkrequest-0.2.4/src/resolver.rs +185 -0
  61. {lkrequest-0.2.3 → lkrequest-0.2.4}/src/response.rs +37 -23
  62. {lkrequest-0.2.3 → lkrequest-0.2.4}/src/retry.rs +54 -12
  63. {lkrequest-0.2.3 → lkrequest-0.2.4}/src/session.rs +185 -127
  64. {lkrequest-0.2.3 → lkrequest-0.2.4}/src/session_pool.rs +78 -60
  65. {lkrequest-0.2.3 → lkrequest-0.2.4}/src/session_state.rs +14 -9
  66. lkrequest-0.2.4/src/trust_anchor.rs +108 -0
  67. {lkrequest-0.2.3 → lkrequest-0.2.4}/src/types.rs +35 -112
  68. {lkrequest-0.2.3 → lkrequest-0.2.4}/src/upload.rs +6 -6
  69. {lkrequest-0.2.3 → lkrequest-0.2.4}/src/websocket.rs +86 -5
  70. {lkrequest-0.2.3 → lkrequest-0.2.4}/tests/_raw_http.py +25 -8
  71. {lkrequest-0.2.3 → lkrequest-0.2.4}/tests/conftest.py +11 -9
  72. {lkrequest-0.2.3 → lkrequest-0.2.4}/tests/test_advanced.py +72 -5
  73. lkrequest-0.2.4/tests/test_broken_quic.py +181 -0
  74. lkrequest-0.2.4/tests/test_build_info.py +65 -0
  75. lkrequest-0.2.4/tests/test_check_proxy.py +80 -0
  76. {lkrequest-0.2.3 → lkrequest-0.2.4}/tests/test_client.py +160 -17
  77. lkrequest-0.2.4/tests/test_client_requests.py +79 -0
  78. lkrequest-0.2.4/tests/test_client_settings.py +135 -0
  79. lkrequest-0.2.4/tests/test_dns.py +241 -0
  80. lkrequest-0.2.4/tests/test_durations.py +54 -0
  81. lkrequest-0.2.4/tests/test_error_details.py +239 -0
  82. {lkrequest-0.2.3 → lkrequest-0.2.4}/tests/test_followup_features.py +79 -9
  83. lkrequest-0.2.4/tests/test_h2_profile_validate.py +89 -0
  84. {lkrequest-0.2.3 → lkrequest-0.2.4}/tests/test_h2_wire.py +17 -0
  85. {lkrequest-0.2.3 → lkrequest-0.2.4}/tests/test_h3_retry.py +22 -0
  86. lkrequest-0.2.4/tests/test_hsts.py +126 -0
  87. lkrequest-0.2.4/tests/test_http1_header_case.py +169 -0
  88. lkrequest-0.2.4/tests/test_http1_host_case.py +169 -0
  89. lkrequest-0.2.4/tests/test_keylog.py +84 -0
  90. {lkrequest-0.2.3 → lkrequest-0.2.4}/tests/test_local_server.py +11 -3
  91. lkrequest-0.2.4/tests/test_logging.py +94 -0
  92. {lkrequest-0.2.3 → lkrequest-0.2.4}/tests/test_masque.py +24 -0
  93. lkrequest-0.2.4/tests/test_protocol_policy.py +182 -0
  94. lkrequest-0.2.4/tests/test_proxy_custom_auth.py +152 -0
  95. lkrequest-0.2.4/tests/test_proxy_pools.py +338 -0
  96. lkrequest-0.2.4/tests/test_randomize_fingerprint.py +115 -0
  97. lkrequest-0.2.4/tests/test_request_options.py +71 -0
  98. lkrequest-0.2.4/tests/test_request_order.py +262 -0
  99. lkrequest-0.2.4/tests/test_tcp_fingerprint.py +143 -0
  100. lkrequest-0.2.4/tests/test_tls_profile_ech.py +93 -0
  101. lkrequest-0.2.4/tests/test_trust_anchor.py +248 -0
  102. lkrequest-0.2.4/tests/test_websocket.py +122 -0
  103. lkrequest-0.2.3/examples/logging_config.py +0 -58
  104. lkrequest-0.2.3/src/error.rs +0 -162
  105. lkrequest-0.2.3/src/hsts.rs +0 -121
  106. lkrequest-0.2.3/src/multipart.rs +0 -70
  107. lkrequest-0.2.3/src/protocol.rs +0 -232
  108. lkrequest-0.2.3/src/proxy.rs +0 -323
  109. {lkrequest-0.2.3 → lkrequest-0.2.4}/.editorconfig +0 -0
  110. {lkrequest-0.2.3 → lkrequest-0.2.4}/.gitignore +0 -0
  111. {lkrequest-0.2.3 → lkrequest-0.2.4}/LICENSE +0 -0
  112. {lkrequest-0.2.3 → lkrequest-0.2.4}/NOTICE +0 -0
  113. {lkrequest-0.2.3 → lkrequest-0.2.4}/SECURITY.md +0 -0
  114. {lkrequest-0.2.3 → lkrequest-0.2.4}/benchmarks/README.md +0 -0
  115. {lkrequest-0.2.3 → lkrequest-0.2.4}/benchmarks/bench_latency.py +0 -0
  116. {lkrequest-0.2.3 → lkrequest-0.2.4}/benchmarks/bench_memory.py +0 -0
  117. {lkrequest-0.2.3 → lkrequest-0.2.4}/benchmarks/bench_throughput.py +0 -0
  118. {lkrequest-0.2.3 → lkrequest-0.2.4}/benchmarks/requirements.txt +0 -0
  119. {lkrequest-0.2.3 → lkrequest-0.2.4}/build.rs +0 -0
  120. {lkrequest-0.2.3 → lkrequest-0.2.4}/examples/http_methods.py +0 -0
  121. {lkrequest-0.2.3 → lkrequest-0.2.4}/examples/ip_family.py +0 -0
  122. {lkrequest-0.2.3 → lkrequest-0.2.4}/examples/streaming_upload.py +0 -0
  123. {lkrequest-0.2.3 → lkrequest-0.2.4}/justfile +0 -0
  124. {lkrequest-0.2.3 → lkrequest-0.2.4}/python/lkrequest/pcap.py +0 -0
  125. {lkrequest-0.2.3 → lkrequest-0.2.4}/rustfmt.toml +0 -0
  126. {lkrequest-0.2.3 → lkrequest-0.2.4}/tests/_masque_proxy.py +0 -0
  127. {lkrequest-0.2.3 → lkrequest-0.2.4}/tests/test_compression.py +0 -0
  128. {lkrequest-0.2.3 → lkrequest-0.2.4}/tests/test_cookie_max_age.py +0 -0
  129. {lkrequest-0.2.3 → lkrequest-0.2.4}/tests/test_package_metadata.py +0 -0
  130. {lkrequest-0.2.3 → lkrequest-0.2.4}/tests/test_proxy_connect.py +0 -0
  131. {lkrequest-0.2.3 → lkrequest-0.2.4}/tests/test_redirect_safety.py +0 -0
  132. {lkrequest-0.2.3 → lkrequest-0.2.4}/tests/test_session_lifecycle.py +0 -0
  133. {lkrequest-0.2.3 → lkrequest-0.2.4}/tests/test_stub_parity.py +0 -0
  134. {lkrequest-0.2.3 → lkrequest-0.2.4}/tests/test_websocket_concurrency.py +0 -0
@@ -113,7 +113,9 @@ jobs:
113
113
  fail-fast: false
114
114
  matrix:
115
115
  include:
116
- - os: ubuntu-latest
116
+ # Pinned: actions/python-versions has no Python 3.9 build for
117
+ # Ubuntu 26.04, which ubuntu-latest now is.
118
+ - os: ubuntu-24.04
117
119
  wheel: wheel-x86_64-unknown-linux-gnu
118
120
  python: "3.9"
119
121
  - os: ubuntu-latest
@@ -2,6 +2,85 @@
2
2
 
3
3
  All important changes to this project will be documented in this file.
4
4
 
5
+ ## lkrequest-py 0.2.4
6
+
7
+ Adds the Chrome 156 and Chrome 157 fingerprints, Trust Anchor Identifiers and a
8
+ switch for sending HTTP/1.1 header names in lowercase, and much of what the Rust
9
+ library offers and the bindings did not:
10
+ every exception now says what failed, pools take their proxies from a function,
11
+ names can be looked up by a DNS server of your own or by Python code, log events
12
+ can go to Python's `logging`, and protocol policies, QUIC cooldowns, HSTS
13
+ policies, proxy credentials, WebSocket connections and TCP and TLS fingerprints
14
+ can be set in full, among the rest listed below. It also moves the bindings to
15
+ PyO3 0.30 and refreshes the Rust dependencies, and fixes the proxy pool, which
16
+ handed out a key rather than a proxy. Default request headers and request
17
+ cookies now go out in the order they were given, as the Rust library sends
18
+ them; a copy made by `randomize_fingerprint()` keeps the options of the client
19
+ it copies; and the default fingerprint, Chrome 155, sends its trust-anchor IDs
20
+ in Chrome's sorted order instead of shuffling them. Apart from those three, and
21
+ unless a new option is used or one `Hsts.dynamic()` is given to several
22
+ sessions, requests go out on the wire exactly as before. Six changes are visible
23
+ from Python and worth reading before upgrading: `ProxyPool.acquire()` now
24
+ returns a `ProxyLease` instead of a string; `TimeoutConfig` and
25
+ `ResourceLimits`, which nothing accepted, are gone; the exception raised for an
26
+ argument of the wrong type no longer names the argument in its message;
27
+ `Client.session(proxy=...)` now raises `ValueError` for a proxy URL it cannot
28
+ parse, where it used to build a session that connected direct; a
29
+ `ProtocolPolicy` whose axes contradict one another raises `ValueError` when it
30
+ is given to a client or a session, rather than on the first request; and the
31
+ sessions given one `Hsts.dynamic()` now share what it learns.
32
+
33
+ ### 🚀 Features
34
+ - **Chrome 157 Fingerprint** — Added `Client.chrome_157()` / `BlockingClient.chrome_157()`, the `"chrome_157"` name for `tls_profile=` / `h2_profile=` / `quic_profile=`, and `TlsProfile.chrome_157()`, `TlsProfile.chrome_157_quic()`, `H2Profile.chrome_157()`, `QuicProfile.chrome_157()`. Captured from Chrome 157.0.8103.39 on Windows, it matches Chrome 156 throughout: the TCP and QUIC ClientHellos differ only in their name, and the H2, QUIC and HTTP/3 profiles are Chrome 156's. The capture confirmed the HTTP/3 SETTINGS and the navigation and favicon priority; timing, loss recovery and resumption carry over from Chrome 156 without being observed again. The default fingerprint stays Chrome 155.
35
+ - **Trust Anchor Identifiers** — `Client` and `BlockingClient` take `trust_anchor_advertisement=`, `trust_anchor_recovery=` and `trust_anchor_bindings=`, for the Rust library's new support of draft-ietf-tls-trust-anchor-ids: the IDs of the roots a client accepts, which the Chrome 152 to 157 profiles send in extension `0xca34`. `trust_anchor_advertisement="trusted"` sends the sorted IDs of the roots the client trusts instead of the profile's list. `trust_anchor_recovery="once"` opens one more connection when a server lists the IDs it can serve and then its certificate chain fails to verify, or it ends the handshake, asking for the first of those IDs the client trusts; at most once per request, with 0-RTT off, and a second failure raises the error of its cause with a message beginning `trust anchor recovery failed`. `trust_anchor_bindings={id: anchor_identity}` gives IDs to roots of your own, and the new `lkrequest.trust_anchor_identity(der)` computes the identity a binding names a root by; the bundled IDs of the Chrome 157 root store need none. The IDs never change which certificates are trusted. A setting the Rust library rejects raises `ValueError`: recovery without `verify`, or with `proxy_fallback_direct=True`, `"trusted"` or `"once"` on a profile without the extension or with TLS synthesis, and a binding to a root the client does not load. `Response.diagnostics` and the `diagnostics` of an exception gained `trust_anchor`, the negotiation of the TLS connection the request opened: the IDs requested and offered, the root that verified the chain and, after a recovery, the first attempt; it is `None` on a pooled connection. Whatever the settings, a TLS failure on a server that listed IDs of its own is no longer retried.
36
+ - **Chrome 156 Fingerprint** — Added `Client.chrome_156()` / `BlockingClient.chrome_156()`, the `"chrome_156"` name for `tls_profile=` / `h2_profile=` / `quic_profile=`, and `TlsProfile.chrome_156()`, `TlsProfile.chrome_156_quic()`, `H2Profile.chrome_156()`, `QuicProfile.chrome_156()`. Captured from Chrome 156.0.8078.48 on Windows, it matches Chrome 155 throughout: the TCP and QUIC ClientHellos differ only in their name, and the H2, QUIC and HTTP/3 profiles are Chrome 155's. The capture confirmed the HTTP/3 SETTINGS and the navigation and favicon priority updates; timing, loss recovery and resumption carry over from Chrome 155 without being observed again. The default fingerprint stays Chrome 155.
37
+ - **Lowercase HTTP/1.1 Header Names** — Added `Client(http1_title_case_headers=...)` on both clients, and a read-only `http1_title_case_headers` property. HTTP/1.1 header names go out in Title-Case (`Content-Type`) by default, as before; `False` sends them in lowercase (`content-type`), including generated headers such as `content-length`, except for `Host`, which keeps its capital `H`. It applies to ordinary and streamed requests, preconnected connections and WebSocket handshakes, and leaves header values and order alone. Either way, the spelling a header was given in is not kept, and HTTP/2 and HTTP/3 always send lowercase names. Lowercase names alone do not make a request match what any particular browser sends over HTTP/1.1.
38
+ - **What An Exception Knows About The Failure** — Every `RequestError` now carries `phase`, `retryable`, `safe_to_replay`, `status_code`, `proxy_error_kind`, `tls_alert` and `diagnostics`, read from the Rust library's error, where an exception used to carry its message alone: an `HttpStatusError` could not give its status code, a `ProxyError` did not say whether the proxy wanted credentials or could not be reached, and the timings of a failed request were thrown away. `phase` names the step of the connection that failed (`"tcp_connect"`, `"proxy_tunnel"`, `"tls_handshake"`, …); `retryable` says whether trying again could succeed, and `safe_to_replay` whether the server cannot have processed the request, so that even a POST can be sent again; `proxy_error_kind` says what went wrong with a proxy (`"authentication_required"`, `"tunnel_refused"`, …) and `tls_alert` which alert a handshake ended in; `diagnostics` holds the timings and transport so far, in the form of `Response.diagnostics`, for a failed request of the HTTP methods and `request()`, and the response's own for `error_for_status()`. An attribute that does not apply is `None`, or `False` for the two flags, on an error built by hand as well, and the attributes survive pickling. A retry callable that also takes `exception=` (or `**kwargs`) is given the exception itself, or `None` when a status is being retried; one taking only `(attempt, error, status)` is called exactly as before.
39
+ - **Proxies From A Function** — `ProxyPool`, `SessionPool` and `BlockingSessionPool` take a function in place of the list of proxies. It is called with no arguments for every proxy the pool hands out and returns a URL or a `ProxyConfig`: the way to use a gateway that picks the exit by a session id in the username, with a new id each time. What the function raises comes out of the `acquire()` that called it, and so does a `ValueError` for a URL that does not parse or a `TypeError` for anything but a URL or a `ProxyConfig`, where the Rust library would only log it and keep asking, so that `acquire()` waited for good. An `async def` is refused with `TypeError`. Every proxy from a function is a new one, so `rotation`, `bad_proxy_config` and `health_check` do not apply to them, and `max_proxies` / `max_sessions` still cap how many are out at once. The async pools call the function on a worker thread that waits for it; `proxy_buffer=n` has `n` proxies made ahead of time on a thread of their own, where a failure is logged and the function is called again a second later, rather than at once and over and over as the Rust library would. `proxy_buffer=` with a list raises `ValueError`.
40
+ - **Blocking `ProxyPool`** — Added `lkrequest.blocking.ProxyPool` (`lkrequest.BlockingProxyPool`), which takes the same arguments as `ProxyPool`, blocks in `acquire()` and `acquire_fresh()`, and hands out the same `ProxyLease`.
41
+ - **`bad_proxy_config=` And `health_check=` For Session Pools** — `SessionPool` and `BlockingSessionPool` now take the `BadProxyConfig` and `HealthCheckConfig` that `ProxyPool` already did, so a session pool can cool a failing proxy down after fewer or more failures than the default three, and probe its proxies in the background.
42
+ - **A DNS Server Of Your Own** — `Client(dns=...)` and `BlockingClient(dns=...)` now take the address of a DNS server besides the seven resolver names: `"ip"` for port 53, or `"ip:port"`, an IPv6 address with a port in brackets (`"[2606:4700::1111]:53"`). It is asked over UDP, and over TCP for an answer too long for UDP. Like the named public resolvers and unlike the system one, it also answers HTTPS records, which is how the client learns a site's ECH configuration and HTTP/3 support from DNS. Anything that is neither a name nor an address still raises `ValueError`, whose message now mentions the address form.
43
+ - **Python Code As The Resolver** — `dns=` also takes Python code that looks names up: a function `resolve(host, port)` returning a list of addresses, each an IP, which takes the requested port, or `"ip:port"`; or an object with that method and, optionally, `lookup_https(host)`, returning `None` or the host's HTTPS record as a dict of `alpn`, `no_default_alpn`, `target`, `port`, `ech_config_list`, `ipv4_hints` and `ipv6_hints`, from which the client learns a site's HTTP/3 support and ECH configuration as it would from DNS. The code must be synchronous, and an `async def` raises `TypeError`; it runs on a thread of the runtime's blocking pool, so a slow lookup holds up no other request. `dns_timeout` bounds each call and `ip_family` filters the answers, as for any resolver. What `resolve` raises becomes the `__cause__` of the request's `ConnectionError`, so its traceback shows the line that failed; an answer that is not an address fails the request with a message saying so, and a `lookup_https` that fails is logged and counts as no record. A copy made by `randomize_fingerprint()` keeps the resolver.
44
+ - **Log Events In Python's `logging`** — `set_log_level(level, format="logging")` hands every event that passes `level` to Python's `logging` instead of writing it to stderr, on the logger `lkrequest.` followed by the event's Rust target with `::` as `.` (`lkrequest.lkrequest.session`, `lkrequest.lktls.handshake`), so `logging.getLogger("lkrequest")` takes them all. The message carries the event's fields as `name=value`; a TRACE event, which `logging` has no level for, has level 5. The events are queued and passed on from a thread of their own, a batch per hold of the GIL, so a request never waits for the GIL to log; a queue that fills up drops events, and none are passed on once the interpreter starts to exit. As with the other formats, only the first call in a process takes effect.
45
+ - **Every Axis Of A Protocol Policy** — `ProtocolPolicy` gained `with_acquisition()`, `with_upgrade()` and `with_fallback()`, the read-only `acquisition`, `upgrade` and `fallback`, and `validate()`, alongside the new `AcquisitionPolicy` (`ReuseExistingFirst`, `EstablishBestAvailable`), `RacePolicy`, `UpgradePolicy` (`Off`, `ProbeOnly`, `ProbeAndMigrate`, `ForceForNewRequests`) and `FallbackPolicy` (`AllowFallback`, `AllowFallbackOnNetworkErrors`, `NoFallback`). Only the four presets and `with_intent()` were available before. A `RacePolicy` races TCP against QUIC with the timing given: whether each takes part, the delay before each starts, and whether to race only an origin that has advertised HTTP/3; what it leaves out keeps `RacePolicy.chrome_like()`, which starts QUIC at once and TCP 0.3 s later. A policy whose axes contradict one another now raises `ValueError` when it is given to a client or a session, where the Rust library would only refuse it once the first request was sent; given to a single request, it is still refused before anything goes out.
46
+ - **WebSocket Extensions, Pings And URL** — `ws_connect()` on both session classes takes `extensions=`, offered in the upgrade's `Sec-WebSocket-Extensions` header in the order given; the header is part of what a browser's handshake looks like. Both connection classes gained `ping(data=b"")`, whose pong comes back through `recv()`, `pong(data=b"")` for an unsolicited pong, and a read-only `url`, still there once the connection is closed. A ping or pong of more than 125 bytes, which a peer would fail the connection for, raises `ValueError` before anything is sent. The frames are still read and written plain, so only an extension that leaves them alone can be used: once a server accepts `permessage-deflate`, the compression Chrome offers, every compressed message it sends fails to be received with `RequestError`.
47
+ - **TCP Options, Keep-Alive And Two More TCP Presets** — `TcpFingerprint(...)` takes `tcp_options=`, the SYN's option order as kind numbers — 0 (EOL), 1 (NOP), 2 (MSS), 3 (window scale), 4 (SACK permitted), 8 (timestamps), the second part of JA4T — and `keepalive=`, `keepalive_idle=` and `keepalive_interval=`. The option order only describes the target fingerprint, since the OS builds the SYN, and shows in `to_ja4t()`; keep-alive is set on the socket: `keepalive=True` turns it on with the OS's timing, either timing in seconds turns it on with that timing, and `keepalive=False` turns it off. An unknown option kind, or `keepalive=False` given with a timing, raises `ValueError`. Added `TcpFingerprint.to_dict()`, which reads every setting back under its argument's name, and the presets `TcpFingerprint.none()`, which sets nothing, and `TcpFingerprint.safari_ios()`.
48
+ - **QUIC Cooldowns Of Your Own** — `session(broken_quic_policy=...)` on both clients also takes a `BrokenQuicConfig`, for when none of the `BrokenQuicPolicy` presets fits how long an origin whose QUIC connection failed should stay on TCP. It takes `initial_cooldown`, the seconds after the first failure; `max_cooldown`, which the cooldown grows to over `failure_escalation_threshold` failures in a row; `route_cooldown`, for a proxy route that cannot carry QUIC; and `max_entries` and `max_route_entries`, how many origins and routes are remembered. What is left out keeps the value of `BrokenQuicPolicy.Strict`, the default, and each value reads back under its own name. A duration that is negative, NaN or infinite, an `initial_cooldown` above `max_cooldown` and a count of zero raise `ValueError`, where the Rust library would quietly cap `initial_cooldown` at `max_cooldown` and take a count of zero as one. `Strict`, `Resilient` and `Disabled` are unchanged.
49
+ - **Proxy Credentials Worked Out Per Connection** — Added `ProxyConfig.with_custom_auth(identity, authorize)`, for a proxy credential that has to be fresh, such as a short-lived token. `authorize()` takes no arguments and returns the value of the `Proxy-Authorization` header, or of the header `with_auth_header()` chose, or `None` to send none. It is called on the thread setting each connection up, which waits for it, and more than once for a connection the proxy challenges with a 407. One that raises, or returns something other than a string, is logged as a warning and the connection goes without a credential, so the request fails with the proxy's answer. `identity` is a stable string, not a secret, that becomes part of `identity()`, so that pools tell two credentials on the same proxy address apart. Like `with_http_auth()`, it replaces any credential already set and raises `ValueError` on a SOCKS5 proxy; an `async def` raises `TypeError`.
50
+ - **Check A Proxy Without A Pool** — Added `Client.check_proxy(proxy, target_host="www.google.com", target_port=443, timeout=5.0)`, and the blocking `BlockingClient.check_proxy()`, which set a tunnel up through a proxy URL or `ProxyConfig`, chains and credentials included, and return the seconds that took: the health check a `ProxyPool` runs in the background, made once. Nothing is sent through the tunnel. Names are resolved as the client resolves them, so `dns=` applies. A proxy that refuses the tunnel or cannot be reached raises `ProxyError`, one that takes longer than `timeout` raises `TimeoutError`, and a MASQUE proxy, which carries no TCP, raises `ProxyError`.
51
+ - **What A `ProxyConfig` Points At** — Added the read-only `ProxyConfig.host` and `ProxyConfig.port`, of the proxy the client talks to last — the only one, or a chain's final hop — with the scheme's port when the URL gives none and an IPv6 address in brackets, and `ProxyConfig.forbids_direct_fallback`, true for a route through a MASQUE proxy, which never falls back to a direct connection, even with `proxy_fallback_direct`.
52
+ - **HSTS Policies Of Your Own** — Added `Hsts.custom(should_upgrade, record_sts=None)`, a policy written in Python. `should_upgrade(host)` says whether an `http://` URL, a redirect's included, is upgraded to `https://`; `record_sts(host, header)`, if given, is told the value of each `Strict-Transport-Security` header received over HTTPS from a host that is not an IP address. They run on the thread running the request, which waits for them; one that raises is logged as a warning and the URL is not upgraded, and an `async def` raises `TypeError`. Added `Hsts.should_upgrade(host)`, which answers as a session would ask the policy — for a dynamic one, after what it has learned.
53
+ - **Requests Straight From A Client** — `Client` and `BlockingClient` gained `get()`, `post()`, `put()`, `delete()`, `head()`, `patch()` and `options()`, each `session().get(url, **kwargs)` and so on: the request goes out from a session made for the call and dropped after it, as the Rust library's `Client::get()` does, and takes what the session's method of the same name takes. No connection or cookie is kept from one call to the next, so for more than one request a session is still the way.
54
+ - **`H2Profile.validate()`** — Raises `ValueError` if an HTTP/2 profile breaks a rule a usable fingerprint keeps: the four pseudo-headers once each, no SETTINGS id twice, `ENABLE_PUSH` 0 or 1, `INITIAL_WINDOW_SIZE` at most 2\*\*31 − 1, `MAX_FRAME_SIZE` from 16384 to 16777215, and a `window_update` that keeps the connection window at most 2\*\*31 − 1. The message says which. Nothing else checks, as before: a client sends a profile that breaks one, made by hand or read from JSON, as it is.
55
+ - **TLS Secrets To A Function** — `Client(keylog=...)` and `BlockingClient(keylog=...)` take a function besides a file path, called with each line of the NSS key log format Wireshark reads, without its newline, several times per handshake. It runs on the thread doing the handshake, which waits for it, so every line of a request's handshake is in by the time the request returns; one that raises is logged as a warning and the handshake goes on. An `async def` raises `TypeError`. A file path may now also be an `os.PathLike`, such as a `pathlib.Path`.
56
+ - **A DER Certificate For The MASQUE Proxy** — `MasqueConfig(ca_cert_der=...)` trusts one DER certificate for the hop to a MASQUE proxy, as `Client(ca_cert_der=...)` does for the origin, alongside any from `ca_cert` and `ca_cert_pem`, and counts in `ca_cert_count`. Bytes that are not a usable certificate raise `ValueError` when the config is built, where the Rust library would only refuse them when the first tunnel is set up.
57
+ - **What A Build Offers** — Added `lkrequest.features()`, which says which optional features this build has, as a dict of `quic-h3`, `masque`, `doh`, `telemetry` and `synthetic-fp` to booleans, so that a build from source can be told apart without trying `hasattr(lkrequest, "QuicProfile")`; the wheels on PyPI have them all. Added `lkrequest.list_presets()`, the browser presets in a stable order, each a dict of `name`, `browser` and `http3`, whether the preset's client speaks HTTP/3 in this build; a name is what `Client.<name>()`, `tls_profile=` and `h2_profile=` take.
58
+ - **What A Client Was Built With** — `Client` and `BlockingClient` gained read-only `tls_profile`, `h2_profile`, `tcp_fingerprint`, `default_headers`, `header_order`, `h3_header_order`, `cookie_order`, `timeouts`, `resource_limits`, `protocol_policy`, `verify`, `ip_family`, `h2_fallback_h1`, `proxy_fallback_direct`, `max_pending_h2_requests` and `ech_config`: the settings in effect once the client is built, a preset applied and the defaults included, mostly under the name of the argument that sets each. `timeouts` and `resource_limits` are dicts keyed by those argument names, durations in seconds, and no cap on connections reads as `None`. `default_headers` gives `(name, value)` pairs in the order they go out, names lowercased.
59
+ - **ECH And The ClientHello Style On A Hand-Built `TlsProfile`** — `TlsProfile(...)` takes `tls_client_hello_style`, the TLS stack whose rules the ClientHello follows where no list says them (`"generic"`, the default, `"chromium_boringssl"`, `"firefox_nss"`, `"safari_cfnetwork"` or `"java_jsse"`), `ech`, the ECH extension to send, and `ech_outer_extensions`, the extensions an inner ClientHello compresses under real ECH. Only `TlsProfile.from_json()` could set them before. The new `EchMode` gives the extension: `EchMode.grease(...)`, with Chrome's HPKE suite unless told otherwise, `EchMode.real(ech_config_list)` and `EchMode.disabled()`. An unknown style, and a GREASE payload minimum above its maximum, raise `ValueError`.
60
+
61
+ ### 🔧 Changes
62
+ - **The Sessions Given One `Hsts` Share What It Learns** — `Hsts.dynamic()` made a new store for every session it was given to, so a host learned from a `Strict-Transport-Security` header in one session was not upgraded in another, even one given the same object. An `Hsts` object now holds one policy, shared by every session it is given to, as one browser profile shares what it learns. Code that gives one `Hsts.dynamic()` to several sessions now has each of them upgrade what any of them learned; to keep them apart, give each session an `Hsts.dynamic()` of its own. `Hsts.static_()`, `Hsts.none()` and the seed of `Hsts.dynamic()` behave as before.
63
+ - **`TimeoutConfig` And `ResourceLimits` Removed** — Both classes could be built, and the README showed them beside the client's timeout options, but nothing ever accepted one, so building one changed nothing. They are gone, from `lkrequest` and `lkrequest.blocking` alike. The `Client` / `BlockingClient` arguments they mirrored — `dns_timeout=`, `tcp_connect_timeout=`, `tls_handshake_timeout=`, `ttfb_timeout=`, `total_timeout=`, `max_response_body_size=`, `max_connections_per_session=`, `min_transfer_rate=` and the rest — are unchanged, and remain the way to set these limits.
64
+ - **PyO3 0.30** — The bindings now build on PyO3 0.30 (previously 0.23) and pyo3-async-runtimes 0.30, and the Rust dependencies are refreshed. The wheels still use the stable ABI of Python 3.9 and later; building from source now needs Rust 1.89 or later. What this changes in Python:
65
+ - **Argument errors no longer name the argument in their message.** `get(url, timeout="x")` used to raise `TypeError: argument 'timeout': must be real number, not str` and now raises `TypeError: must be real number, not str`. On Python 3.11 and later the argument is named in a note (`while processing 'timeout'`), which tracebacks print; Python 3.9 and 3.10 have no exception notes, so there the argument is not named at all. The exception types are unchanged, and so are the messages lkrequest raises itself, such as `tls_profile must be str (preset name) or TlsProfile object`. A value of the wrong class is now reported as `'str' object is not an instance of 'ProtocolPolicy'` rather than `'str' object cannot be converted to 'ProtocolPolicy'`.
66
+ - **Enum values returned by lkrequest are the class attributes themselves**, so `response.version is HttpVersion.H2` now holds; `==` and hashing behave as before.
67
+ - **The async client hands each result to the event loop from tokio's blocking thread pool** rather than from the thread that ran the request, so a result arriving while another thread holds the GIL no longer holds up the requests still in flight. Under load the process may show a few dozen more threads; they exit after ten idle seconds.
68
+ - Built from source for a free-threaded Python (3.13t, 3.14t), the module still turns the GIL back on when it is imported, as before: it has not been reviewed for running without the GIL.
69
+ - **Dependencies Without Known Advisories** — The wheels now build with rustls 0.23.46 (0.2.3: 0.23.36), rustls-webpki 0.103.16 (0.103.9), webpki-roots 1.0.10 (1.0.6), aws-lc-sys 0.46.0 (0.37.1), Tokio 1.54.1 (1.49.0) and zstd 0.13.4 (0.13.3) with zstd-safe 7.4.0 (7.2.4); the Rust library now requires rustls-webpki, webpki-roots, Tokio and zstd at 0.103.16, 1.0.10, 1.53.3 and 0.13.4 or later. When this was written, `cargo audit` found no advisory against the `Cargo.lock` the wheels build from, and 23 vulnerabilities against 0.2.3's. One fix shows from Python: certificate verification now rejects a chain through one of emSign's eight distrusted G3 intermediates, whatever root it ends in, and the bundled Mozilla roots no longer include emSign ECC Root CA - G3, so a server whose certificate was issued under them fails verification.
70
+
71
+ ### 🐞 Fixes
72
+ - **Chrome 155 Sends Its Trust-Anchor IDs In Chrome's Order** — inherited from upstream, and it changes the default fingerprint on the wire. `Client.chrome_155()`, the default, shuffled the 28 IDs of its `trust_anchors` extension (`0xca34`) on every handshake, where Chrome 155 sorts them before sending them. It now sends them in that one fixed, sorted order, over TCP and QUIC alike, and still shuffles the order of the extensions on every handshake; `TlsProfile.chrome_155()` lists the IDs sorted, with `shuffle` now `False`. The Chrome 152 to 154 presets keep shuffling the IDs.
73
+ - **`ProxyPool.acquire()` Returns A Lease With A Usable Proxy** — `acquire()` returned the proxy's pool key, `ProxyConfig.identity()`, which is not a URL: lately a JSON array that leaves the password out. Passed to `session(proxy=...)`, as it was meant to be, it built a session without a proxy (see the next entry), so the requests went out direct. And because the pool's share was handed back the moment `acquire()` returned, `max_proxies` capped nothing. `acquire()` now returns a `ProxyLease`, an incompatible change. Its `proxy` is the `ProxyConfig` to pass to `session(proxy=...)`, credentials included, or `None` from a pool created with an empty list, which used to return `""`. The lease holds one of the pool's `max_proxies` shares (default 100) until `release()`, the end of a `with` or `async with` block, or garbage collection. Added `ProxyLease.mark_bad()`; `ProxyPool.acquire_fresh(lease)`, which records a failure of the lease's proxy, releases the lease and leases another proxy; and `ProxyPool.max_concurrent`. `ProxyPool.mark_bad(identity)` is unchanged: pass it `lease.proxy.identity()`.
74
+ - **A Session Proxy URL That Does Not Parse Raises Instead Of Connecting Direct** — `Client.session(proxy=...)` and `BlockingClient.session(proxy=...)` handed a proxy URL to the Rust library, which logs a URL it cannot parse and builds the session without a proxy. A typo in the scheme, or a pool key such as `ProxyConfig.identity()` returns, therefore sent every request of that session straight from this machine to the target, exposing its address, and nothing was raised. Such a URL now raises `ValueError`, as it already did in `ProxyConfig(...)`, in the proxy lists of `ProxyPool` and `SessionPool`, and in a request's own `proxy=`.
75
+ - **`randomize_fingerprint()` Keeps The Rest Of The Client** — `Client.randomize_fingerprint()` and `BlockingClient.randomize_fingerprint()` built the copy from the TLS, HTTP/2, TCP and QUIC ClientHello profiles alone, so everything else the client was created with fell back to its default: default headers, header and cookie order, `http1_title_case_headers`, timeouts and limits, `verify` and the CA certificates, DNS, `ip_family`, `connect_to`, middleware, the QUIC profile and MASQUE. The copy of a browser preset also lost the preset's header-order templates and QUIC profile, so it stopped sending what that browser sends. The copy is now built again from the same options, or the same preset, with only the TLS profile replaced. A CA file named by `ca_cert` is read again when the copy is made.
76
+ - **A `Part` Can Be Added To A Form** — `Part` could be built and given a filename and a content type, but nothing accepted it, so it never reached a request. Added `Multipart.part(part)`. A `Part` says what `text()` and `file()` cannot, such as binary data without a filename, and the same `Part` can be added to any number of forms.
77
+ - **A `Multipart` Can Be Sent More Than Once** — a request emptied the `Multipart` it was given, so the same form sent again went out with no fields at all, only its closing boundary, and nothing was raised. A form now keeps its fields, and each request encodes them afresh with a boundary of its own, as a browser gives each submission of a form; a field added after a request goes out with the next one.
78
+ - **`head()`, `options()` And `send_streaming()` Take Every Request Option** — `head()` and `options()` did not accept `no_decompress`, `accept_encoding`, `priority`, `h2_priority_weight`, `preferred_http_version` or `idempotency`, and `send_streaming()` lacked all of them but `accept_encoding`, although the README listed them for every method and the Rust library honours them on these requests too. Passing one raised `TypeError`; the way around it was `request("HEAD", ...)` for the first two and there was none for a streamed request. `head()` and `options()` now take what `get()` takes, and `send_streaming()` what `request()` takes, on both session classes.
79
+ - **Default Headers And Request Cookies Keep Their Order** — `Client(default_headers=...)` and the `cookies=` / `cookie_override=` of every request method were copied into unordered maps, so each client sent its default headers, and each request its cookies, in an arbitrary order of its own: clients built from the same defaults did not agree with one another, and the same request did not produce the same `Cookie` header twice — orders no browser produces, over HTTP/1.1 and HTTP/2 alike. The Rust library keeps both in the order they are given, and they are now handed on that way. Default headers go out in the order given wherever `header_order` does not place them. Request cookies follow the session's own cookies in the order given, overrides last, unless `cookie_order` places them. All three parameters also accept a list of `(name, value)` pairs, as `headers=` does. A default header given twice is sent once, with the last value, where it first appeared; a cookie given twice in `cookies=` is sent twice, as the Rust library sends it.
80
+ - **`on_request` Hooks See The Headers In Order** — the `headers` dict an `on_request` hook receives now lists the request's headers in the order they were given, instead of in an arbitrary one.
81
+ - **A Duration Too Large To Hold Raises `ValueError`** — A number of seconds of 2\*\*64 or more, such as `total_timeout=1e30`, a request's `timeout=` or a `RacePolicy` delay, crashed with a `PanicException`, which derives from `BaseException`, so `except Exception` did not catch it. Every option that takes seconds now raises `ValueError` for it, as for a negative, NaN or infinite value. A retry callable that returns such a delay no longer crashes the request either: as with a negative delay, the request is not retried and fails with its own error.
82
+ - **A `TcpFingerprint` Shows What It Sets** — its repr was `TcpFingerprint()` whenever the four values JA4T is made of were not all set, as for a fingerprint that sets nothing, so `TcpFingerprint(ttl=128, keepalive=True)` looked like `TcpFingerprint.none()`; and when they were, it showed the JA4T alone, so `TcpFingerprint.chrome_win()` and the same JA4T without a TTL looked alike. It now names every setting that is set, the JA4T ones as one `ja4t=` when all four are: `TcpFingerprint(ja4t='64240_2-1-3-1-1-4_1460_8', ttl=128, tcp_nodelay=True)`.
83
+
5
84
  ## lkrequest-py 0.2.3
6
85
 
7
86
  Adds the Chrome 155 fingerprint and picks up upstream's switch of the default
@@ -7,7 +7,7 @@ binding over the `lkrequest` Rust workspace, built with
7
7
  ## Development setup
8
8
 
9
9
  ```bash
10
- git clone https://github.com/EZ-XLabs/lkrequest-py
10
+ git clone https://github.com/EZXLabs/lkrequest-py
11
11
  cd lkrequest-py
12
12
  python3 -m venv .venv # Linux/macOS
13
13
  # py -m venv .venv # Windows