httpunk 0.1.0a1__tar.gz → 0.1.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.
Files changed (133) hide show
  1. {httpunk-0.1.0a1 → httpunk-0.1.1}/Cargo.lock +11 -11
  2. {httpunk-0.1.0a1 → httpunk-0.1.1}/Cargo.toml +2 -3
  3. {httpunk-0.1.0a1 → httpunk-0.1.1}/PKG-INFO +54 -15
  4. {httpunk-0.1.0a1 → httpunk-0.1.1}/README.md +51 -13
  5. {httpunk-0.1.0a1 → httpunk-0.1.1}/crates/vendor-h2/src/lib.rs +1 -1
  6. {httpunk-0.1.0a1 → httpunk-0.1.1}/crates/vendor-hyper/src/lib.rs +1 -1
  7. {httpunk-0.1.0a1 → httpunk-0.1.1}/httpunk/_backend/__init__.py +7 -2
  8. {httpunk-0.1.0a1 → httpunk-0.1.1}/httpunk/_backend/asyncio.py +23 -2
  9. {httpunk-0.1.0a1 → httpunk-0.1.1}/httpunk/_backend/tonio.py +7 -2
  10. {httpunk-0.1.0a1 → httpunk-0.1.1}/httpunk/asyncio.py +102 -10
  11. {httpunk-0.1.0a1 → httpunk-0.1.1}/httpunk/h1/server.py +49 -15
  12. {httpunk-0.1.0a1 → httpunk-0.1.1}/httpunk/h2/client.py +1 -2
  13. {httpunk-0.1.0a1 → httpunk-0.1.1}/httpunk/util/__init__.py +1 -3
  14. {httpunk-0.1.0a1 → httpunk-0.1.1}/httpunk/util/pool.py +1 -1
  15. {httpunk-0.1.0a1 → httpunk-0.1.1}/pyproject.toml +5 -7
  16. {httpunk-0.1.0a1/src/py → httpunk-0.1.1/src}/h1/codec.rs +1 -1
  17. {httpunk-0.1.0a1/src/py → httpunk-0.1.1/src}/h2/codec.rs +1 -1
  18. {httpunk-0.1.0a1/src/py → httpunk-0.1.1/src}/h2/streams.rs +1 -1
  19. {httpunk-0.1.0a1 → httpunk-0.1.1}/src/lib.rs +10 -7
  20. httpunk-0.1.1/tests/test_asyncio_client_protocol.py +165 -0
  21. {httpunk-0.1.0a1 → httpunk-0.1.1}/tests/test_h1_server.py +29 -0
  22. httpunk-0.1.0a1/src/py/mod.rs +0 -26
  23. {httpunk-0.1.0a1 → httpunk-0.1.1}/LICENSE +0 -0
  24. {httpunk-0.1.0a1 → httpunk-0.1.1}/crates/vendor-h2/Cargo.toml +0 -0
  25. {httpunk-0.1.0a1 → httpunk-0.1.1}/crates/vendor-h2/LICENSE +0 -0
  26. {httpunk-0.1.0a1 → httpunk-0.1.1}/crates/vendor-h2/UPSTREAM_VERSION +0 -0
  27. {httpunk-0.1.0a1 → httpunk-0.1.1}/crates/vendor-h2/src/codec/error.rs +0 -0
  28. {httpunk-0.1.0a1 → httpunk-0.1.1}/crates/vendor-h2/src/codec/mod.rs +0 -0
  29. {httpunk-0.1.0a1 → httpunk-0.1.1}/crates/vendor-h2/src/ext.rs +0 -0
  30. {httpunk-0.1.0a1 → httpunk-0.1.1}/crates/vendor-h2/src/frame/data.rs +0 -0
  31. {httpunk-0.1.0a1 → httpunk-0.1.1}/crates/vendor-h2/src/frame/go_away.rs +0 -0
  32. {httpunk-0.1.0a1 → httpunk-0.1.1}/crates/vendor-h2/src/frame/head.rs +0 -0
  33. {httpunk-0.1.0a1 → httpunk-0.1.1}/crates/vendor-h2/src/frame/headers.rs +0 -0
  34. {httpunk-0.1.0a1 → httpunk-0.1.1}/crates/vendor-h2/src/frame/mod.rs +0 -0
  35. {httpunk-0.1.0a1 → httpunk-0.1.1}/crates/vendor-h2/src/frame/ping.rs +0 -0
  36. {httpunk-0.1.0a1 → httpunk-0.1.1}/crates/vendor-h2/src/frame/priority.rs +0 -0
  37. {httpunk-0.1.0a1 → httpunk-0.1.1}/crates/vendor-h2/src/frame/reason.rs +0 -0
  38. {httpunk-0.1.0a1 → httpunk-0.1.1}/crates/vendor-h2/src/frame/reset.rs +0 -0
  39. {httpunk-0.1.0a1 → httpunk-0.1.1}/crates/vendor-h2/src/frame/settings.rs +0 -0
  40. {httpunk-0.1.0a1 → httpunk-0.1.1}/crates/vendor-h2/src/frame/stream_id.rs +0 -0
  41. {httpunk-0.1.0a1 → httpunk-0.1.1}/crates/vendor-h2/src/frame/util.rs +0 -0
  42. {httpunk-0.1.0a1 → httpunk-0.1.1}/crates/vendor-h2/src/frame/window_update.rs +0 -0
  43. {httpunk-0.1.0a1 → httpunk-0.1.1}/crates/vendor-h2/src/hpack/decoder.rs +0 -0
  44. {httpunk-0.1.0a1 → httpunk-0.1.1}/crates/vendor-h2/src/hpack/encoder.rs +0 -0
  45. {httpunk-0.1.0a1 → httpunk-0.1.1}/crates/vendor-h2/src/hpack/header.rs +0 -0
  46. {httpunk-0.1.0a1 → httpunk-0.1.1}/crates/vendor-h2/src/hpack/huffman/mod.rs +0 -0
  47. {httpunk-0.1.0a1 → httpunk-0.1.1}/crates/vendor-h2/src/hpack/huffman/table.rs +0 -0
  48. {httpunk-0.1.0a1 → httpunk-0.1.1}/crates/vendor-h2/src/hpack/mod.rs +0 -0
  49. {httpunk-0.1.0a1 → httpunk-0.1.1}/crates/vendor-h2/src/hpack/table.rs +0 -0
  50. {httpunk-0.1.0a1 → httpunk-0.1.1}/crates/vendor-h2/src/proto/error.rs +0 -0
  51. {httpunk-0.1.0a1 → httpunk-0.1.1}/crates/vendor-h2/src/proto/mod.rs +0 -0
  52. {httpunk-0.1.0a1 → httpunk-0.1.1}/crates/vendor-h2/src/proto/streams/flow_control.rs +0 -0
  53. {httpunk-0.1.0a1 → httpunk-0.1.1}/crates/vendor-h2/src/proto/streams/mod.rs +0 -0
  54. {httpunk-0.1.0a1 → httpunk-0.1.1}/crates/vendor-h2/src/proto/streams/state.rs +0 -0
  55. {httpunk-0.1.0a1 → httpunk-0.1.1}/crates/vendor-hyper/Cargo.toml +0 -0
  56. {httpunk-0.1.0a1 → httpunk-0.1.1}/crates/vendor-hyper/LICENSE +0 -0
  57. {httpunk-0.1.0a1 → httpunk-0.1.1}/crates/vendor-hyper/UPSTREAM_VERSION +0 -0
  58. {httpunk-0.1.0a1 → httpunk-0.1.1}/crates/vendor-hyper/src/body/length.rs +0 -0
  59. {httpunk-0.1.0a1 → httpunk-0.1.1}/crates/vendor-hyper/src/body/mod.rs +0 -0
  60. {httpunk-0.1.0a1 → httpunk-0.1.1}/crates/vendor-hyper/src/cfg.rs +0 -0
  61. {httpunk-0.1.0a1 → httpunk-0.1.1}/crates/vendor-hyper/src/common/date.rs +0 -0
  62. {httpunk-0.1.0a1 → httpunk-0.1.1}/crates/vendor-hyper/src/common/mod.rs +0 -0
  63. {httpunk-0.1.0a1 → httpunk-0.1.1}/crates/vendor-hyper/src/error.rs +0 -0
  64. {httpunk-0.1.0a1 → httpunk-0.1.1}/crates/vendor-hyper/src/ext/h1_reason_phrase.rs +0 -0
  65. {httpunk-0.1.0a1 → httpunk-0.1.1}/crates/vendor-hyper/src/ext/informational.rs +0 -0
  66. {httpunk-0.1.0a1 → httpunk-0.1.1}/crates/vendor-hyper/src/ext/mod.rs +0 -0
  67. {httpunk-0.1.0a1 → httpunk-0.1.1}/crates/vendor-hyper/src/headers.rs +0 -0
  68. {httpunk-0.1.0a1 → httpunk-0.1.1}/crates/vendor-hyper/src/proto/h1/decode.rs +0 -0
  69. {httpunk-0.1.0a1 → httpunk-0.1.1}/crates/vendor-hyper/src/proto/h1/encode.rs +0 -0
  70. {httpunk-0.1.0a1 → httpunk-0.1.1}/crates/vendor-hyper/src/proto/h1/httpunk.rs +0 -0
  71. {httpunk-0.1.0a1 → httpunk-0.1.1}/crates/vendor-hyper/src/proto/h1/io.rs +0 -0
  72. {httpunk-0.1.0a1 → httpunk-0.1.1}/crates/vendor-hyper/src/proto/h1/mod.rs +0 -0
  73. {httpunk-0.1.0a1 → httpunk-0.1.1}/crates/vendor-hyper/src/proto/h1/role.rs +0 -0
  74. {httpunk-0.1.0a1 → httpunk-0.1.1}/crates/vendor-hyper/src/proto/mod.rs +0 -0
  75. {httpunk-0.1.0a1 → httpunk-0.1.1}/crates/vendor-hyper/src/trace.rs +0 -0
  76. {httpunk-0.1.0a1 → httpunk-0.1.1}/crates/vendor-hyper-util/Cargo.toml +0 -0
  77. {httpunk-0.1.0a1 → httpunk-0.1.1}/crates/vendor-hyper-util/LICENSE +0 -0
  78. {httpunk-0.1.0a1 → httpunk-0.1.1}/crates/vendor-hyper-util/UPSTREAM_VERSION +0 -0
  79. {httpunk-0.1.0a1 → httpunk-0.1.1}/crates/vendor-hyper-util/src/client/mod.rs +0 -0
  80. {httpunk-0.1.0a1 → httpunk-0.1.1}/crates/vendor-hyper-util/src/client/proxy/matcher.rs +0 -0
  81. {httpunk-0.1.0a1 → httpunk-0.1.1}/crates/vendor-hyper-util/src/client/proxy/mod.rs +0 -0
  82. {httpunk-0.1.0a1 → httpunk-0.1.1}/crates/vendor-hyper-util/src/lib.rs +0 -0
  83. {httpunk-0.1.0a1 → httpunk-0.1.1}/httpunk/__init__.py +0 -0
  84. {httpunk-0.1.0a1 → httpunk-0.1.1}/httpunk/_common.py +0 -0
  85. {httpunk-0.1.0a1 → httpunk-0.1.1}/httpunk/_httpunk.pyi +0 -0
  86. {httpunk-0.1.0a1 → httpunk-0.1.1}/httpunk/exceptions.py +0 -0
  87. {httpunk-0.1.0a1 → httpunk-0.1.1}/httpunk/h1/__init__.py +0 -0
  88. {httpunk-0.1.0a1 → httpunk-0.1.1}/httpunk/h1/client.py +0 -0
  89. {httpunk-0.1.0a1 → httpunk-0.1.1}/httpunk/h1/connection.py +0 -0
  90. {httpunk-0.1.0a1 → httpunk-0.1.1}/httpunk/h1/share.py +0 -0
  91. {httpunk-0.1.0a1 → httpunk-0.1.1}/httpunk/h2/__init__.py +0 -0
  92. {httpunk-0.1.0a1 → httpunk-0.1.1}/httpunk/h2/connection.py +0 -0
  93. {httpunk-0.1.0a1 → httpunk-0.1.1}/httpunk/h2/server.py +0 -0
  94. {httpunk-0.1.0a1 → httpunk-0.1.1}/httpunk/h2/settings.py +0 -0
  95. {httpunk-0.1.0a1 → httpunk-0.1.1}/httpunk/h2/share.py +0 -0
  96. {httpunk-0.1.0a1 → httpunk-0.1.1}/httpunk/h2/stream.py +0 -0
  97. {httpunk-0.1.0a1 → httpunk-0.1.1}/httpunk/h2/streams.py +0 -0
  98. {httpunk-0.1.0a1 → httpunk-0.1.1}/httpunk/http.py +0 -0
  99. {httpunk-0.1.0a1 → httpunk-0.1.1}/httpunk/py.typed +0 -0
  100. {httpunk-0.1.0a1 → httpunk-0.1.1}/httpunk/types.py +0 -0
  101. {httpunk-0.1.0a1 → httpunk-0.1.1}/httpunk/util/auto.py +0 -0
  102. {httpunk-0.1.0a1 → httpunk-0.1.1}/httpunk/util/client.py +0 -0
  103. {httpunk-0.1.0a1 → httpunk-0.1.1}/httpunk/util/graceful.py +0 -0
  104. {httpunk-0.1.0a1 → httpunk-0.1.1}/httpunk/util/proxy.py +0 -0
  105. {httpunk-0.1.0a1/src/py → httpunk-0.1.1/src}/errors.rs +0 -0
  106. {httpunk-0.1.0a1/src/py → httpunk-0.1.1/src}/h1/mod.rs +0 -0
  107. {httpunk-0.1.0a1/src/py → httpunk-0.1.1/src}/h2/mod.rs +0 -0
  108. {httpunk-0.1.0a1/src/py → httpunk-0.1.1/src}/http/mod.rs +0 -0
  109. {httpunk-0.1.0a1/src/py → httpunk-0.1.1/src}/proxy.rs +0 -0
  110. {httpunk-0.1.0a1 → httpunk-0.1.1}/tests/_client.py +0 -0
  111. {httpunk-0.1.0a1 → httpunk-0.1.1}/tests/conftest.py +0 -0
  112. {httpunk-0.1.0a1 → httpunk-0.1.1}/tests/test_asyncio_backend.py +0 -0
  113. {httpunk-0.1.0a1 → httpunk-0.1.1}/tests/test_asyncio_e2e.py +0 -0
  114. {httpunk-0.1.0a1 → httpunk-0.1.1}/tests/test_asyncio_protocol.py +0 -0
  115. {httpunk-0.1.0a1 → httpunk-0.1.1}/tests/test_backend.py +0 -0
  116. {httpunk-0.1.0a1 → httpunk-0.1.1}/tests/test_h1_codec.py +0 -0
  117. {httpunk-0.1.0a1 → httpunk-0.1.1}/tests/test_h1_conn.py +0 -0
  118. {httpunk-0.1.0a1 → httpunk-0.1.1}/tests/test_h1_decode.py +0 -0
  119. {httpunk-0.1.0a1 → httpunk-0.1.1}/tests/test_h2_codec.py +0 -0
  120. {httpunk-0.1.0a1 → httpunk-0.1.1}/tests/test_h2_conn.py +0 -0
  121. {httpunk-0.1.0a1 → httpunk-0.1.1}/tests/test_h2_errors.py +0 -0
  122. {httpunk-0.1.0a1 → httpunk-0.1.1}/tests/test_h2_flow.py +0 -0
  123. {httpunk-0.1.0a1 → httpunk-0.1.1}/tests/test_h2_flow_control.py +0 -0
  124. {httpunk-0.1.0a1 → httpunk-0.1.1}/tests/test_h2_ready.py +0 -0
  125. {httpunk-0.1.0a1 → httpunk-0.1.1}/tests/test_h2_server.py +0 -0
  126. {httpunk-0.1.0a1 → httpunk-0.1.1}/tests/test_h2_settings.py +0 -0
  127. {httpunk-0.1.0a1 → httpunk-0.1.1}/tests/test_h2_state.py +0 -0
  128. {httpunk-0.1.0a1 → httpunk-0.1.1}/tests/test_util_auto.py +0 -0
  129. {httpunk-0.1.0a1 → httpunk-0.1.1}/tests/test_util_connect.py +0 -0
  130. {httpunk-0.1.0a1 → httpunk-0.1.1}/tests/test_util_graceful.py +0 -0
  131. {httpunk-0.1.0a1 → httpunk-0.1.1}/tests/test_util_pool.py +0 -0
  132. {httpunk-0.1.0a1 → httpunk-0.1.1}/tests/test_util_proxy.py +0 -0
  133. {httpunk-0.1.0a1 → httpunk-0.1.1}/tests/test_util_tls.py +0 -0
