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.
- http_fields-0.1.1/.gitignore +26 -0
- http_fields-0.1.1/CHANGELOG.md +50 -0
- http_fields-0.1.1/LICENSE.txt +16 -0
- http_fields-0.1.1/PKG-INFO +88 -0
- http_fields-0.1.1/README.md +60 -0
- http_fields-0.1.1/SECURITY.md +26 -0
- http_fields-0.1.1/docs/conf.py +45 -0
- http_fields-0.1.1/docs/explanation/design.md +77 -0
- http_fields-0.1.1/docs/how-to/custom-and-unknown-headers.md +51 -0
- http_fields-0.1.1/docs/how-to/parse-and-build-headers.md +108 -0
- http_fields-0.1.1/docs/how-to/structured-fields.md +75 -0
- http_fields-0.1.1/docs/index.md +44 -0
- http_fields-0.1.1/docs/reference/api.md +13 -0
- http_fields-0.1.1/docs/reference/headers.md +156 -0
- http_fields-0.1.1/docs/reference/structured-fields.md +61 -0
- http_fields-0.1.1/docs/tutorials/getting-started.md +99 -0
- http_fields-0.1.1/pyproject.toml +107 -0
- http_fields-0.1.1/src/http_fields/__init__.py +193 -0
- http_fields-0.1.1/src/http_fields/accept.py +37 -0
- http_fields-0.1.1/src/http_fields/acceptcharset.py +48 -0
- http_fields-0.1.1/src/http_fields/acceptencoding.py +42 -0
- http_fields-0.1.1/src/http_fields/acceptlanguage.py +47 -0
- http_fields-0.1.1/src/http_fields/acceptranges.py +35 -0
- http_fields-0.1.1/src/http_fields/accesscontrol.py +197 -0
- http_fields-0.1.1/src/http_fields/age.py +34 -0
- http_fields-0.1.1/src/http_fields/allow.py +46 -0
- http_fields-0.1.1/src/http_fields/altsvc.py +70 -0
- http_fields-0.1.1/src/http_fields/authbases.py +79 -0
- http_fields-0.1.1/src/http_fields/authenticationinfo.py +19 -0
- http_fields-0.1.1/src/http_fields/authorization.py +23 -0
- http_fields-0.1.1/src/http_fields/cachecontrol.py +119 -0
- http_fields-0.1.1/src/http_fields/cachestatus.py +12 -0
- http_fields-0.1.1/src/http_fields/connection.py +35 -0
- http_fields-0.1.1/src/http_fields/contentdigest.py +19 -0
- http_fields-0.1.1/src/http_fields/contentdisposition.py +66 -0
- http_fields-0.1.1/src/http_fields/contentencoding.py +45 -0
- http_fields-0.1.1/src/http_fields/contentlanguage.py +38 -0
- http_fields-0.1.1/src/http_fields/contentlength.py +34 -0
- http_fields-0.1.1/src/http_fields/contentlocation.py +24 -0
- http_fields-0.1.1/src/http_fields/contentrange.py +74 -0
- http_fields-0.1.1/src/http_fields/contenttype.py +104 -0
- http_fields-0.1.1/src/http_fields/cookie.py +38 -0
- http_fields-0.1.1/src/http_fields/date.py +17 -0
- http_fields-0.1.1/src/http_fields/dateheader.py +32 -0
- http_fields-0.1.1/src/http_fields/entitytaglist.py +39 -0
- http_fields-0.1.1/src/http_fields/etag.py +46 -0
- http_fields-0.1.1/src/http_fields/expect.py +52 -0
- http_fields-0.1.1/src/http_fields/expires.py +17 -0
- http_fields-0.1.1/src/http_fields/forwarded.py +36 -0
- http_fields-0.1.1/src/http_fields/fromheader.py +42 -0
- http_fields-0.1.1/src/http_fields/header.py +250 -0
- http_fields-0.1.1/src/http_fields/host.py +44 -0
- http_fields-0.1.1/src/http_fields/ifmatch.py +24 -0
- http_fields-0.1.1/src/http_fields/ifmodifiedsince.py +17 -0
- http_fields-0.1.1/src/http_fields/ifnonematch.py +24 -0
- http_fields-0.1.1/src/http_fields/ifrange.py +49 -0
- http_fields-0.1.1/src/http_fields/ifunmodifiedsince.py +17 -0
- http_fields-0.1.1/src/http_fields/lastmodified.py +17 -0
- http_fields-0.1.1/src/http_fields/link.py +35 -0
- http_fields-0.1.1/src/http_fields/location.py +24 -0
- http_fields-0.1.1/src/http_fields/maxforwards.py +32 -0
- http_fields-0.1.1/src/http_fields/origin.py +42 -0
- http_fields-0.1.1/src/http_fields/parsedobjs.py +62 -0
- http_fields-0.1.1/src/http_fields/prefer.py +64 -0
- http_fields-0.1.1/src/http_fields/priority.py +55 -0
- http_fields-0.1.1/src/http_fields/productlistheader.py +33 -0
- http_fields-0.1.1/src/http_fields/proxyauthenticate.py +17 -0
- http_fields-0.1.1/src/http_fields/proxyauthenticationinfo.py +17 -0
- http_fields-0.1.1/src/http_fields/proxyauthorization.py +17 -0
- http_fields-0.1.1/src/http_fields/proxystatus.py +12 -0
- http_fields-0.1.1/src/http_fields/py.typed +0 -0
- http_fields-0.1.1/src/http_fields/range.py +37 -0
- http_fields-0.1.1/src/http_fields/referer.py +24 -0
- http_fields-0.1.1/src/http_fields/retryafter.py +45 -0
- http_fields-0.1.1/src/http_fields/server.py +18 -0
- http_fields-0.1.1/src/http_fields/setcookie.py +419 -0
- http_fields-0.1.1/src/http_fields/stricttransportsecurity.py +96 -0
- http_fields-0.1.1/src/http_fields/structuredfields.py +356 -0
- http_fields-0.1.1/src/http_fields/structuredheaders.py +90 -0
- http_fields-0.1.1/src/http_fields/te.py +36 -0
- http_fields-0.1.1/src/http_fields/trailer.py +44 -0
- http_fields-0.1.1/src/http_fields/upgrade.py +35 -0
- http_fields-0.1.1/src/http_fields/uriheader.py +36 -0
- http_fields-0.1.1/src/http_fields/useragent.py +18 -0
- http_fields-0.1.1/src/http_fields/vary.py +40 -0
- http_fields-0.1.1/src/http_fields/via.py +35 -0
- http_fields-0.1.1/src/http_fields/visitors/__init__.py +1 -0
- http_fields-0.1.1/src/http_fields/visitors/rfc3986.py +195 -0
- http_fields-0.1.1/src/http_fields/visitors/rfc6265.py +62 -0
- http_fields-0.1.1/src/http_fields/visitors/rfc6266.py +234 -0
- http_fields-0.1.1/src/http_fields/visitors/rfc7230.py +80 -0
- http_fields-0.1.1/src/http_fields/visitors/rfc7239.py +43 -0
- http_fields-0.1.1/src/http_fields/visitors/rfc7240.py +77 -0
- http_fields-0.1.1/src/http_fields/visitors/rfc7838.py +77 -0
- http_fields-0.1.1/src/http_fields/visitors/rfc8288.py +56 -0
- http_fields-0.1.1/src/http_fields/visitors/rfc9110/__init__.py +117 -0
- http_fields-0.1.1/src/http_fields/visitors/rfc9110/_base.py +71 -0
- http_fields-0.1.1/src/http_fields/visitors/rfc9110/accept.py +135 -0
- http_fields-0.1.1/src/http_fields/visitors/rfc9110/acceptcharset.py +26 -0
- http_fields-0.1.1/src/http_fields/visitors/rfc9110/acceptencoding.py +76 -0
- http_fields-0.1.1/src/http_fields/visitors/rfc9110/acceptlanguage.py +55 -0
- http_fields-0.1.1/src/http_fields/visitors/rfc9110/acceptranges.py +13 -0
- http_fields-0.1.1/src/http_fields/visitors/rfc9110/allow.py +13 -0
- http_fields-0.1.1/src/http_fields/visitors/rfc9110/authenticationinfo.py +19 -0
- http_fields-0.1.1/src/http_fields/visitors/rfc9110/authorization.py +77 -0
- http_fields-0.1.1/src/http_fields/visitors/rfc9110/authparam.py +40 -0
- http_fields-0.1.1/src/http_fields/visitors/rfc9110/authscheme.py +17 -0
- http_fields-0.1.1/src/http_fields/visitors/rfc9110/challenge.py +57 -0
- http_fields-0.1.1/src/http_fields/visitors/rfc9110/comment.py +99 -0
- http_fields-0.1.1/src/http_fields/visitors/rfc9110/connection.py +13 -0
- http_fields-0.1.1/src/http_fields/visitors/rfc9110/contentlanguage.py +21 -0
- http_fields-0.1.1/src/http_fields/visitors/rfc9110/contentlength.py +9 -0
- http_fields-0.1.1/src/http_fields/visitors/rfc9110/contentrange.py +70 -0
- http_fields-0.1.1/src/http_fields/visitors/rfc9110/contenttype.py +52 -0
- http_fields-0.1.1/src/http_fields/visitors/rfc9110/date.py +12 -0
- http_fields-0.1.1/src/http_fields/visitors/rfc9110/entitytag.py +66 -0
- http_fields-0.1.1/src/http_fields/visitors/rfc9110/etag.py +12 -0
- http_fields-0.1.1/src/http_fields/visitors/rfc9110/field.py +15 -0
- http_fields-0.1.1/src/http_fields/visitors/rfc9110/host.py +32 -0
- http_fields-0.1.1/src/http_fields/visitors/rfc9110/httpdate.py +167 -0
- http_fields-0.1.1/src/http_fields/visitors/rfc9110/ifmatch.py +24 -0
- http_fields-0.1.1/src/http_fields/visitors/rfc9110/ifmodifiedsince.py +12 -0
- http_fields-0.1.1/src/http_fields/visitors/rfc9110/ifnonematch.py +26 -0
- http_fields-0.1.1/src/http_fields/visitors/rfc9110/ifunmodifiedsince.py +12 -0
- http_fields-0.1.1/src/http_fields/visitors/rfc9110/lastmodified.py +14 -0
- http_fields-0.1.1/src/http_fields/visitors/rfc9110/location.py +9 -0
- http_fields-0.1.1/src/http_fields/visitors/rfc9110/parameters.py +124 -0
- http_fields-0.1.1/src/http_fields/visitors/rfc9110/product.py +39 -0
- http_fields-0.1.1/src/http_fields/visitors/rfc9110/quotedstring.py +66 -0
- http_fields-0.1.1/src/http_fields/visitors/rfc9110/range.py +100 -0
- http_fields-0.1.1/src/http_fields/visitors/rfc9110/rangeunit.py +17 -0
- http_fields-0.1.1/src/http_fields/visitors/rfc9110/retryafter.py +20 -0
- http_fields-0.1.1/src/http_fields/visitors/rfc9110/server.py +16 -0
- http_fields-0.1.1/src/http_fields/visitors/rfc9110/te.py +83 -0
- http_fields-0.1.1/src/http_fields/visitors/rfc9110/token.py +18 -0
- http_fields-0.1.1/src/http_fields/visitors/rfc9110/token68.py +18 -0
- http_fields-0.1.1/src/http_fields/visitors/rfc9110/type.py +17 -0
- http_fields-0.1.1/src/http_fields/visitors/rfc9110/upgrade.py +57 -0
- http_fields-0.1.1/src/http_fields/visitors/rfc9110/useragent.py +16 -0
- http_fields-0.1.1/src/http_fields/visitors/rfc9110/vary.py +10 -0
- http_fields-0.1.1/src/http_fields/visitors/rfc9110/via.py +86 -0
- http_fields-0.1.1/src/http_fields/visitors/rfc9110/weight.py +40 -0
- http_fields-0.1.1/src/http_fields/visitors/rfc9110/wwwauthenticate.py +21 -0
- http_fields-0.1.1/src/http_fields/visitors/rfc9111.py +92 -0
- http_fields-0.1.1/src/http_fields/wwwauthenticate.py +24 -0
- http_fields-0.1.1/tests/__init__.py +0 -0
- http_fields-0.1.1/tests/test_accept.py +45 -0
- http_fields-0.1.1/tests/test_acceptcharset.py +14 -0
- http_fields-0.1.1/tests/test_acceptencoding.py +22 -0
- http_fields-0.1.1/tests/test_acceptlanguage.py +18 -0
- http_fields-0.1.1/tests/test_acceptranges.py +25 -0
- http_fields-0.1.1/tests/test_accesscontrol.py +76 -0
- http_fields-0.1.1/tests/test_age.py +45 -0
- http_fields-0.1.1/tests/test_allow.py +24 -0
- http_fields-0.1.1/tests/test_altsvc.py +26 -0
- http_fields-0.1.1/tests/test_authenticationinfo.py +19 -0
- http_fields-0.1.1/tests/test_authorization.py +36 -0
- http_fields-0.1.1/tests/test_cachecontrol.py +71 -0
- http_fields-0.1.1/tests/test_cachestatus.py +39 -0
- http_fields-0.1.1/tests/test_connection.py +28 -0
- http_fields-0.1.1/tests/test_contentdigest.py +45 -0
- http_fields-0.1.1/tests/test_contentdisposition.py +105 -0
- http_fields-0.1.1/tests/test_contentencoding.py +18 -0
- http_fields-0.1.1/tests/test_contentlanguage.py +14 -0
- http_fields-0.1.1/tests/test_contentlength.py +27 -0
- http_fields-0.1.1/tests/test_contentlocation.py +12 -0
- http_fields-0.1.1/tests/test_contentrange.py +45 -0
- http_fields-0.1.1/tests/test_contenttype.py +107 -0
- http_fields-0.1.1/tests/test_cookie.py +54 -0
- http_fields-0.1.1/tests/test_date.py +51 -0
- http_fields-0.1.1/tests/test_etag.py +42 -0
- http_fields-0.1.1/tests/test_expect.py +11 -0
- http_fields-0.1.1/tests/test_expires.py +18 -0
- http_fields-0.1.1/tests/test_forwarded.py +22 -0
- http_fields-0.1.1/tests/test_from.py +18 -0
- http_fields-0.1.1/tests/test_header.py +66 -0
- http_fields-0.1.1/tests/test_host.py +48 -0
- http_fields-0.1.1/tests/test_ifmatch.py +18 -0
- http_fields-0.1.1/tests/test_ifmodifiedsince.py +18 -0
- http_fields-0.1.1/tests/test_ifnonematch.py +18 -0
- http_fields-0.1.1/tests/test_ifrange.py +30 -0
- http_fields-0.1.1/tests/test_ifunmodifiedsince.py +18 -0
- http_fields-0.1.1/tests/test_injection.py +180 -0
- http_fields-0.1.1/tests/test_lastmodified.py +18 -0
- http_fields-0.1.1/tests/test_limits.py +29 -0
- http_fields-0.1.1/tests/test_link.py +17 -0
- http_fields-0.1.1/tests/test_location.py +32 -0
- http_fields-0.1.1/tests/test_maxforwards.py +22 -0
- http_fields-0.1.1/tests/test_origin.py +16 -0
- http_fields-0.1.1/tests/test_plausibles.py +112 -0
- http_fields-0.1.1/tests/test_prefer.py +27 -0
- http_fields-0.1.1/tests/test_priority.py +29 -0
- http_fields-0.1.1/tests/test_proxyauth.py +40 -0
- http_fields-0.1.1/tests/test_range.py +44 -0
- http_fields-0.1.1/tests/test_referer.py +12 -0
- http_fields-0.1.1/tests/test_retryafter.py +57 -0
- http_fields-0.1.1/tests/test_roundtrip.py +117 -0
- http_fields-0.1.1/tests/test_server.py +10 -0
- http_fields-0.1.1/tests/test_setcookie.py +290 -0
- http_fields-0.1.1/tests/test_stricttransportsecurity.py +48 -0
- http_fields-0.1.1/tests/test_structuredfields.py +134 -0
- http_fields-0.1.1/tests/test_structuredfields_properties.py +150 -0
- http_fields-0.1.1/tests/test_te.py +20 -0
- http_fields-0.1.1/tests/test_trailer.py +18 -0
- http_fields-0.1.1/tests/test_upgrade.py +16 -0
- http_fields-0.1.1/tests/test_useragent.py +35 -0
- http_fields-0.1.1/tests/test_value_cache.py +33 -0
- http_fields-0.1.1/tests/test_vary.py +29 -0
- http_fields-0.1.1/tests/test_via.py +22 -0
- http_fields-0.1.1/tests/test_wwwauthenticate.py +34 -0
- http_fields-0.1.1/tests/visitors/__init__.py +0 -0
- http_fields-0.1.1/tests/visitors/rfc9110/__iinit__.py +0 -0
- http_fields-0.1.1/tests/visitors/rfc9110/test_accept.py +99 -0
- http_fields-0.1.1/tests/visitors/rfc9110/test_acceptcharset.py +13 -0
- http_fields-0.1.1/tests/visitors/rfc9110/test_acceptencoding.py +32 -0
- http_fields-0.1.1/tests/visitors/rfc9110/test_acceptlanguage.py +19 -0
- http_fields-0.1.1/tests/visitors/rfc9110/test_authenticationinfo.py +18 -0
- http_fields-0.1.1/tests/visitors/rfc9110/test_authorization.py +80 -0
- http_fields-0.1.1/tests/visitors/rfc9110/test_authparam.py +49 -0
- http_fields-0.1.1/tests/visitors/rfc9110/test_authscheme.py +14 -0
- http_fields-0.1.1/tests/visitors/rfc9110/test_base.py +31 -0
- http_fields-0.1.1/tests/visitors/rfc9110/test_challenge.py +72 -0
- http_fields-0.1.1/tests/visitors/rfc9110/test_comment.py +21 -0
- http_fields-0.1.1/tests/visitors/rfc9110/test_connection.py +11 -0
- http_fields-0.1.1/tests/visitors/rfc9110/test_contentlength.py +11 -0
- http_fields-0.1.1/tests/visitors/rfc9110/test_contentrange.py +40 -0
- http_fields-0.1.1/tests/visitors/rfc9110/test_etag.py +17 -0
- http_fields-0.1.1/tests/visitors/rfc9110/test_host.py +17 -0
- http_fields-0.1.1/tests/visitors/rfc9110/test_httpdate.py +111 -0
- http_fields-0.1.1/tests/visitors/rfc9110/test_ifmatch.py +20 -0
- http_fields-0.1.1/tests/visitors/rfc9110/test_ifnonematch.py +20 -0
- http_fields-0.1.1/tests/visitors/rfc9110/test_parameters.py +50 -0
- http_fields-0.1.1/tests/visitors/rfc9110/test_product.py +6 -0
- http_fields-0.1.1/tests/visitors/rfc9110/test_quotedstring.py +47 -0
- http_fields-0.1.1/tests/visitors/rfc9110/test_token.py +25 -0
- http_fields-0.1.1/tests/visitors/rfc9110/test_token68.py +24 -0
- http_fields-0.1.1/tests/visitors/rfc9110/test_useragent.py +15 -0
- http_fields-0.1.1/tests/visitors/rfc9110/test_weight.py +18 -0
- http_fields-0.1.1/tests/visitors/test_entitytag.py +56 -0
- http_fields-0.1.1/tests/visitors/test_misc.py +30 -0
- http_fields-0.1.1/tests/visitors/test_rfc3986.py +30 -0
- http_fields-0.1.1/tests/visitors/test_rfc6265.py +14 -0
- http_fields-0.1.1/tests/visitors/test_rfc6266.py +203 -0
- http_fields-0.1.1/tests/visitors/test_rfc7230.py +70 -0
- 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
|
+
```
|