http-fields 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 (245) hide show
  1. http_fields-0.1.1/.gitignore +26 -0
  2. http_fields-0.1.1/CHANGELOG.md +50 -0
  3. http_fields-0.1.1/LICENSE.txt +16 -0
  4. http_fields-0.1.1/PKG-INFO +88 -0
  5. http_fields-0.1.1/README.md +60 -0
  6. http_fields-0.1.1/SECURITY.md +26 -0
  7. http_fields-0.1.1/docs/conf.py +45 -0
  8. http_fields-0.1.1/docs/explanation/design.md +77 -0
  9. http_fields-0.1.1/docs/how-to/custom-and-unknown-headers.md +51 -0
  10. http_fields-0.1.1/docs/how-to/parse-and-build-headers.md +108 -0
  11. http_fields-0.1.1/docs/how-to/structured-fields.md +75 -0
  12. http_fields-0.1.1/docs/index.md +44 -0
  13. http_fields-0.1.1/docs/reference/api.md +13 -0
  14. http_fields-0.1.1/docs/reference/headers.md +156 -0
  15. http_fields-0.1.1/docs/reference/structured-fields.md +61 -0
  16. http_fields-0.1.1/docs/tutorials/getting-started.md +99 -0
  17. http_fields-0.1.1/pyproject.toml +107 -0
  18. http_fields-0.1.1/src/http_fields/__init__.py +193 -0
  19. http_fields-0.1.1/src/http_fields/accept.py +37 -0
  20. http_fields-0.1.1/src/http_fields/acceptcharset.py +48 -0
  21. http_fields-0.1.1/src/http_fields/acceptencoding.py +42 -0
  22. http_fields-0.1.1/src/http_fields/acceptlanguage.py +47 -0
  23. http_fields-0.1.1/src/http_fields/acceptranges.py +35 -0
  24. http_fields-0.1.1/src/http_fields/accesscontrol.py +197 -0
  25. http_fields-0.1.1/src/http_fields/age.py +34 -0
  26. http_fields-0.1.1/src/http_fields/allow.py +46 -0
  27. http_fields-0.1.1/src/http_fields/altsvc.py +70 -0
  28. http_fields-0.1.1/src/http_fields/authbases.py +79 -0
  29. http_fields-0.1.1/src/http_fields/authenticationinfo.py +19 -0
  30. http_fields-0.1.1/src/http_fields/authorization.py +23 -0
  31. http_fields-0.1.1/src/http_fields/cachecontrol.py +119 -0
  32. http_fields-0.1.1/src/http_fields/cachestatus.py +12 -0
  33. http_fields-0.1.1/src/http_fields/connection.py +35 -0
  34. http_fields-0.1.1/src/http_fields/contentdigest.py +19 -0
  35. http_fields-0.1.1/src/http_fields/contentdisposition.py +66 -0
  36. http_fields-0.1.1/src/http_fields/contentencoding.py +45 -0
  37. http_fields-0.1.1/src/http_fields/contentlanguage.py +38 -0
  38. http_fields-0.1.1/src/http_fields/contentlength.py +34 -0
  39. http_fields-0.1.1/src/http_fields/contentlocation.py +24 -0
  40. http_fields-0.1.1/src/http_fields/contentrange.py +74 -0
  41. http_fields-0.1.1/src/http_fields/contenttype.py +104 -0
  42. http_fields-0.1.1/src/http_fields/cookie.py +38 -0
  43. http_fields-0.1.1/src/http_fields/date.py +17 -0
  44. http_fields-0.1.1/src/http_fields/dateheader.py +32 -0
  45. http_fields-0.1.1/src/http_fields/entitytaglist.py +39 -0
  46. http_fields-0.1.1/src/http_fields/etag.py +46 -0
  47. http_fields-0.1.1/src/http_fields/expect.py +52 -0
  48. http_fields-0.1.1/src/http_fields/expires.py +17 -0
  49. http_fields-0.1.1/src/http_fields/forwarded.py +36 -0
  50. http_fields-0.1.1/src/http_fields/fromheader.py +42 -0
  51. http_fields-0.1.1/src/http_fields/header.py +250 -0
  52. http_fields-0.1.1/src/http_fields/host.py +44 -0
  53. http_fields-0.1.1/src/http_fields/ifmatch.py +24 -0
  54. http_fields-0.1.1/src/http_fields/ifmodifiedsince.py +17 -0
  55. http_fields-0.1.1/src/http_fields/ifnonematch.py +24 -0
  56. http_fields-0.1.1/src/http_fields/ifrange.py +49 -0
  57. http_fields-0.1.1/src/http_fields/ifunmodifiedsince.py +17 -0
  58. http_fields-0.1.1/src/http_fields/lastmodified.py +17 -0
  59. http_fields-0.1.1/src/http_fields/link.py +35 -0
  60. http_fields-0.1.1/src/http_fields/location.py +24 -0
  61. http_fields-0.1.1/src/http_fields/maxforwards.py +32 -0
  62. http_fields-0.1.1/src/http_fields/origin.py +42 -0
  63. http_fields-0.1.1/src/http_fields/parsedobjs.py +62 -0
  64. http_fields-0.1.1/src/http_fields/prefer.py +64 -0
  65. http_fields-0.1.1/src/http_fields/priority.py +55 -0
  66. http_fields-0.1.1/src/http_fields/productlistheader.py +33 -0
  67. http_fields-0.1.1/src/http_fields/proxyauthenticate.py +17 -0
  68. http_fields-0.1.1/src/http_fields/proxyauthenticationinfo.py +17 -0
  69. http_fields-0.1.1/src/http_fields/proxyauthorization.py +17 -0
  70. http_fields-0.1.1/src/http_fields/proxystatus.py +12 -0
  71. http_fields-0.1.1/src/http_fields/py.typed +0 -0
  72. http_fields-0.1.1/src/http_fields/range.py +37 -0
  73. http_fields-0.1.1/src/http_fields/referer.py +24 -0
  74. http_fields-0.1.1/src/http_fields/retryafter.py +45 -0
  75. http_fields-0.1.1/src/http_fields/server.py +18 -0
  76. http_fields-0.1.1/src/http_fields/setcookie.py +419 -0
  77. http_fields-0.1.1/src/http_fields/stricttransportsecurity.py +96 -0
  78. http_fields-0.1.1/src/http_fields/structuredfields.py +356 -0
  79. http_fields-0.1.1/src/http_fields/structuredheaders.py +90 -0
  80. http_fields-0.1.1/src/http_fields/te.py +36 -0
  81. http_fields-0.1.1/src/http_fields/trailer.py +44 -0
  82. http_fields-0.1.1/src/http_fields/upgrade.py +35 -0
  83. http_fields-0.1.1/src/http_fields/uriheader.py +36 -0
  84. http_fields-0.1.1/src/http_fields/useragent.py +18 -0
  85. http_fields-0.1.1/src/http_fields/vary.py +40 -0
  86. http_fields-0.1.1/src/http_fields/via.py +35 -0
  87. http_fields-0.1.1/src/http_fields/visitors/__init__.py +1 -0
  88. http_fields-0.1.1/src/http_fields/visitors/rfc3986.py +195 -0
  89. http_fields-0.1.1/src/http_fields/visitors/rfc6265.py +62 -0
  90. http_fields-0.1.1/src/http_fields/visitors/rfc6266.py +234 -0
  91. http_fields-0.1.1/src/http_fields/visitors/rfc7230.py +80 -0
  92. http_fields-0.1.1/src/http_fields/visitors/rfc7239.py +43 -0
  93. http_fields-0.1.1/src/http_fields/visitors/rfc7240.py +77 -0
  94. http_fields-0.1.1/src/http_fields/visitors/rfc7838.py +77 -0
  95. http_fields-0.1.1/src/http_fields/visitors/rfc8288.py +56 -0
  96. http_fields-0.1.1/src/http_fields/visitors/rfc9110/__init__.py +117 -0
  97. http_fields-0.1.1/src/http_fields/visitors/rfc9110/_base.py +71 -0
  98. http_fields-0.1.1/src/http_fields/visitors/rfc9110/accept.py +135 -0
  99. http_fields-0.1.1/src/http_fields/visitors/rfc9110/acceptcharset.py +26 -0
  100. http_fields-0.1.1/src/http_fields/visitors/rfc9110/acceptencoding.py +76 -0
  101. http_fields-0.1.1/src/http_fields/visitors/rfc9110/acceptlanguage.py +55 -0
  102. http_fields-0.1.1/src/http_fields/visitors/rfc9110/acceptranges.py +13 -0
  103. http_fields-0.1.1/src/http_fields/visitors/rfc9110/allow.py +13 -0
  104. http_fields-0.1.1/src/http_fields/visitors/rfc9110/authenticationinfo.py +19 -0
  105. http_fields-0.1.1/src/http_fields/visitors/rfc9110/authorization.py +77 -0
  106. http_fields-0.1.1/src/http_fields/visitors/rfc9110/authparam.py +40 -0
  107. http_fields-0.1.1/src/http_fields/visitors/rfc9110/authscheme.py +17 -0
  108. http_fields-0.1.1/src/http_fields/visitors/rfc9110/challenge.py +57 -0
  109. http_fields-0.1.1/src/http_fields/visitors/rfc9110/comment.py +99 -0
  110. http_fields-0.1.1/src/http_fields/visitors/rfc9110/connection.py +13 -0
  111. http_fields-0.1.1/src/http_fields/visitors/rfc9110/contentlanguage.py +21 -0
  112. http_fields-0.1.1/src/http_fields/visitors/rfc9110/contentlength.py +9 -0
  113. http_fields-0.1.1/src/http_fields/visitors/rfc9110/contentrange.py +70 -0
  114. http_fields-0.1.1/src/http_fields/visitors/rfc9110/contenttype.py +52 -0
  115. http_fields-0.1.1/src/http_fields/visitors/rfc9110/date.py +12 -0
  116. http_fields-0.1.1/src/http_fields/visitors/rfc9110/entitytag.py +66 -0
  117. http_fields-0.1.1/src/http_fields/visitors/rfc9110/etag.py +12 -0
  118. http_fields-0.1.1/src/http_fields/visitors/rfc9110/field.py +15 -0
  119. http_fields-0.1.1/src/http_fields/visitors/rfc9110/host.py +32 -0
  120. http_fields-0.1.1/src/http_fields/visitors/rfc9110/httpdate.py +167 -0
  121. http_fields-0.1.1/src/http_fields/visitors/rfc9110/ifmatch.py +24 -0
  122. http_fields-0.1.1/src/http_fields/visitors/rfc9110/ifmodifiedsince.py +12 -0
  123. http_fields-0.1.1/src/http_fields/visitors/rfc9110/ifnonematch.py +26 -0
  124. http_fields-0.1.1/src/http_fields/visitors/rfc9110/ifunmodifiedsince.py +12 -0
  125. http_fields-0.1.1/src/http_fields/visitors/rfc9110/lastmodified.py +14 -0
  126. http_fields-0.1.1/src/http_fields/visitors/rfc9110/location.py +9 -0
  127. http_fields-0.1.1/src/http_fields/visitors/rfc9110/parameters.py +124 -0
  128. http_fields-0.1.1/src/http_fields/visitors/rfc9110/product.py +39 -0
  129. http_fields-0.1.1/src/http_fields/visitors/rfc9110/quotedstring.py +66 -0
  130. http_fields-0.1.1/src/http_fields/visitors/rfc9110/range.py +100 -0
  131. http_fields-0.1.1/src/http_fields/visitors/rfc9110/rangeunit.py +17 -0
  132. http_fields-0.1.1/src/http_fields/visitors/rfc9110/retryafter.py +20 -0
  133. http_fields-0.1.1/src/http_fields/visitors/rfc9110/server.py +16 -0
  134. http_fields-0.1.1/src/http_fields/visitors/rfc9110/te.py +83 -0
  135. http_fields-0.1.1/src/http_fields/visitors/rfc9110/token.py +18 -0
  136. http_fields-0.1.1/src/http_fields/visitors/rfc9110/token68.py +18 -0
  137. http_fields-0.1.1/src/http_fields/visitors/rfc9110/type.py +17 -0
  138. http_fields-0.1.1/src/http_fields/visitors/rfc9110/upgrade.py +57 -0
  139. http_fields-0.1.1/src/http_fields/visitors/rfc9110/useragent.py +16 -0
  140. http_fields-0.1.1/src/http_fields/visitors/rfc9110/vary.py +10 -0
  141. http_fields-0.1.1/src/http_fields/visitors/rfc9110/via.py +86 -0
  142. http_fields-0.1.1/src/http_fields/visitors/rfc9110/weight.py +40 -0
  143. http_fields-0.1.1/src/http_fields/visitors/rfc9110/wwwauthenticate.py +21 -0
  144. http_fields-0.1.1/src/http_fields/visitors/rfc9111.py +92 -0
  145. http_fields-0.1.1/src/http_fields/wwwauthenticate.py +24 -0
  146. http_fields-0.1.1/tests/__init__.py +0 -0
  147. http_fields-0.1.1/tests/test_accept.py +45 -0
  148. http_fields-0.1.1/tests/test_acceptcharset.py +14 -0
  149. http_fields-0.1.1/tests/test_acceptencoding.py +22 -0
  150. http_fields-0.1.1/tests/test_acceptlanguage.py +18 -0
  151. http_fields-0.1.1/tests/test_acceptranges.py +25 -0
  152. http_fields-0.1.1/tests/test_accesscontrol.py +76 -0
  153. http_fields-0.1.1/tests/test_age.py +45 -0
  154. http_fields-0.1.1/tests/test_allow.py +24 -0
  155. http_fields-0.1.1/tests/test_altsvc.py +26 -0
  156. http_fields-0.1.1/tests/test_authenticationinfo.py +19 -0
  157. http_fields-0.1.1/tests/test_authorization.py +36 -0
  158. http_fields-0.1.1/tests/test_cachecontrol.py +71 -0
  159. http_fields-0.1.1/tests/test_cachestatus.py +39 -0
  160. http_fields-0.1.1/tests/test_connection.py +28 -0
  161. http_fields-0.1.1/tests/test_contentdigest.py +45 -0
  162. http_fields-0.1.1/tests/test_contentdisposition.py +105 -0
  163. http_fields-0.1.1/tests/test_contentencoding.py +18 -0
  164. http_fields-0.1.1/tests/test_contentlanguage.py +14 -0
  165. http_fields-0.1.1/tests/test_contentlength.py +27 -0
  166. http_fields-0.1.1/tests/test_contentlocation.py +12 -0
  167. http_fields-0.1.1/tests/test_contentrange.py +45 -0
  168. http_fields-0.1.1/tests/test_contenttype.py +107 -0
  169. http_fields-0.1.1/tests/test_cookie.py +54 -0
  170. http_fields-0.1.1/tests/test_date.py +51 -0
  171. http_fields-0.1.1/tests/test_etag.py +42 -0
  172. http_fields-0.1.1/tests/test_expect.py +11 -0
  173. http_fields-0.1.1/tests/test_expires.py +18 -0
  174. http_fields-0.1.1/tests/test_forwarded.py +22 -0
  175. http_fields-0.1.1/tests/test_from.py +18 -0
  176. http_fields-0.1.1/tests/test_header.py +66 -0
  177. http_fields-0.1.1/tests/test_host.py +48 -0
  178. http_fields-0.1.1/tests/test_ifmatch.py +18 -0
  179. http_fields-0.1.1/tests/test_ifmodifiedsince.py +18 -0
  180. http_fields-0.1.1/tests/test_ifnonematch.py +18 -0
  181. http_fields-0.1.1/tests/test_ifrange.py +30 -0
  182. http_fields-0.1.1/tests/test_ifunmodifiedsince.py +18 -0
  183. http_fields-0.1.1/tests/test_injection.py +180 -0
  184. http_fields-0.1.1/tests/test_lastmodified.py +18 -0
  185. http_fields-0.1.1/tests/test_limits.py +29 -0
  186. http_fields-0.1.1/tests/test_link.py +17 -0
  187. http_fields-0.1.1/tests/test_location.py +32 -0
  188. http_fields-0.1.1/tests/test_maxforwards.py +22 -0
  189. http_fields-0.1.1/tests/test_origin.py +16 -0
  190. http_fields-0.1.1/tests/test_plausibles.py +112 -0
  191. http_fields-0.1.1/tests/test_prefer.py +27 -0
  192. http_fields-0.1.1/tests/test_priority.py +29 -0
  193. http_fields-0.1.1/tests/test_proxyauth.py +40 -0
  194. http_fields-0.1.1/tests/test_range.py +44 -0
  195. http_fields-0.1.1/tests/test_referer.py +12 -0
  196. http_fields-0.1.1/tests/test_retryafter.py +57 -0
  197. http_fields-0.1.1/tests/test_roundtrip.py +117 -0
  198. http_fields-0.1.1/tests/test_server.py +10 -0
  199. http_fields-0.1.1/tests/test_setcookie.py +290 -0
  200. http_fields-0.1.1/tests/test_stricttransportsecurity.py +48 -0
  201. http_fields-0.1.1/tests/test_structuredfields.py +134 -0
  202. http_fields-0.1.1/tests/test_structuredfields_properties.py +150 -0
  203. http_fields-0.1.1/tests/test_te.py +20 -0
  204. http_fields-0.1.1/tests/test_trailer.py +18 -0
  205. http_fields-0.1.1/tests/test_upgrade.py +16 -0
  206. http_fields-0.1.1/tests/test_useragent.py +35 -0
  207. http_fields-0.1.1/tests/test_value_cache.py +33 -0
  208. http_fields-0.1.1/tests/test_vary.py +29 -0
  209. http_fields-0.1.1/tests/test_via.py +22 -0
  210. http_fields-0.1.1/tests/test_wwwauthenticate.py +34 -0
  211. http_fields-0.1.1/tests/visitors/__init__.py +0 -0
  212. http_fields-0.1.1/tests/visitors/rfc9110/__iinit__.py +0 -0
  213. http_fields-0.1.1/tests/visitors/rfc9110/test_accept.py +99 -0
  214. http_fields-0.1.1/tests/visitors/rfc9110/test_acceptcharset.py +13 -0
  215. http_fields-0.1.1/tests/visitors/rfc9110/test_acceptencoding.py +32 -0
  216. http_fields-0.1.1/tests/visitors/rfc9110/test_acceptlanguage.py +19 -0
  217. http_fields-0.1.1/tests/visitors/rfc9110/test_authenticationinfo.py +18 -0
  218. http_fields-0.1.1/tests/visitors/rfc9110/test_authorization.py +80 -0
  219. http_fields-0.1.1/tests/visitors/rfc9110/test_authparam.py +49 -0
  220. http_fields-0.1.1/tests/visitors/rfc9110/test_authscheme.py +14 -0
  221. http_fields-0.1.1/tests/visitors/rfc9110/test_base.py +31 -0
  222. http_fields-0.1.1/tests/visitors/rfc9110/test_challenge.py +72 -0
  223. http_fields-0.1.1/tests/visitors/rfc9110/test_comment.py +21 -0
  224. http_fields-0.1.1/tests/visitors/rfc9110/test_connection.py +11 -0
  225. http_fields-0.1.1/tests/visitors/rfc9110/test_contentlength.py +11 -0
  226. http_fields-0.1.1/tests/visitors/rfc9110/test_contentrange.py +40 -0
  227. http_fields-0.1.1/tests/visitors/rfc9110/test_etag.py +17 -0
  228. http_fields-0.1.1/tests/visitors/rfc9110/test_host.py +17 -0
  229. http_fields-0.1.1/tests/visitors/rfc9110/test_httpdate.py +111 -0
  230. http_fields-0.1.1/tests/visitors/rfc9110/test_ifmatch.py +20 -0
  231. http_fields-0.1.1/tests/visitors/rfc9110/test_ifnonematch.py +20 -0
  232. http_fields-0.1.1/tests/visitors/rfc9110/test_parameters.py +50 -0
  233. http_fields-0.1.1/tests/visitors/rfc9110/test_product.py +6 -0
  234. http_fields-0.1.1/tests/visitors/rfc9110/test_quotedstring.py +47 -0
  235. http_fields-0.1.1/tests/visitors/rfc9110/test_token.py +25 -0
  236. http_fields-0.1.1/tests/visitors/rfc9110/test_token68.py +24 -0
  237. http_fields-0.1.1/tests/visitors/rfc9110/test_useragent.py +15 -0
  238. http_fields-0.1.1/tests/visitors/rfc9110/test_weight.py +18 -0
  239. http_fields-0.1.1/tests/visitors/test_entitytag.py +56 -0
  240. http_fields-0.1.1/tests/visitors/test_misc.py +30 -0
  241. http_fields-0.1.1/tests/visitors/test_rfc3986.py +30 -0
  242. http_fields-0.1.1/tests/visitors/test_rfc6265.py +14 -0
  243. http_fields-0.1.1/tests/visitors/test_rfc6266.py +203 -0
  244. http_fields-0.1.1/tests/visitors/test_rfc7230.py +70 -0
  245. http_fields-0.1.1/tests/visitors/test_rfc9111.py +67 -0