@@ -16,9 +16,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
16
16
 
17
17
  [[package]]
18
18
  name = "bytes"
19
- version = "1.11.1"
19
+ version = "1.12.1"
20
20
  source = "registry+https://github.com/rust-lang/crates.io-index"
21
- checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
21
+ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
22
22
 
23
23
  [[package]]
24
24
  name = "equivalent"
@@ -86,9 +86,9 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
86
86
 
87
87
  [[package]]
88
88
  name = "http"
89
- version = "1.4.0"
89
+ version = "1.4.2"
90
90
  source = "registry+https://github.com/rust-lang/crates.io-index"
91
- checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a"
91
+ checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425"
92
92
  dependencies = [
93
93
  "bytes",
94
94
  "itoa",
@@ -118,7 +118,7 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
118
118
 
119
119
  [[package]]
120
120
  name = "httpunk"
121
- version = "0.1.0-alpha1"
121
+ version = "0.1.1"
122
122
  dependencies = [
123
123
  "bytes",
124
124
  "h2",
@@ -175,9 +175,9 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
175
175
 
176
176
  [[package]]
177
177
  name = "libc"
178
- version = "0.2.183"
178
+ version = "0.2.186"
179
179
  source = "registry+https://github.com/rust-lang/crates.io-index"
180
- checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d"
180
+ checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
181
181
 
182
182
  [[package]]
183
183
  name = "once_cell"
@@ -271,9 +271,9 @@ dependencies = [
271
271
 
272
272
  [[package]]
273
273
  name = "quote"
274
- version = "1.0.45"
274
+ version = "1.0.46"
275
275
  source = "registry+https://github.com/rust-lang/crates.io-index"
276
- checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
276
+ checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368"
277
277
  dependencies = [
278
278
  "proc-macro2",
279
279
  ]
@@ -292,9 +292,9 @@ checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90"
292
292
 
293
293
  [[package]]
294
294
  name = "syn"
295
- version = "2.0.117"
295
+ version = "2.0.118"
296
296
  source = "registry+https://github.com/rust-lang/crates.io-index"
297
- checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
297
+ checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422"
298
298
  dependencies = [
299
299
  "proc-macro2",
300
300
  "quote",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "httpunk"
3
- version = "0.1.0-alpha1"
3
+ version = "0.1.1"
4
4
  description = "The Rust HTTP library for Python"
5
5
  authors = [
6
6
  "Giovanni Barillari <g@baro.dev>",
@@ -30,8 +30,7 @@ include = [
30
30
 
31
31
  # The vendored third-party crates live in their own workspace members so each
32
32
  # owns its crate root (upstream `crate::` paths resolve with no rewrite → truly
33
- # byte-identical) and is excluded from the main crate's fmt/clippy. See
34
- # THIRD-PARTY.md and PLAN.md §4.
33
+ # byte-identical) and is excluded from the main crate's fmt/clippy.
35
34
  [workspace]
36
35
  members = ["crates/vendor-h2", "crates/vendor-hyper", "crates/vendor-hyper-util"]
37
36
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: httpunk
3
- Version: 0.1.0a1
3
+ Version: 0.1.1
4
4
  Classifier: Development Status :: 3 - Alpha
5
5
  Classifier: Intended Audience :: Developers
6
6
  Classifier: License :: OSI Approved :: BSD License
@@ -19,7 +19,8 @@ Classifier: Programming Language :: Python :: Implementation :: PyPy
19
19
  Classifier: Programming Language :: Python
20
20
  Classifier: Programming Language :: Rust
21
21
  Classifier: Topic :: Internet :: WWW/HTTP
22
- Requires-Dist: tonio>=0.7.1 ; python_full_version >= '3.14' and platform_system != 'Windows'
22
+ Requires-Dist: tonio~=0.8.1 ; python_full_version >= '3.14' and extra == 'tonio'
23
+ Provides-Extra: tonio
23
24
  License-File: LICENSE
24
25
  Summary: The Rust HTTP library for Python
25
26
  Keywords: http,h10,h11,h2
@@ -34,14 +35,16 @@ Project-URL: Source, https://github.com/gi0baro/httpunk
34
35
 
35
36
  # httpunk
36
37
 
37
- httpunk is a Rust-powered HTTP library for Python.
38
+ httpunk is a Rust-powered async HTTP library for Python.
38
39
  It's powered by the [hyper](https://github.com/hyperium/hyper) stack and Rust crates like [http](https://github.com/hyperium/http).
39
40
 
40
41
  httpunk is deliberately *low-level*: you bring your own connected transport, and
41
42
  build requests and read responses directly. It's meant to be a solid, performant
42
43
  base for building HTTP clients and servers on top of.
43
44
 
44
- > **Note:** httpunk is in an early, alpha stage. The API may change between releases.
45
+ httpunk's API mirrors hyper's wherever possible.
46
+
47
+ > **Note:** httpunk is in an early, alpha stage.
45
48
 
46
49
  > **Note:** httpunk was built with substantial help from LLMs, under human supervision.
47
50
 
@@ -99,16 +102,19 @@ asyncio.run(main())
99
102
  pip install httpunk
100
103
  ```
101
104
 
102
- The `asyncio` backend has no extra dependencies. The `tonio` backend pulls in
103
- [tonio](https://github.com/gi0baro/tonio) and is only installed automatically on free-threaded
104
- CPython 3.14+ on Unix systems.
105
+ httpunk supports additional backends beyond `asyncio`, but they require extra dependencies.
106
+ Enable one via the relevant extra:
107
+
108
+ ```
109
+ pip install httpunk[tonio]
110
+ ```
105
111
 
106
112
  ## Features
107
113
 
108
114
  - **HTTP/1 and HTTP/2**, client and server implementations
109
115
  - **Protocol-neutral structures** such as `Request`, `Response`, `HeaderMap`
110
116
  - **Multiple backend support**: `asyncio` and `tonio` (with `trio` targeted for future releases)
111
- - **AsyncIO ready-to-go protocols**: extensible `asyncio.Protocol` server classes (H1, H2, Auto)
117
+ - **AsyncIO ready-to-go protocols**: extensible `asyncio.Protocol` classes (H1, H2, Auto)
112
118
  - **Batteries in the `util` module**: connect and ALPN negotiation, h1/h2 auto-detection, connection pooling, graceful shutdown, proxy-environment matching.
113
119
 
114
120
  ## Usage
@@ -305,7 +311,7 @@ except HTTPunkError:
305
311
  ### Utilities
306
312
 
307
313
  `httpunk.util` collects the higher-level conveniences a real client/server host needs. Unlike
308
- the core, these carry no wire-protocol fidelity constraint — they mirror hyper-util's shapes.
314
+ the core, these carry no wire-protocol fidelity constraint.
309
315
 
310
316
  #### connect
311
317
 
@@ -367,10 +373,9 @@ All pools expose `retain(predicate)` (evict connections a predicate rejects), `i
367
373
 
368
374
  #### Graceful shutdown
369
375
 
370
- `GracefulShutdown` coordinates a graceful shutdown across many connections (hyper-util's
371
- `server::graceful::GracefulShutdown`). `watch(server, serve)` registers a connection and
372
- returns the coroutine that drives it; `shutdown()` signals every watched connection and waits
373
- for them to drain.
376
+ `GracefulShutdown` coordinates a graceful shutdown across many connections.
377
+ `watch(server, serve)` registers a connection and returns the coroutine that drives it;
378
+ `shutdown()` signals every watched connection and waits for them to drain.
374
379
 
375
380
  ```python
376
381
  from httpunk.util import GracefulShutdown
@@ -402,8 +407,10 @@ if intercept is not None:
402
407
 
403
408
  ### AsyncIO utilities
404
409
 
405
- `httpunk.asyncio` provides reusable `asyncio.Protocol` server classes so you can embed httpunk
406
- in any asyncio-based server. Just subclass one and implement `handle(request)`:
410
+ `httpunk.asyncio` provides reusable `asyncio.Protocol` classes so you
411
+ can embed httpunk in any asyncio program.
412
+
413
+ **Server protocols** — subclass one and implement `handle(request)`:
407
414
 
408
415
  - `H1ServerProtocol` / `H2ServerProtocol` — force the protocol.
409
416
  - `AutoServerProtocol` — detect HTTP/1 vs HTTP/2 from the client's opening bytes.
@@ -442,6 +449,38 @@ server.close() # stop accepting new connections
442
449
  await conns.shutdown(timeout=30) # drain in-flight, force-close stragglers
443
450
  ```
444
451
 
452
+ **Client protocols** — the mirror of the server ones, for `loop.create_connection`. Once the
453
+ connection is up, `await proto.ready()` returns the httpunk client connection to send requests
454
+ on. Configuration (`authority`/`scheme`) is passed via a factory closure, since
455
+ `create_connection` calls the factory with no arguments.
456
+
457
+ - `H1ClientProtocol` / `H2ClientProtocol` — force the protocol.
458
+ - `AutoClientProtocol` — pick HTTP/1 vs HTTP/2 from the TLS ALPN result (plain TCP → HTTP/1).
459
+
460
+ ```python
461
+ import asyncio
462
+ import ssl
463
+
464
+ import httpunk.asyncio
465
+
466
+
467
+ async def main():
468
+ loop = asyncio.get_running_loop()
469
+ ctx = ssl.create_default_context()
470
+ ctx.set_alpn_protocols(["h2", "http/1.1"])
471
+ transport, proto = await loop.create_connection(
472
+ lambda: httpunk.asyncio.H2ClientProtocol(authority="example.com:443", scheme="https"),
473
+ "example.com", 443, ssl=ctx, server_hostname="example.com",
474
+ )
475
+ conn = await proto.ready() # await handshake -> H2Connection
476
+ resp = await conn.request("GET", "/", headers={"host": "example.com"})
477
+ print(resp.status, await resp.read())
478
+ await proto.aclose()
479
+
480
+
481
+ asyncio.run(main())
482
+ ```
483
+
445
484
  ## License
446
485
 
447
486
  httpunk is released under the BSD 3-Clause License.
@@ -1,13 +1,15 @@
1
1
  # httpunk
2
2
 
3
- httpunk is a Rust-powered HTTP library for Python.
3
+ httpunk is a Rust-powered async HTTP library for Python.
4
4
  It's powered by the [hyper](https://github.com/hyperium/hyper) stack and Rust crates like [http](https://github.com/hyperium/http).
5
5
 
6
6
  httpunk is deliberately *low-level*: you bring your own connected transport, and
7
7
  build requests and read responses directly. It's meant to be a solid, performant
8
8
  base for building HTTP clients and servers on top of.
9
9
 
10
- > **Note:** httpunk is in an early, alpha stage. The API may change between releases.
10
+ httpunk's API mirrors hyper's wherever possible.
11
+
12
+ > **Note:** httpunk is in an early, alpha stage.
11
13
 
12
14
  > **Note:** httpunk was built with substantial help from LLMs, under human supervision.
13
15
 
@@ -65,16 +67,19 @@ asyncio.run(main())
65
67
  pip install httpunk
66
68
  ```
67
69
 
68
- The `asyncio` backend has no extra dependencies. The `tonio` backend pulls in
69
- [tonio](https://github.com/gi0baro/tonio) and is only installed automatically on free-threaded
70
- CPython 3.14+ on Unix systems.
70
+ httpunk supports additional backends beyond `asyncio`, but they require extra dependencies.
71
+ Enable one via the relevant extra:
72
+
73
+ ```
74
+ pip install httpunk[tonio]
75
+ ```
71
76
 
72
77
  ## Features
73
78
 
74
79
  - **HTTP/1 and HTTP/2**, client and server implementations
75
80
  - **Protocol-neutral structures** such as `Request`, `Response`, `HeaderMap`
76
81
  - **Multiple backend support**: `asyncio` and `tonio` (with `trio` targeted for future releases)
77
- - **AsyncIO ready-to-go protocols**: extensible `asyncio.Protocol` server classes (H1, H2, Auto)
82
+ - **AsyncIO ready-to-go protocols**: extensible `asyncio.Protocol` classes (H1, H2, Auto)
78
83
  - **Batteries in the `util` module**: connect and ALPN negotiation, h1/h2 auto-detection, connection pooling, graceful shutdown, proxy-environment matching.
79
84
 
80
85
  ## Usage
@@ -271,7 +276,7 @@ except HTTPunkError:
271
276
  ### Utilities
272
277
 
273
278
  `httpunk.util` collects the higher-level conveniences a real client/server host needs. Unlike
274
- the core, these carry no wire-protocol fidelity constraint — they mirror hyper-util's shapes.
279
+ the core, these carry no wire-protocol fidelity constraint.
275
280
 
276
281
  #### connect
277
282
 
@@ -333,10 +338,9 @@ All pools expose `retain(predicate)` (evict connections a predicate rejects), `i
333
338
 
334
339
  #### Graceful shutdown
335
340
 
336
- `GracefulShutdown` coordinates a graceful shutdown across many connections (hyper-util's
337
- `server::graceful::GracefulShutdown`). `watch(server, serve)` registers a connection and
338
- returns the coroutine that drives it; `shutdown()` signals every watched connection and waits
339
- for them to drain.
341
+ `GracefulShutdown` coordinates a graceful shutdown across many connections.
342
+ `watch(server, serve)` registers a connection and returns the coroutine that drives it;
343
+ `shutdown()` signals every watched connection and waits for them to drain.
340
344
 
341
345
  ```python
342
346
  from httpunk.util import GracefulShutdown
@@ -368,8 +372,10 @@ if intercept is not None:
368
372
 
369
373
  ### AsyncIO utilities
370
374
 
371
- `httpunk.asyncio` provides reusable `asyncio.Protocol` server classes so you can embed httpunk
372
- in any asyncio-based server. Just subclass one and implement `handle(request)`:
375
+ `httpunk.asyncio` provides reusable `asyncio.Protocol` classes so you
376
+ can embed httpunk in any asyncio program.
377
+
378
+ **Server protocols** — subclass one and implement `handle(request)`:
373
379
 
374
380
  - `H1ServerProtocol` / `H2ServerProtocol` — force the protocol.
375
381
  - `AutoServerProtocol` — detect HTTP/1 vs HTTP/2 from the client's opening bytes.
@@ -408,6 +414,38 @@ server.close() # stop accepting new connections
408
414
  await conns.shutdown(timeout=30) # drain in-flight, force-close stragglers
409
415
  ```
410
416
 
417
+ **Client protocols** — the mirror of the server ones, for `loop.create_connection`. Once the
418
+ connection is up, `await proto.ready()` returns the httpunk client connection to send requests
419
+ on. Configuration (`authority`/`scheme`) is passed via a factory closure, since
420
+ `create_connection` calls the factory with no arguments.
421
+
422
+ - `H1ClientProtocol` / `H2ClientProtocol` — force the protocol.
423
+ - `AutoClientProtocol` — pick HTTP/1 vs HTTP/2 from the TLS ALPN result (plain TCP → HTTP/1).
424
+
425
+ ```python
426
+ import asyncio
427
+ import ssl
428
+
429
+ import httpunk.asyncio
430
+
431
+
432
+ async def main():
433
+ loop = asyncio.get_running_loop()
434
+ ctx = ssl.create_default_context()
435
+ ctx.set_alpn_protocols(["h2", "http/1.1"])
436
+ transport, proto = await loop.create_connection(
437
+ lambda: httpunk.asyncio.H2ClientProtocol(authority="example.com:443", scheme="https"),
438
+ "example.com", 443, ssl=ctx, server_hostname="example.com",
439
+ )
440
+ conn = await proto.ready() # await handshake -> H2Connection
441
+ resp = await conn.request("GET", "/", headers={"host": "example.com"})
442
+ print(resp.status, await resp.read())
443
+ await proto.aclose()
444
+
445
+
446
+ asyncio.run(main())
447
+ ```
448
+
411
449
  ## License
412
450
 
413
451
  httpunk is released under the BSD 3-Clause License.
@@ -6,7 +6,7 @@
6
6
  //! frame parse/serialize) and `hpack` (HPACK header compression), plus the tiny
7
7
  //! `ext::Protocol` type they depend on. h2's async `codec`, `proto`, `client`
8
8
  //! and `server` are intentionally *not* vendored — that orchestration is
9
- //! rewritten in Python (see `PLAN.md`).
9
+ //! rewritten in Python.
10
10
  //!
11
11
  //! The `.rs` files under here are kept **byte-identical to upstream modulo a
12
12
  //! uniform `crate::` -> `crate::` rewrite** the vendor script applies, so
@@ -5,7 +5,7 @@
5
5
  //! support types they depend on (`headers`, `ext`, `error`, `body::length`,
6
6
  //! `proto::MessageHead`). hyper's async `conn`, `decode`, `io`, `dispatch`,
7
7
  //! `client`, `server` and `upgrade` are intentionally *not* vendored — that
8
- //! orchestration is rewritten in Python (see `PLAN.md`).
8
+ //! orchestration is rewritten in Python.
9
9
  //!
10
10
  //! The `.rs` files under here are kept **byte-identical to upstream** so a
11
11
  //! `git diff` between two vendored versions shows only genuine upstream changes.
@@ -25,8 +25,13 @@ class Backend(enum.Enum):
25
25
 
26
26
  def create(self) -> Any:
27
27
  if self is Backend.tonio:
28
- from .tonio import TonioBackend
29
-
28
+ try:
29
+ from .tonio import TonioBackend
30
+ except ImportError as exc:
31
+ raise ImportError(
32
+ "the tonio backend requires the 'tonio' package — install it with "
33
+ "`pip install httpunk[tonio]` (needs free-threaded CPython >= 3.14 on Unix)"
34
+ ) from exc
30
35
  return TonioBackend()
31
36
  from .asyncio import AsyncioBackend
32
37
 
@@ -1,10 +1,10 @@
1
1
  """asyncio backend: the runtime primitives the drivers need, on `asyncio`.
2
2
 
3
3
  The second backend (after tonio), validating that the drivers depend only on the
4
- seam (see PLAN §12). `import asyncio` below is the stdlib (absolute imports) — this
4
+ seam. `import asyncio` below is the stdlib (absolute imports) — this
5
5
  module is `httpunk._backend.asyncio`, not the top-level package.
6
6
 
7
- Approach B (PLAN §12.4): asyncio's TCP is transport/protocol-based and eagerly
7
+ asyncio's TCP is transport/protocol-based and eagerly
8
8
  drains the kernel socket into a userspace buffer, so — unlike tonio — a non-blocking
9
9
  `receive_nowait` must peek *that* buffer. Rather than lean on `StreamReader`'s
10
10
  private `_buffer`, we own a custom `asyncio.Protocol` (`_AsyncioStream`) that IS the
@@ -162,6 +162,12 @@ class _AsyncioStream(asyncio.Protocol):
162
162
  if waiter is not None and not waiter.done():
163
163
  waiter.set_result(None)
164
164
 
165
+ def interrupt_read(self):
166
+ """Wake a parked `receive_some` so it returns b"" — lets a graceful shutdown release
167
+ an idle keep-alive read WITHOUT racing a coroutine (asyncio can wake a parked read from
168
+ another task; tonio can't, which is why it uses a select-based race instead)."""
169
+ self._wake_reader()
170
+
165
171
 
166
172
  class _AsyncioScope:
167
173
  """A nursery over `asyncio` tasks, matching tonio's scope surface: `spawn`,
@@ -297,3 +303,18 @@ class AsyncioBackend:
297
303
  scope = _AsyncioScope
298
304
  monotonic = staticmethod(_time.monotonic)
299
305
  sleep = staticmethod(asyncio.sleep) # async sleep(seconds), for deadline races (`select`)
306
+
307
+ # Every backend implements `timeout` (above), so the h1 server always uses it for the
308
+ # head-read deadline. This extra capability is asyncio-only: `_AsyncioStream.interrupt_read`
309
+ # can wake a parked idle read from another task, so the h1 server can skip the per-read
310
+ # shutdown `select`. tonio can't (no such method) → it keeps the select there.
311
+ native_read_interrupt = True
312
+
313
+ async def timeout(self, coro, seconds):
314
+ """Run `coro` bounded by `seconds`; return `(result, True)` on completion, or
315
+ `(None, False)` on timeout (the coro is cancelled). One task + one timer, far cheaper
316
+ than racing a `sleep()` coroutine via `select`."""
317
+ try:
318
+ return await asyncio.wait_for(coro, seconds), True
319
+ except (TimeoutError, asyncio.TimeoutError):
320
+ return None, False
@@ -12,7 +12,7 @@ from tonio.colored.net import open_tcp_stream as _open_tcp_stream
12
12
  from tonio.colored.net.tls import open_tls_over_tcp_stream as _open_tls_over_tcp_stream
13
13
  from tonio.colored.sync import Lock as _Lock, Semaphore as _Semaphore
14
14
  from tonio.colored.sync.channel import unbounded as _unbounded
15
- from tonio.colored.time import time as _now
15
+ from tonio.colored.time import time as _now, timeout as _timeout
16
16
 
17
17
 
18
18
  class _TonioSemaphore:
@@ -107,4 +107,9 @@ class TonioBackend:
107
107
  semaphore = _TonioSemaphore
108
108
  queue = staticmethod(_unbounded)
109
109
  monotonic = staticmethod(_now)
110
- sleep = staticmethod(_colored.sleep) # async sleep(seconds), for deadline races (`select`)
110
+ sleep = staticmethod(_colored.sleep) # async sleep(seconds)
111
+ # `timeout(coro, seconds) -> (result, completed)` — tonio's native deadline; the h1 server
112
+ # uses it to bound the request-head read (measured ~12-16% faster than racing sleep() via
113
+ # select). Same contract as AsyncioBackend.timeout. tonio still can't wake a parked recv from
114
+ # another task, so it does NOT advertise `native_read_interrupt` (idle shutdown stays select).
115
+ timeout = staticmethod(_timeout)
@@ -1,11 +1,11 @@
1
- """Reusable asyncio server protocols — embed httpunk in any asyncio server (uvicorn,
2
- hypercorn, …) by subclassing one of these and implementing `handle(request)`.
1
+ """Reusable asyncio protocols — embed httpunk in any asyncio program. They are
2
+ `asyncio.Protocol` subclasses of the asyncio backend's `_AsyncioStream` (the byte
3
+ transport the driver reads/writes); the host owns the loop and hands the protocol a
4
+ transport. Unlike uvicorn's h11/httptools protocols (HTTP/1 only), these also bring **HTTP/2**.
3
5
 
4
- Unlike uvicorn's h11/httptools protocols (HTTP/1 only), these also bring **HTTP/2**.
5
- They are `asyncio.Protocol` subclasses of the asyncio backend's `_AsyncioStream`
6
- (approach B, PLAN §12.8): the host owns the loop and hands the protocol a transport
7
- via `loop.create_server(factory)`; the protocol drives httpunk's server over *itself*
8
- and calls your `handle` per request.
6
+ **Server** — subclass one and implement `handle(request)`, then hand it to
7
+ `loop.create_server(factory)`; the protocol drives httpunk's server over *itself* and calls
8
+ your `handle` per request:
9
9
 
10
10
  class MyServer(httpunk.asyncio.AutoServerProtocol):
11
11
  async def handle(self, request):
@@ -16,8 +16,19 @@ and calls your `handle` per request.
16
16
  async with server:
17
17
  await server.serve_forever()
18
18
 
19
- `H1ServerProtocol` / `H2ServerProtocol` force the protocol; `AutoServerProtocol` sniffs h1-vs-h2 from
20
- the client's opening bytes.
19
+ **Client** (the mirror) — hand one to `loop.create_connection(factory)`; once connected,
20
+ `await proto.ready()` returns the httpunk client connection to send requests on:
21
+
22
+ transport, proto = await loop.create_connection(
23
+ lambda: httpunk.asyncio.H2ClientProtocol(authority="example.com:443", scheme="https"),
24
+ "example.com", 443, ssl=ctx,
25
+ )
26
+ conn = await proto.ready()
27
+ resp = await conn.request("GET", "/", headers={"host": "example.com"})
28
+ await proto.aclose()
29
+
30
+ `H1*`/`H2*` force the protocol; `Auto*` picks h1-vs-h2 (server: from the client's opening
31
+ bytes; client: from the TLS ALPN result).
21
32
  """
22
33
 
23
34
  from __future__ import annotations
@@ -27,12 +38,22 @@ import contextlib
27
38
  from typing import Any, TypeVar
28
39
 
29
40
  from ._backend.asyncio import AsyncioBackend, _AsyncioStream
41
+ from .h1.client import H1Connection
30
42
  from .h1.server import H1Server
43
+ from .h2.client import H2Connection
31
44
  from .h2.server import H2Server
32
45
  from .util import auto
33
46
 
34
47
 
35
- __all__ = ["AutoServerProtocol", "H1ServerProtocol", "H2ServerProtocol", "ServerConnections"]
48
+ __all__ = [
49
+ "AutoClientProtocol",
50
+ "AutoServerProtocol",
51
+ "H1ClientProtocol",
52
+ "H1ServerProtocol",
53
+ "H2ClientProtocol",
54
+ "H2ServerProtocol",
55
+ "ServerConnections",
56
+ ]
36
57
 
37
58
  _ProtocolT = TypeVar("_ProtocolT", bound="_ServerProtocol")
38
59
 
@@ -229,3 +250,74 @@ class ServerConnections:
229
250
  if c._serve_task is not None:
230
251
  c._serve_task.cancel() # hard-cancel a stuck handler / accept loop
231
252
  await asyncio.wait(pending)
253
+
254
+
255
+ class _ClientProtocol(_AsyncioStream):
256
+ """Base for the reusable client protocols — the client-side mirror of `_ServerProtocol`.
257
+ `_AsyncioStream` is the transport the driver reads/writes; on `connection_made` this builds
258
+ an httpunk client connection over `self` and spawns its handshake. `ready()` awaits the
259
+ handshake and returns the connection facade to send requests on. Subclasses provide
260
+ `_make_client`. There is no `handle` — the client is caller-driven, not push."""
261
+
262
+ def __init__(self, *, authority: str | None = None, scheme: str | None = None) -> None:
263
+ super().__init__()
264
+ self._backend = AsyncioBackend() # it's an asyncio protocol -> the asyncio backend
265
+ self._authority = authority
266
+ self._scheme = scheme
267
+ # Built in connection_made; None until then (so aclose() before a dial is a no-op).
268
+ self._client: H1Connection | H2Connection | None = None
269
+ self._connect_task: asyncio.Task[object] | None = None
270
+
271
+ def connection_made(self, transport):
272
+ super().connection_made(transport)
273
+ self._client = self._make_client()
274
+ # Run the HTTP handshake (the facade's __aenter__ -> conn.connect(): preface + SETTINGS
275
+ # for h2, a no-op for h1) concurrently; `ready()` awaits it. Retrieve the task's result
276
+ # so a handshake failure doesn't surface as a bare "Task exception was never retrieved".
277
+ task = self._loop.create_task(self._client.__aenter__())
278
+ self._connect_task = task
279
+ task.add_done_callback(lambda t: t.cancelled() or t.exception())
280
+
281
+ def _make_client(self) -> H1Connection | H2Connection:
282
+ raise NotImplementedError
283
+
284
+ async def ready(self) -> H1Connection | H2Connection:
285
+ """Await the connection handshake, then return the ready client facade
286
+ (`H1Connection`/`H2Connection`) to send requests on."""
287
+ assert self._connect_task is not None and self._client is not None # set in connection_made
288
+ await self._connect_task
289
+ await self._client.ready()
290
+ return self._client
291
+
292
+ async def aclose(self) -> None:
293
+ """Close the connection — drops the read/write scopes and the transport. A no-op if the
294
+ connection was never established (`connection_made` not yet called)."""
295
+ if self._client is not None:
296
+ await self._client.__aexit__(None, None, None)
297
+
298
+
299
+ class H1ClientProtocol(_ClientProtocol):
300
+ """Speak HTTP/1 on this connection."""
301
+
302
+ def _make_client(self):
303
+ return H1Connection(self, authority=self._authority, backend=self._backend)
304
+
305
+
306
+ class H2ClientProtocol(_ClientProtocol):
307
+ """Speak HTTP/2 on this connection."""
308
+
309
+ def _make_client(self):
310
+ return H2Connection(self, authority=self._authority, scheme=self._scheme or "http", backend=self._backend)
311
+
312
+
313
+ class AutoClientProtocol(_ClientProtocol):
314
+ """Pick HTTP/1 or HTTP/2 from the TLS ALPN result — the client-side analogue of
315
+ `AutoServerProtocol`'s preface sniff. `h2` -> HTTP/2; anything else (including plain TCP
316
+ with no ALPN) -> HTTP/1, matching `httpunk.util.connect`."""
317
+
318
+ def _make_client(self):
319
+ ssl_object = self._transport.get_extra_info("ssl_object")
320
+ selected = ssl_object.selected_alpn_protocol() if ssl_object is not None else None
321
+ if selected == "h2":
322
+ return H2Connection(self, authority=self._authority, scheme=self._scheme or "https", backend=self._backend)
323
+ return H1Connection(self, authority=self._authority, backend=self._backend)