@@ -0,0 +1,26 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+
5
+ # Build artifacts
6
+ build/
7
+ dist/
8
+ *.egg-info/
9
+ docs/_build/
10
+
11
+ # Test / type-check caches
12
+ .coverage
13
+ .pytest_cache/
14
+ .mypy_cache/
15
+
16
+ # Environments
17
+ .venv/
18
+
19
+ # Release artifacts (generated by the release workflow / its local dry-run)
20
+ sbom/
21
+ requirements-runtime.txt
22
+
23
+ # Editor / OS
24
+ .vscode/
25
+ *.code-workspace
26
+ .DS_Store
@@ -0,0 +1,50 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here. The format is based on
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This project is in its 0.x series;
5
+ the API may still change between minor versions.
6
+
7
+ ## [0.1.1] - 2026-07-22
8
+
9
+ Initial release published to PyPI as **http-fields** (`import http_fields`).
10
+
11
+ (`0.1.0` was tagged but never published to PyPI: its package metadata was missing a summary
12
+ and carried a `Development Status :: 5 - Production/Stable` classifier that overstated the
13
+ stability of a 0.x release. Caught on TestPyPI before the PyPI upload.)
14
+
15
+ ### Added
16
+
17
+ - A typed, immutable model of HTTP header fields: each header is a frozen dataclass whose fields
18
+ are the header's structured components, parsed and validated against the relevant RFC ABNF
19
+ grammar — RFC 9110/9111, 6265, 6266, widely-deployed extensions (Forwarded, CORS, Link, HSTS,
20
+ Alt-Svc, Prefer), and the RFC 9651 Structured Fields headers.
21
+ - **One construction contract.** `Header.parse(value)` is the only entry point that accepts a raw
22
+ string; constructors take each field as its already-parsed type, so a constructor never re-runs
23
+ the grammar on untrusted text. Passing a raw string where a typed field is expected raises
24
+ `TypeError` pointing at `parse()`. Value objects (`Protocol`, `LinkValue`, `AltValue`,
25
+ `ForwardedElement`, `Preference`, `Host`, …) accept plain strings for their individual parts and
26
+ coerce them to validated leaf types.
27
+ - Self-validating leaf types, so an invalid value cannot be held at all: `Token`, `Method`,
28
+ `FieldName`, `RangeUnit`, `LanguageTag`, `CorsMethod`/`CorsFieldName`, `Expectation`,
29
+ `Hostname`, `ProtocolName`/`ProtocolVersion`, `ReceivedProtocol`/`ReceivedBy`/`Comment`,
30
+ `URIReference`, `ProtocolId`/`AltAuthority`, and a shared optional-valued `Param`.
31
+ - An annotation-driven runtime field-type check on every header, applied uniformly with no
32
+ per-class configuration.
33
+ - A standalone RFC 9651 Structured Fields implementation (Item / List / Dictionary).
34
+
35
+ ### Security
36
+
37
+ - Every construction path — not just `parse()` — rejects CR/LF/NUL and other invalid content, so a
38
+ header built from partially attacker-controlled data cannot be used for response splitting /
39
+ header injection. `parse()` input is length-bounded to limit CPU exhaustion from pathological
40
+ input.
41
+
42
+ ### Performance
43
+
44
+ - A header is parsed **at most once**: visitors build field values as trusted leaf types, skipping
45
+ a re-parse of text the grammar just validated. Structured-Fields headers validate by serializing
46
+ rather than re-parsing.
47
+ - Each header's serialized `value` (and `str()`/`bytes()`/`asgi_value`) is computed once and cached
48
+ per instance.
49
+
50
+ [0.1.1]: https://github.com/declaresub/http-fields/releases/tag/v0.1.1
@@ -0,0 +1,16 @@
1
+ Copyright 2022 Charles Yeomans
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software
4
+ and associated documentation files (the "Software"), to deal in the Software without
5
+ restriction, including without limitation the rights to use, copy, modify, merge, publish,
6
+ distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
7
+ Software is furnished to do so, subject to the following conditions:
8
+
9
+ The above copyright notice and this permission notice shall be included in all copies or
10
+ substantial portions of the Software.
11
+
12
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
13
+ BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
14
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
15
+ DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
16
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,88 @@
1
+ Metadata-Version: 2.4
2
+ Name: http-fields
3
+ Version: 0.1.1
4
+ Summary: Typed, immutable HTTP header fields for Python, parsed and validated against RFC ABNF grammars
5
+ Project-URL: Homepage, https://github.com/declaresub/http-fields
6
+ Project-URL: Documentation, https://http-fields.readthedocs.io/
7
+ Project-URL: Repository, https://github.com/declaresub/http-fields
8
+ Project-URL: Changelog, https://github.com/declaresub/http-fields/blob/master/CHANGELOG.md
9
+ Project-URL: Issues, https://github.com/declaresub/http-fields/issues
10
+ Author-email: Charles Yeomans <charles@declaresub.com>
11
+ License-Expression: MIT
12
+ License-File: LICENSE.txt
13
+ Keywords: abnf,generator,headers,http,parser
14
+ Classifier: Development Status :: 4 - Beta
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: Programming Language :: Python
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Programming Language :: Python :: 3.14
23
+ Classifier: Topic :: Internet :: WWW/HTTP
24
+ Requires-Python: >=3.10
25
+ Requires-Dist: abnf>=2.6.0
26
+ Requires-Dist: typing-extensions>=4.0
27
+ Description-Content-Type: text/markdown
28
+
29
+ # http-fields
30
+
31
+ Typed, validated HTTP headers for Python. Each header is an immutable
32
+ [dataclass](https://docs.python.org/3/library/dataclasses.html) whose fields are the structured
33
+ components of the header. [abnf](https://pypi.org/project/abnf/) grammars are used both to
34
+ **parse** incoming header strings and to **validate** field values, so a constructed header is
35
+ always well-formed.
36
+
37
+ Requires Python 3.10+.
38
+
39
+ ```python
40
+ from http_fields import ContentType, Header
41
+
42
+ ct = ContentType.parse("text/html; charset=UTF-8")
43
+ ct.type, ct.subtype, ct.charset # ('text', 'html', 'UTF-8')
44
+ str(ct) # 'Content-Type: text/html;charset=UTF-8'
45
+
46
+ ContentType.of(type="text", subtype="html", charset="utf-8") # build from pieces
47
+ Header.create("x-request-id", "abc123") # dispatch by name
48
+ ```
49
+
50
+ ## Install
51
+
52
+ ```sh
53
+ uv add http-fields
54
+ # or: pip install http-fields
55
+ ```
56
+
57
+ ## Documentation
58
+
59
+ Full documentation: **<https://http-fields.readthedocs.io/>**
60
+
61
+ It follows the [Diátaxis](https://diataxis.fr/) model:
62
+
63
+ - **Tutorial:**
64
+ [Getting started](https://http-fields.readthedocs.io/en/latest/tutorials/getting-started.html)
65
+ - **How-to guides:**
66
+ [parse & build](https://http-fields.readthedocs.io/en/latest/how-to/parse-and-build-headers.html) ·
67
+ [custom / unknown headers](https://http-fields.readthedocs.io/en/latest/how-to/custom-and-unknown-headers.html) ·
68
+ [Structured Fields](https://http-fields.readthedocs.io/en/latest/how-to/structured-fields.html)
69
+ - **Reference:**
70
+ [header catalog](https://http-fields.readthedocs.io/en/latest/reference/headers.html) ·
71
+ [`structuredfields` API](https://http-fields.readthedocs.io/en/latest/reference/structured-fields.html)
72
+ - **Explanation:**
73
+ [the header model](https://http-fields.readthedocs.io/en/latest/explanation/design.html)
74
+
75
+ The sources live in [`docs/`](docs/index.md).
76
+
77
+ ## Development
78
+
79
+ ```sh
80
+ uv sync # create the environment
81
+ uv run pytest # run the tests
82
+ uv run ruff check . # lint
83
+ uv run ruff format . # format
84
+ uv run basedpyright # type-check
85
+
86
+ uv sync --group docs # add the docs toolchain
87
+ uv run sphinx-build -b html docs docs/_build/html # build the docs locally
88
+ ```
@@ -0,0 +1,60 @@
1
+ # http-fields
2
+
3
+ Typed, validated HTTP headers for Python. Each header is an immutable
4
+ [dataclass](https://docs.python.org/3/library/dataclasses.html) whose fields are the structured
5
+ components of the header. [abnf](https://pypi.org/project/abnf/) grammars are used both to
6
+ **parse** incoming header strings and to **validate** field values, so a constructed header is
7
+ always well-formed.
8
+
9
+ Requires Python 3.10+.
10
+
11
+ ```python
12
+ from http_fields import ContentType, Header
13
+
14
+ ct = ContentType.parse("text/html; charset=UTF-8")
15
+ ct.type, ct.subtype, ct.charset # ('text', 'html', 'UTF-8')
16
+ str(ct) # 'Content-Type: text/html;charset=UTF-8'
17
+
18
+ ContentType.of(type="text", subtype="html", charset="utf-8") # build from pieces
19
+ Header.create("x-request-id", "abc123") # dispatch by name
20
+ ```
21
+
22
+ ## Install
23
+
24
+ ```sh
25
+ uv add http-fields
26
+ # or: pip install http-fields
27
+ ```
28
+
29
+ ## Documentation
30
+
31
+ Full documentation: **<https://http-fields.readthedocs.io/>**
32
+
33
+ It follows the [Diátaxis](https://diataxis.fr/) model:
34
+
35
+ - **Tutorial:**
36
+ [Getting started](https://http-fields.readthedocs.io/en/latest/tutorials/getting-started.html)
37
+ - **How-to guides:**
38
+ [parse & build](https://http-fields.readthedocs.io/en/latest/how-to/parse-and-build-headers.html) ·
39
+ [custom / unknown headers](https://http-fields.readthedocs.io/en/latest/how-to/custom-and-unknown-headers.html) ·
40
+ [Structured Fields](https://http-fields.readthedocs.io/en/latest/how-to/structured-fields.html)
41
+ - **Reference:**
42
+ [header catalog](https://http-fields.readthedocs.io/en/latest/reference/headers.html) ·
43
+ [`structuredfields` API](https://http-fields.readthedocs.io/en/latest/reference/structured-fields.html)
44
+ - **Explanation:**
45
+ [the header model](https://http-fields.readthedocs.io/en/latest/explanation/design.html)
46
+
47
+ The sources live in [`docs/`](docs/index.md).
48
+
49
+ ## Development
50
+
51
+ ```sh
52
+ uv sync # create the environment
53
+ uv run pytest # run the tests
54
+ uv run ruff check . # lint
55
+ uv run ruff format . # format
56
+ uv run basedpyright # type-check
57
+
58
+ uv sync --group docs # add the docs toolchain
59
+ uv run sphinx-build -b html docs docs/_build/html # build the docs locally
60
+ ```
@@ -0,0 +1,26 @@
1
+ # Security Policy
2
+
3
+ `http-fields` parses and validates HTTP header values, often from untrusted input, so
4
+ correctness of that validation is a security property (e.g. rejecting CR/LF/NUL to prevent
5
+ response splitting / header injection).
6
+
7
+ ## Reporting a vulnerability
8
+
9
+ Please report suspected vulnerabilities **privately**, not via a public issue:
10
+
11
+ - Preferred: GitHub's **"Report a vulnerability"** button on the repository's *Security* tab
12
+ (private vulnerability reporting).
13
+ - Alternatively, email the maintainer at **charles@declaresub.com**.
14
+
15
+ Please include a description, affected version/commit, and a minimal reproduction. You'll get
16
+ an acknowledgement, and we'll coordinate a fix and disclosure with you.
17
+
18
+ ## Supported versions
19
+
20
+ This project is **pre-release** (no tagged release yet); the latest `master` is the supported
21
+ version. Once releases are published, this section will list the supported release lines.
22
+
23
+ ## Verifying a release
24
+
25
+ Once releases are published to PyPI, this section will describe how to verify a release's
26
+ provenance (build attestations / Sigstore bundle and the attached SBOM).
@@ -0,0 +1,45 @@
1
+ # Sphinx configuration. Prose lives in Markdown; MyST-Parser reads it directly.
2
+ from importlib.metadata import PackageNotFoundError
3
+ from importlib.metadata import version as _version
4
+
5
+ project = "http-fields"
6
+ author = "Charles Yeomans"
7
+ copyright = "Charles Yeomans" # noqa: A001
8
+
9
+ try:
10
+ release = _version("http-fields")
11
+ except PackageNotFoundError: # not installed in the docs environment
12
+ release = "0.0.0"
13
+ version = release
14
+
15
+ extensions = [
16
+ "myst_parser",
17
+ "sphinx.ext.autodoc",
18
+ "sphinx.ext.napoleon",
19
+ "sphinx.ext.intersphinx",
20
+ ]
21
+
22
+ # autodoc
23
+ autodoc_member_order = "bysource"
24
+ autodoc_default_options = {
25
+ "members": True,
26
+ "show-inheritance": True,
27
+ }
28
+ autodoc_typehints = "description" # render annotations into the field list
29
+ python_use_unqualified_type_names = True
30
+
31
+ # Cross-link stdlib types (datetime, Decimal, ...) in signatures.
32
+ intersphinx_mapping = {"python": ("https://docs.python.org/3", None)}
33
+
34
+ # Resolve [text](other.md) cross-links between source files, and allow the
35
+ # ::: fenced form for directives in addition to the ``` form.
36
+ myst_enable_extensions = ["colon_fence"]
37
+
38
+ # Generate anchors for h1-h3 so in-page links like [x](#some-heading) resolve.
39
+ myst_heading_anchors = 3
40
+
41
+ source_suffix = {".md": "markdown"}
42
+ exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
43
+
44
+ html_theme = "furo"
45
+ html_title = "http-fields"
@@ -0,0 +1,77 @@
1
+ # The header model
2
+
3
+ This page explains *why* `http-fields` is shaped the way it is. For task-focused instructions,
4
+ see the [how-to guides](../how-to/parse-and-build-headers.md); for the full API, the
5
+ [reference](../reference/headers.md).
6
+
7
+ ## Headers are immutable value objects
8
+
9
+ Every header is a frozen dataclass whose fields are the *structured components* of the header —
10
+ `Age.seconds`, `ContentType.media_type`, `SetCookie.max_age`, and so on. Consequences:
11
+
12
+ - **Immutable.** You never mutate a header in place; you build a new one. This makes headers safe
13
+ to share and cache.
14
+ - **Comparable and hashable.** Two headers are equal when their fields are equal, so headers work
15
+ in sets and as dict keys.
16
+ - **The value is derived.** `.value` (and `str()`, `bytes()`) are computed from the fields, so the
17
+ serialized form always reflects the current structured state — they can't drift apart.
18
+
19
+ ## abnf drives parsing *and* validation
20
+
21
+ The library is built on [abnf](https://pypi.org/project/abnf/) grammars for the relevant RFCs.
22
+ Those grammars are used in both directions:
23
+
24
+ - **Parsing** — `Header.parse(value)` runs the grammar over the raw value and a visitor turns the
25
+ parse tree into structured fields.
26
+ - **Validation** — the field types themselves (`Token`, `NonNegativeInt`, `EntityTag`, …) parse
27
+ and validate on construction. So validity isn't a separate step bolted on; it's a property of
28
+ the types the fields are made of.
29
+
30
+ A constructed header is therefore always well-formed: invalid input raises `ValueError` (or
31
+ `TypeError` for wrong argument types), whether it came from `parse()` or direct construction.
32
+
33
+ Because each field type is self-validating, a header is parsed **at most once**. `parse()` runs
34
+ the grammar and the visitor wraps the already-validated node text as leaf types with
35
+ `parse=False` (a trusted construction that skips re-parsing); direct construction validates only
36
+ the parts you actually pass. There is no second grammar pass to re-check a value the visitor just
37
+ built. Structured-Fields headers take this a step further: their serializer *is* the validator —
38
+ a value that would inject control characters or exceed a numeric bound simply fails to serialize —
39
+ so a successful serialization is grammar-valid by construction, with no re-parse at all.
40
+
41
+ ## One construction contract
42
+
43
+ There is a single, consistent way to make headers:
44
+
45
+ - **`Header.parse(value)`** is the only string entry point.
46
+ - **Direct construction** takes the structured fields as their parsed types. Scalars coerce plain
47
+ values (`Age(60)`); list-style headers take leaf types as varargs
48
+ (`Connection(Token("keep-alive"), Token("close"))`); headers with richer values take value
49
+ objects (`Upgrade(Protocol("HTTP", "2"))`). A raw string where a typed field is expected is a
50
+ `TypeError` — reach for `parse()` instead.
51
+ - **Builders** (`ContentType.of(...)`, `SetCookie.build(...)`, `ETag.from_tag(...)`) exist only
52
+ where piece-wise construction is richer than the stored fields.
53
+ - **`Header.create(name, value)`** dispatches by field name to the right subclass, or a
54
+ `CustomHeader`.
55
+
56
+ This is a deliberate break from the older overloaded "string *or* pieces" constructors: one
57
+ object, one obvious way to build it, and precise types throughout.
58
+
59
+ ## Shared bases where shapes repeat
60
+
61
+ Families that share an identical shape are factored onto a common base — dates (`DateHeader`),
62
+ entity-tag lists (`EntityTagListHeader`), product lists (`UserAgent`/`Server`), the auth pairs
63
+ (`WWWAuthenticate`/`ProxyAuthenticate`, …), and Structured-Fields lists/dictionaries. Where a
64
+ family only *rhymes* (different element types or separators), the headers stay individual — a
65
+ shared base isn't forced.
66
+
67
+ ## Scope
68
+
69
+ Coverage spans the core HTTP RFCs (9110, 9111, 6265, 6266), widely-deployed extensions
70
+ (Forwarded, CORS, Link, HSTS, Alt-Svc, Prefer), and the modern Structured-Fields headers
71
+ (RFC 9651 and the specs built on it). Headers defined outside a clean RFC ABNF grammar are out of
72
+ scope, because the whole library is grammar-driven.
73
+
74
+ ---
75
+
76
+ *Implementation notes, design decisions, and the migration history live in the project's internal
77
+ design document (`.claude/design/header-model.md`).*
@@ -0,0 +1,51 @@
1
+ # How to handle custom and unknown headers
2
+
3
+ ## Dispatch an incoming header by name
4
+
5
+ When you receive a `(name, value)` pair and don't know the type ahead of time, use
6
+ `Header.create()`. It matches the name (case-insensitively) against the known headers and returns
7
+ that subclass, falling back to `CustomHeader` for anything unrecognized:
8
+
9
+ ```python
10
+ from http_fields import Header, ContentType, CustomHeader
11
+
12
+ h = Header.create("Content-Type", "text/plain")
13
+ isinstance(h, ContentType) # True
14
+
15
+ h = Header.create("X-Request-Id", "abc123")
16
+ isinstance(h, CustomHeader) # True
17
+ ```
18
+
19
+ `create()` validates the value through the matched header's `parse()`, so an invalid known-header
20
+ value still raises `ValueError`.
21
+
22
+ ## Represent an arbitrary header
23
+
24
+ `CustomHeader` models any header with a non-standard field name. Its name and value are validated
25
+ as an RFC 9110 `field-name` / `field-value`:
26
+
27
+ ```python
28
+ from http_fields import CustomHeader
29
+
30
+ h = CustomHeader("X-Request-Id", "abc123")
31
+ h.name # 'X-Request-Id'
32
+ h.value # 'abc123'
33
+ str(h) # 'X-Request-Id: abc123'
34
+ h.asgi_value # (b'X-Request-Id', b'abc123')
35
+
36
+ CustomHeader("bad name", "v") # ValueError: Invalid field-name "bad name".
37
+ ```
38
+
39
+ ## Iterate the known headers
40
+
41
+ `Header.subclass_tree()` yields every registered header class, which is handy for tooling or
42
+ introspection:
43
+
44
+ ```python
45
+ from http_fields import Header
46
+
47
+ names = sorted(
48
+ c.name for c in Header.subclass_tree()
49
+ if isinstance(getattr(c, "name", None), str)
50
+ )
51
+ ```
@@ -0,0 +1,108 @@
1
+ # How to parse and build headers
2
+
3
+ There are three ways to get a header instance. This guide shows when to use each.
4
+
5
+ ## Parse a raw value string
6
+
7
+ `Header.parse(value)` is the only string entry point. It validates against the header's abnf
8
+ grammar and raises `ValueError` on bad input.
9
+
10
+ ```python
11
+ from http_fields import CacheControl
12
+
13
+ cc = CacheControl.parse("max-age=3600, no-cache, immutable")
14
+ cc.max_age # 3600
15
+ cc.no_cache # True
16
+ cc.immutable # True
17
+ ```
18
+
19
+ Pass the header *value* only — not the `Name: value` line.
20
+
21
+ ## Construct from parsed fields
22
+
23
+ `parse()` is the entry point for *strings*. Direct construction takes each field as its
24
+ already-parsed type — a leaf type or value object — so a constructor never re-runs the grammar
25
+ on untrusted text. Passing a raw string where a typed field is expected raises `TypeError`
26
+ pointing you back to `parse()`.
27
+
28
+ Plain scalars still coerce their natural Python value:
29
+
30
+ ```python
31
+ from datetime import datetime, timezone
32
+ from http_fields import Age, Date, RetryAfter
33
+
34
+ Age(60) # int -> NonNegativeInt
35
+ Date(datetime(2030, 1, 1, tzinfo=timezone.utc)) # a datetime
36
+ RetryAfter(120) # delay-seconds ...
37
+ RetryAfter(datetime(2030, 1, 1, tzinfo=timezone.utc)) # ... or an HTTP-date
38
+ ```
39
+
40
+ List-style headers take their leaf types as varargs. Build the leaves (each validates itself),
41
+ or `parse()` a whole value string:
42
+
43
+ ```python
44
+ from http_fields import Connection, FieldName, Token, Vary
45
+
46
+ Connection(Token("keep-alive"), Token("close"))
47
+ Vary(FieldName("accept-encoding"), FieldName("accept-language"))
48
+
49
+ Connection.parse("keep-alive, close") # from a raw string
50
+ ```
51
+
52
+ `Connection("keep-alive")` — a bare string — is now a `TypeError`; use `Connection.parse(...)`.
53
+
54
+ Headers with structured values take value objects, which coerce their string parts to validated
55
+ leaves:
56
+
57
+ ```python
58
+ from http_fields import Protocol, Upgrade
59
+
60
+ Upgrade(Protocol("HTTP", "2"), Protocol("WebSocket"))
61
+ Upgrade.parse("HTTP/2, WebSocket") # equivalent, from a string
62
+ ```
63
+
64
+ ## Use a builder classmethod
65
+
66
+ Headers whose piece-wise construction is richer than their stored fields expose a builder:
67
+
68
+ | Header | Builder |
69
+ |---|---|
70
+ | `ContentType` | `ContentType.of(type, subtype, *, charset=None, boundary=None, params=None)` |
71
+ | `SetCookie` | `SetCookie.build(cookie_name, cookie_value, *, domain=None, path=None, expires=None, max_age=None, secure=False, http_only=False, samesite="Lax", extension=None)` |
72
+ | `ContentDisposition` | `ContentDisposition.build(disposition_type, disposition_parms=None)` |
73
+ | `ETag` | `ETag.from_tag(tag, weak=False)` |
74
+
75
+ ```python
76
+ from http_fields import ContentType, SetCookie, ETag
77
+
78
+ ContentType.of(type="text", subtype="html", charset="utf-8")
79
+ SetCookie.build(cookie_name="SID", cookie_value="abc", path="/", secure=True)
80
+ ETag.from_tag("deadbeef", weak=True)
81
+ ```
82
+
83
+ `.build()` validates its input strictly (raising `TypeError`/`ValueError`); `SetCookie.parse()`
84
+ uses the more lenient RFC 6265 §5 algorithm for interoperability.
85
+
86
+ ## Wildcard headers
87
+
88
+ `If-Match` / `If-None-Match` use `wildcard=True` for `*`; `AltSvc` uses `clear=True` for `clear`:
89
+
90
+ ```python
91
+ from http_fields import IfMatch, AltSvc
92
+ from http_fields.visitors.rfc9110 import EntityTag
93
+
94
+ IfMatch(wildcard=True) # "*"
95
+ IfMatch(EntityTag("deadbeef")) # a specific tag
96
+ AltSvc(clear=True) # "clear"
97
+ ```
98
+
99
+ ## Serialize
100
+
101
+ Any header round-trips to text and bytes:
102
+
103
+ ```python
104
+ str(cc) # 'Cache-Control: max-age=3600,no-cache,immutable'
105
+ cc.value # 'max-age=3600,no-cache,immutable'
106
+ bytes(cc) # b'Cache-Control: ...'
107
+ cc.asgi_value # (b'Cache-Control', b'...')
108
+ ```
@@ -0,0 +1,75 @@
1
+ # How to work with Structured Fields
2
+
3
+ [RFC 9651 Structured Field Values](https://www.rfc-editor.org/rfc/rfc9651) is the type system
4
+ that modern HTTP headers are built on. `http-fields` ships a standalone implementation in
5
+ `http_fields.structuredfields`, plus header classes that use it.
6
+
7
+ ## Use the Structured-Fields headers
8
+
9
+ These behave like any other header:
10
+
11
+ ```python
12
+ from http_fields import Priority, CacheStatus, ContentDigest
13
+ import base64
14
+
15
+ Priority.parse("u=5, i").urgency # 5
16
+ Priority(urgency=5, incremental=True).value # 'u=5, i'
17
+
18
+ digest = base64.b64encode(b"hello").decode()
19
+ cd = ContentDigest.parse(f"sha-256=:{digest}:")
20
+ cd.digests # (('sha-256', b'hello'),)
21
+
22
+ cs = CacheStatus.parse("ExampleCache; hit; ttl=376")
23
+ cs.members[0].value # 'ExampleCache' (a Token)
24
+ cs.members[0].parameters # (('hit', True), ('ttl', 376))
25
+ ```
26
+
27
+ ## Parse and serialize raw Structured Fields
28
+
29
+ Use the `parse_*` / `serialize_*` functions directly for the three top-level types — Item, List,
30
+ and Dictionary:
31
+
32
+ ```python
33
+ from http_fields.structuredfields import (
34
+ parse_item, parse_list, parse_dictionary,
35
+ serialize_item, serialize_list, serialize_dictionary,
36
+ )
37
+
38
+ parse_item("5;foo=bar") # Item(value=5, parameters=(('foo', Token('bar')),))
39
+ parse_list("a, b, (c d);x=1") # tuple of Item / InnerList
40
+ parse_dictionary("a=1, b, c=?0") # tuple of (key, Item / InnerList) pairs
41
+ ```
42
+
43
+ Bare items map to native Python types:
44
+
45
+ | Structured Fields type | Python type |
46
+ |---|---|
47
+ | Integer | `int` |
48
+ | Decimal | `decimal.Decimal` |
49
+ | String | `str` |
50
+ | Token | `Token` (a `str` subclass) |
51
+ | Boolean | `bool` |
52
+ | Byte Sequence | `bytes` |
53
+ | Date | `datetime.datetime` |
54
+ | Display String | `DisplayString` (a `str` subclass) |
55
+
56
+ `Token` and `DisplayString` are distinct `str` subclasses so they serialize correctly (a `Token`
57
+ is bare, a `str` is quoted):
58
+
59
+ ```python
60
+ from http_fields.structuredfields import Item, Token, serialize_item
61
+
62
+ serialize_item(Item(Token("gzip"))) # 'gzip'
63
+ serialize_item(Item("gzip")) # '"gzip"'
64
+ ```
65
+
66
+ ## Build values from scratch
67
+
68
+ Compose `Item` / `InnerList` and serialize:
69
+
70
+ ```python
71
+ from http_fields.structuredfields import Item, InnerList, Token, serialize_list
72
+
73
+ members = (Item(Token("sugar")), InnerList((Item(1), Item(2)), (("x", True),)))
74
+ serialize_list(members) # 'sugar, (1 2);x'
75
+ ```
@@ -0,0 +1,44 @@
1
+ # http-fields documentation
2
+
3
+ ```{toctree}
4
+ :hidden:
5
+ :maxdepth: 2
6
+
7
+ tutorials/getting-started
8
+ how-to/parse-and-build-headers
9
+ how-to/custom-and-unknown-headers
10
+ how-to/structured-fields
11
+ reference/headers
12
+ reference/structured-fields
13
+ reference/api
14
+ explanation/design
15
+ ```
16
+
17
+ `http-fields` represents HTTP headers as immutable Python dataclasses. [abnf](https://pypi.org/project/abnf/)
18
+ grammars drive both directions: **parsing** incoming header strings and **validating** field
19
+ values, so a constructed header is always well-formed.
20
+
21
+ Requires Python 3.10+.
22
+
23
+ ## Documentation
24
+
25
+ The docs follow the [Diátaxis](https://diataxis.fr/) model:
26
+
27
+ - **[Tutorials](tutorials/getting-started.md)** — start here if you're new. A hands-on walk
28
+ through installing, parsing, building, and serializing headers.
29
+ - **How-to guides** — task-focused recipes:
30
+ - [Parse and build headers](how-to/parse-and-build-headers.md)
31
+ - [Custom and unknown headers](how-to/custom-and-unknown-headers.md)
32
+ - [Work with Structured Fields](how-to/structured-fields.md)
33
+ - **Reference** — precise technical description:
34
+ - [Header catalog](reference/headers.md) — every header, grouped by spec, with its constructors.
35
+ - [`structuredfields` API](reference/structured-fields.md)
36
+ - **[Explanation](explanation/design.md)** — the *why*: the frozen-dataclass model, the
37
+ construction contract, and where validation lives.
38
+
39
+ ## Install
40
+
41
+ ```sh
42
+ uv add http-fields
43
+ # or: pip install http-fields
44
+ ```