http-fields 0.1.1__py3-none-any.whl

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 (132) hide show
  1. http_fields/__init__.py +193 -0
  2. http_fields/accept.py +37 -0
  3. http_fields/acceptcharset.py +48 -0
  4. http_fields/acceptencoding.py +42 -0
  5. http_fields/acceptlanguage.py +47 -0
  6. http_fields/acceptranges.py +35 -0
  7. http_fields/accesscontrol.py +197 -0
  8. http_fields/age.py +34 -0
  9. http_fields/allow.py +46 -0
  10. http_fields/altsvc.py +70 -0
  11. http_fields/authbases.py +79 -0
  12. http_fields/authenticationinfo.py +19 -0
  13. http_fields/authorization.py +23 -0
  14. http_fields/cachecontrol.py +119 -0
  15. http_fields/cachestatus.py +12 -0
  16. http_fields/connection.py +35 -0
  17. http_fields/contentdigest.py +19 -0
  18. http_fields/contentdisposition.py +66 -0
  19. http_fields/contentencoding.py +45 -0
  20. http_fields/contentlanguage.py +38 -0
  21. http_fields/contentlength.py +34 -0
  22. http_fields/contentlocation.py +24 -0
  23. http_fields/contentrange.py +74 -0
  24. http_fields/contenttype.py +104 -0
  25. http_fields/cookie.py +38 -0
  26. http_fields/date.py +17 -0
  27. http_fields/dateheader.py +32 -0
  28. http_fields/entitytaglist.py +39 -0
  29. http_fields/etag.py +46 -0
  30. http_fields/expect.py +52 -0
  31. http_fields/expires.py +17 -0
  32. http_fields/forwarded.py +36 -0
  33. http_fields/fromheader.py +42 -0
  34. http_fields/header.py +250 -0
  35. http_fields/host.py +44 -0
  36. http_fields/ifmatch.py +24 -0
  37. http_fields/ifmodifiedsince.py +17 -0
  38. http_fields/ifnonematch.py +24 -0
  39. http_fields/ifrange.py +49 -0
  40. http_fields/ifunmodifiedsince.py +17 -0
  41. http_fields/lastmodified.py +17 -0
  42. http_fields/link.py +35 -0
  43. http_fields/location.py +24 -0
  44. http_fields/maxforwards.py +32 -0
  45. http_fields/origin.py +42 -0
  46. http_fields/parsedobjs.py +62 -0
  47. http_fields/prefer.py +64 -0
  48. http_fields/priority.py +55 -0
  49. http_fields/productlistheader.py +33 -0
  50. http_fields/proxyauthenticate.py +17 -0
  51. http_fields/proxyauthenticationinfo.py +17 -0
  52. http_fields/proxyauthorization.py +17 -0
  53. http_fields/proxystatus.py +12 -0
  54. http_fields/py.typed +0 -0
  55. http_fields/range.py +37 -0
  56. http_fields/referer.py +24 -0
  57. http_fields/retryafter.py +45 -0
  58. http_fields/server.py +18 -0
  59. http_fields/setcookie.py +419 -0
  60. http_fields/stricttransportsecurity.py +96 -0
  61. http_fields/structuredfields.py +356 -0
  62. http_fields/structuredheaders.py +90 -0
  63. http_fields/te.py +36 -0
  64. http_fields/trailer.py +44 -0
  65. http_fields/upgrade.py +35 -0
  66. http_fields/uriheader.py +36 -0
  67. http_fields/useragent.py +18 -0
  68. http_fields/vary.py +40 -0
  69. http_fields/via.py +35 -0
  70. http_fields/visitors/__init__.py +1 -0
  71. http_fields/visitors/rfc3986.py +195 -0
  72. http_fields/visitors/rfc6265.py +62 -0
  73. http_fields/visitors/rfc6266.py +234 -0
  74. http_fields/visitors/rfc7230.py +80 -0
  75. http_fields/visitors/rfc7239.py +43 -0
  76. http_fields/visitors/rfc7240.py +77 -0
  77. http_fields/visitors/rfc7838.py +77 -0
  78. http_fields/visitors/rfc8288.py +56 -0
  79. http_fields/visitors/rfc9110/__init__.py +117 -0
  80. http_fields/visitors/rfc9110/_base.py +71 -0
  81. http_fields/visitors/rfc9110/accept.py +135 -0
  82. http_fields/visitors/rfc9110/acceptcharset.py +26 -0
  83. http_fields/visitors/rfc9110/acceptencoding.py +76 -0
  84. http_fields/visitors/rfc9110/acceptlanguage.py +55 -0
  85. http_fields/visitors/rfc9110/acceptranges.py +13 -0
  86. http_fields/visitors/rfc9110/allow.py +13 -0
  87. http_fields/visitors/rfc9110/authenticationinfo.py +19 -0
  88. http_fields/visitors/rfc9110/authorization.py +77 -0
  89. http_fields/visitors/rfc9110/authparam.py +40 -0
  90. http_fields/visitors/rfc9110/authscheme.py +17 -0
  91. http_fields/visitors/rfc9110/challenge.py +57 -0
  92. http_fields/visitors/rfc9110/comment.py +99 -0
  93. http_fields/visitors/rfc9110/connection.py +13 -0
  94. http_fields/visitors/rfc9110/contentlanguage.py +21 -0
  95. http_fields/visitors/rfc9110/contentlength.py +9 -0
  96. http_fields/visitors/rfc9110/contentrange.py +70 -0
  97. http_fields/visitors/rfc9110/contenttype.py +52 -0
  98. http_fields/visitors/rfc9110/date.py +12 -0
  99. http_fields/visitors/rfc9110/entitytag.py +66 -0
  100. http_fields/visitors/rfc9110/etag.py +12 -0
  101. http_fields/visitors/rfc9110/field.py +15 -0
  102. http_fields/visitors/rfc9110/host.py +32 -0
  103. http_fields/visitors/rfc9110/httpdate.py +167 -0
  104. http_fields/visitors/rfc9110/ifmatch.py +24 -0
  105. http_fields/visitors/rfc9110/ifmodifiedsince.py +12 -0
  106. http_fields/visitors/rfc9110/ifnonematch.py +26 -0
  107. http_fields/visitors/rfc9110/ifunmodifiedsince.py +12 -0
  108. http_fields/visitors/rfc9110/lastmodified.py +14 -0
  109. http_fields/visitors/rfc9110/location.py +9 -0
  110. http_fields/visitors/rfc9110/parameters.py +124 -0
  111. http_fields/visitors/rfc9110/product.py +39 -0
  112. http_fields/visitors/rfc9110/quotedstring.py +66 -0
  113. http_fields/visitors/rfc9110/range.py +100 -0
  114. http_fields/visitors/rfc9110/rangeunit.py +17 -0
  115. http_fields/visitors/rfc9110/retryafter.py +20 -0
  116. http_fields/visitors/rfc9110/server.py +16 -0
  117. http_fields/visitors/rfc9110/te.py +83 -0
  118. http_fields/visitors/rfc9110/token.py +18 -0
  119. http_fields/visitors/rfc9110/token68.py +18 -0
  120. http_fields/visitors/rfc9110/type.py +17 -0
  121. http_fields/visitors/rfc9110/upgrade.py +57 -0
  122. http_fields/visitors/rfc9110/useragent.py +16 -0
  123. http_fields/visitors/rfc9110/vary.py +10 -0
  124. http_fields/visitors/rfc9110/via.py +86 -0
  125. http_fields/visitors/rfc9110/weight.py +40 -0
  126. http_fields/visitors/rfc9110/wwwauthenticate.py +21 -0
  127. http_fields/visitors/rfc9111.py +92 -0
  128. http_fields/wwwauthenticate.py +24 -0
  129. http_fields-0.1.1.dist-info/METADATA +88 -0
  130. http_fields-0.1.1.dist-info/RECORD +132 -0
  131. http_fields-0.1.1.dist-info/WHEEL +4 -0
  132. http_fields-0.1.1.dist-info/licenses/LICENSE.txt +16 -0
@@ -0,0 +1,83 @@
1
+ from dataclasses import dataclass
2
+
3
+ from abnf import Node, NodeVisitor
4
+
5
+ from http_fields.visitors.rfc9110.parameters import Parameter
6
+ from http_fields.visitors.rfc9110.quotedstring import QuotedStringVisitor
7
+ from http_fields.visitors.rfc9110.token import Token, TokenVisitor
8
+ from http_fields.visitors.rfc9110.weight import Weight, WeightVisitor, as_qvalue
9
+
10
+ __all__ = ["TCoding", "TEVisitor"]
11
+
12
+
13
+ @dataclass(frozen=True)
14
+ class TCoding:
15
+ """A single TE t-coding: a transfer-coding (or "trailers") with an optional weight and
16
+ transfer-parameters."""
17
+
18
+ coding: Token
19
+ weight: Weight | None = None
20
+ parameters: tuple[Parameter, ...] = ()
21
+
22
+ def __init__(
23
+ self,
24
+ coding: str,
25
+ weight: Weight | None = None,
26
+ parameters: tuple[Parameter, ...] = (),
27
+ ) -> None:
28
+ # coding self-validates as a Token; weight and Parameter are already safe.
29
+ object.__setattr__(self, "coding", Token(coding))
30
+ object.__setattr__(self, "weight", weight)
31
+ object.__setattr__(self, "parameters", tuple(parameters))
32
+
33
+ def __str__(self) -> str:
34
+ parts = [self.coding, *(str(p) for p in self.parameters)]
35
+ if self.weight is not None:
36
+ parts.append(str(self.weight))
37
+ return ";".join(parts)
38
+
39
+
40
+ class TEVisitor(NodeVisitor):
41
+ visit_token = TokenVisitor()
42
+ visit_quoted_string = QuotedStringVisitor()
43
+ visit_weight = WeightVisitor()
44
+
45
+ def visit_transfer_parameter(self, node: Node) -> Parameter:
46
+ items = [x for x in map(self.visit, node.children) if x is not None]
47
+ return Parameter(str(items[0]), str(items[-1]))
48
+
49
+ def visit_transfer_coding(self, node: Node) -> tuple[Token, list[Parameter]]:
50
+ coding: Token | None = None
51
+ params: list[Parameter] = []
52
+ for child in node.children:
53
+ result = self.visit(child)
54
+ if isinstance(result, Parameter):
55
+ params.append(result)
56
+ elif isinstance(result, Token):
57
+ coding = result
58
+ assert coding is not None
59
+ return (coding, params)
60
+
61
+ def visit_t_codings(self, node: Node) -> TCoding:
62
+ if node.value.strip().lower() == "trailers":
63
+ return TCoding("trailers")
64
+ coding = ""
65
+ weight: Weight | None = None
66
+ params: list[Parameter] = []
67
+ for child in node.children:
68
+ result = self.visit(child)
69
+ if isinstance(result, tuple):
70
+ coding, params = result
71
+ elif isinstance(result, Weight):
72
+ weight = result
73
+ # the parser usually captures "q=..." as a transfer-parameter rather than a weight.
74
+ # Only treat it as a weight if it is an unquoted, in-range qvalue.
75
+ if weight is None and params and str(params[-1].name).lower() == "q":
76
+ qvalue = as_qvalue(params[-1].value)
77
+ if qvalue is not None:
78
+ weight = Weight(qvalue=qvalue)
79
+ params = params[:-1]
80
+ return TCoding(coding, weight, tuple(params))
81
+
82
+ def visit_te(self, node: Node) -> list[TCoding]:
83
+ return [x for x in map(self.visit, node.children) if isinstance(x, TCoding)]
@@ -0,0 +1,18 @@
1
+ from abnf import Node, NodeVisitor
2
+ from abnf.grammars import rfc9110
3
+
4
+ from http_fields.parsedobjs import CaselessMixin, ParsedStr
5
+
6
+
7
+ class Token(CaselessMixin, ParsedStr):
8
+ """Represents an RFC 9110 token."""
9
+
10
+ parser = rfc9110.Rule("token")
11
+
12
+
13
+ class TokenVisitor(NodeVisitor):
14
+ """Visits a token node."""
15
+
16
+ @staticmethod
17
+ def visit_token(node: Node) -> Token:
18
+ return Token(node.value, parse=False)
@@ -0,0 +1,18 @@
1
+ from abnf import Node, NodeVisitor
2
+ from abnf.grammars import rfc9110
3
+
4
+ from http_fields.parsedobjs import CaselessMixin, ParsedStr
5
+
6
+
7
+ class Token68(CaselessMixin, ParsedStr):
8
+ """Represents an RFC 9110 token68."""
9
+
10
+ parser = rfc9110.Rule("token68")
11
+
12
+
13
+ class Token68Visitor(NodeVisitor):
14
+ """Visits a token68 node."""
15
+
16
+ @staticmethod
17
+ def visit_token68(node: Node) -> Token68:
18
+ return Token68(node.value, parse=False)
@@ -0,0 +1,17 @@
1
+ from abnf import Node, NodeVisitor
2
+
3
+ import http_fields.visitors.rfc9110.token as token
4
+
5
+
6
+ class TypeVisitor(NodeVisitor):
7
+ visit_token = token.TokenVisitor()
8
+
9
+ def visit_type(self, node: Node):
10
+ return next(filter(None, map(self.visit, node.children)))
11
+
12
+
13
+ class SubtypeVisitor(NodeVisitor):
14
+ visit_token = token.TokenVisitor()
15
+
16
+ def visit_subtype(self, node: Node):
17
+ return next(filter(None, map(self.visit, node.children)))
@@ -0,0 +1,57 @@
1
+ from dataclasses import dataclass
2
+
3
+ from abnf import Node, NodeVisitor
4
+ from abnf.grammars import rfc9110
5
+
6
+ from http_fields.parsedobjs import ParsedStr
7
+
8
+ __all__ = ["Protocol", "ProtocolName", "ProtocolVersion", "UpgradeVisitor"]
9
+
10
+
11
+ class ProtocolName(ParsedStr):
12
+ """An RFC 9110 protocol-name token. Self-validating."""
13
+
14
+ parser = rfc9110.Rule("protocol-name")
15
+
16
+
17
+ class ProtocolVersion(ParsedStr):
18
+ """An RFC 9110 protocol-version token. Self-validating."""
19
+
20
+ parser = rfc9110.Rule("protocol-version")
21
+
22
+
23
+ @dataclass(frozen=True)
24
+ class Protocol:
25
+ """An RFC 9110 protocol: a name with an optional version, e.g. ``HTTP/2``."""
26
+
27
+ name: ProtocolName
28
+ version: ProtocolVersion | None = None
29
+
30
+ def __init__(self, name: str, version: str | None = None) -> None:
31
+ # Coerce str -> leaf (which validates); an existing leaf passes through
32
+ # unchanged, so a value from the visitor is not re-parsed.
33
+ object.__setattr__(self, "name", ProtocolName(name))
34
+ object.__setattr__(
35
+ self, "version", ProtocolVersion(version) if version is not None else None
36
+ )
37
+
38
+ def __str__(self) -> str:
39
+ return self.name + (f"/{self.version}" if self.version else "")
40
+
41
+
42
+ class UpgradeVisitor(NodeVisitor):
43
+ @staticmethod
44
+ def visit_protocol_name(node: Node) -> str:
45
+ return node.value
46
+
47
+ @staticmethod
48
+ def visit_protocol_version(node: Node) -> str:
49
+ return node.value
50
+
51
+ def visit_protocol(self, node: Node) -> Protocol:
52
+ items = [x for x in map(self.visit, node.children) if x is not None]
53
+ version = ProtocolVersion(items[1], parse=False) if len(items) == 2 else None
54
+ return Protocol(ProtocolName(items[0], parse=False), version)
55
+
56
+ def visit_upgrade(self, node: Node) -> list[Protocol]:
57
+ return [x for x in map(self.visit, node.children) if isinstance(x, Protocol)]
@@ -0,0 +1,16 @@
1
+ from abnf import Node, NodeVisitor
2
+
3
+ import http_fields.visitors.rfc9110.comment as comment
4
+ import http_fields.visitors.rfc9110.product as product
5
+ import http_fields.visitors.rfc9110.token as token
6
+
7
+ __all__ = ["UserAgentVisitor"]
8
+
9
+
10
+ class UserAgentVisitor(NodeVisitor):
11
+ visit_comment = comment.CommentVisitor()
12
+ visit_product = product.ProductVisitor()
13
+ visit_token = token.TokenVisitor()
14
+
15
+ def visit_user_agent(self, node: Node) -> list[product.Product | comment.Comment]:
16
+ return list(filter(None, map(self.visit, node.children)))
@@ -0,0 +1,10 @@
1
+ from abnf import Node, NodeVisitor
2
+
3
+
4
+ class VaryVisitor(NodeVisitor):
5
+ def visit_vary(self, node: Node) -> list[str]:
6
+ return list(filter(None, map(self.visit, node.children)))
7
+
8
+ @staticmethod
9
+ def visit_field_name(node: Node):
10
+ return node.value
@@ -0,0 +1,86 @@
1
+ from dataclasses import dataclass
2
+
3
+ from abnf import Node, NodeVisitor
4
+ from abnf.grammars import rfc9110
5
+
6
+ from http_fields.parsedobjs import ParsedStr
7
+
8
+ __all__ = ["Comment", "ReceivedBy", "ReceivedProtocol", "ViaElement", "ViaVisitor"]
9
+
10
+
11
+ class ReceivedProtocol(ParsedStr):
12
+ """An RFC 9110 received-protocol (e.g. ``HTTP/1.1``). Self-validating."""
13
+
14
+ parser = rfc9110.Rule("received-protocol")
15
+
16
+
17
+ class ReceivedBy(ParsedStr):
18
+ """An RFC 9110 received-by (a host[:port] or pseudonym). Self-validating."""
19
+
20
+ parser = rfc9110.Rule("received-by")
21
+
22
+
23
+ class Comment(ParsedStr):
24
+ """An RFC 9110 comment, including its enclosing parentheses. Self-validating."""
25
+
26
+ parser = rfc9110.Rule("comment")
27
+
28
+
29
+ @dataclass(frozen=True)
30
+ class ViaElement:
31
+ """One hop in a Via header: a received-protocol, a received-by, and an optional comment."""
32
+
33
+ received_protocol: ReceivedProtocol
34
+ received_by: ReceivedBy
35
+ comment: Comment | None = None
36
+
37
+ def __init__(
38
+ self,
39
+ received_protocol: str,
40
+ received_by: str,
41
+ comment: str | None = None,
42
+ ) -> None:
43
+ # Each field self-validates as a leaf; an existing leaf passes through unchanged.
44
+ object.__setattr__(
45
+ self, "received_protocol", ReceivedProtocol(received_protocol)
46
+ )
47
+ object.__setattr__(self, "received_by", ReceivedBy(received_by))
48
+ object.__setattr__(
49
+ self, "comment", Comment(comment) if comment is not None else None
50
+ )
51
+
52
+ def __str__(self) -> str:
53
+ text = f"{self.received_protocol} {self.received_by}"
54
+ if self.comment:
55
+ text += f" {self.comment}"
56
+ return text
57
+
58
+
59
+ def _element(proto: str, by: str, comment: str | None) -> ViaElement:
60
+ # Build from already-validated node text (parse=False), so construction does not re-parse.
61
+ return ViaElement(
62
+ ReceivedProtocol(proto, parse=False),
63
+ ReceivedBy(by, parse=False),
64
+ Comment(comment, parse=False) if comment is not None else None,
65
+ )
66
+
67
+
68
+ class ViaVisitor(NodeVisitor):
69
+ def visit_via(self, node: Node) -> list[ViaElement]:
70
+ # the Via node is a flat sequence; each element starts at a received-protocol.
71
+ elements: list[ViaElement] = []
72
+ proto: str | None = None
73
+ by: str = ""
74
+ comment: str | None = None
75
+ for child in node.children:
76
+ if child.name == "received-protocol":
77
+ if proto is not None:
78
+ elements.append(_element(proto, by, comment))
79
+ proto, by, comment = child.value, "", None
80
+ elif child.name == "received-by":
81
+ by = child.value
82
+ elif child.name == "comment":
83
+ comment = child.value
84
+ if proto is not None:
85
+ elements.append(_element(proto, by, comment))
86
+ return elements
@@ -0,0 +1,40 @@
1
+ import re
2
+ from dataclasses import dataclass
3
+
4
+ from abnf import Node, NodeVisitor
5
+
6
+ __all__ = ["Weight", "WeightVisitor", "as_qvalue"]
7
+
8
+ # qvalue = ( "0" [ "." 0*3DIGIT ] ) / ( "1" [ "." 0*3("0") ] ) -- unquoted, 0..1.
9
+ _QVALUE_RE = re.compile(r"^(?:0(?:\.\d{0,3})?|1(?:\.0{0,3})?)$")
10
+
11
+
12
+ def as_qvalue(value: object) -> float | None:
13
+ """Return the float weight if ``value`` is a syntactically valid, unquoted
14
+ qvalue in [0, 1]; otherwise None (so a quoted or out-of-range "q" stays an
15
+ ordinary parameter rather than crashing or being silently clamped)."""
16
+ text = str(value)
17
+ return float(text) if _QVALUE_RE.match(text) else None
18
+
19
+
20
+ @dataclass(frozen=True)
21
+ class Weight:
22
+ qvalue: float
23
+
24
+ def __str__(self):
25
+ return f"q={self.qvalue:.3f}".rstrip("0").rstrip(".")
26
+
27
+
28
+ class WeightVisitor(NodeVisitor):
29
+ def visit_weight(self, node: Node) -> Weight:
30
+ # We ignore everything other than the qvalue. Filter on "is not None"
31
+ # rather than truthiness: a qvalue of 0.0 is valid ("not acceptable")
32
+ # but falsy, so filter(None, ...) would wrongly discard it.
33
+ qvalue: float = next(
34
+ value for value in map(self.visit, node.children) if value is not None
35
+ )
36
+ return Weight(qvalue=qvalue)
37
+
38
+ @staticmethod
39
+ def visit_qvalue(node: Node):
40
+ return float(node.value)
@@ -0,0 +1,21 @@
1
+ from abnf import Node, NodeVisitor
2
+
3
+ import http_fields.visitors.rfc9110.challenge as challenge
4
+
5
+
6
+ class WWWAuthenticateVisitor(NodeVisitor):
7
+ visit_challenge = challenge.ChallengeVisitor()
8
+
9
+ def visit_www_authenticate(
10
+ self, node: Node
11
+ ) -> list[challenge.TokenChallenge | challenge.AuthParamChallenge]:
12
+ return list(filter(None, map(self.visit, node.children)))
13
+
14
+
15
+ class ProxyAuthenticateVisitor(NodeVisitor):
16
+ visit_challenge = challenge.ChallengeVisitor()
17
+
18
+ def visit_proxy_authenticate(
19
+ self, node: Node
20
+ ) -> list[challenge.TokenChallenge | challenge.AuthParamChallenge]:
21
+ return list(filter(None, map(self.visit, node.children)))
@@ -0,0 +1,92 @@
1
+ from dataclasses import dataclass
2
+ from datetime import datetime
3
+ from typing import Any
4
+
5
+ from abnf import Node, NodeVisitor
6
+
7
+ from http_fields.parsedobjs import NonNegativeInt
8
+ from http_fields.visitors.rfc9110 import (
9
+ HttpDateVisitor,
10
+ QuotedString,
11
+ QuotedStringVisitor,
12
+ Token,
13
+ TokenVisitor,
14
+ )
15
+
16
+
17
+ class AgeVisitor(NodeVisitor):
18
+ @staticmethod
19
+ def visit_age(node: Node):
20
+ return NonNegativeInt(node.value)
21
+
22
+
23
+ @dataclass(frozen=True)
24
+ class CacheDirective:
25
+ name: Token
26
+ value: Any = True
27
+
28
+ def __init__(self, name: str, value: Any = True) -> None:
29
+ object.__setattr__(self, "name", Token(name))
30
+ resolved: Any
31
+ if name == "max-stale":
32
+ # RFC 9111 section 5.2.1.2: max-stale may be used with no value,
33
+ # meaning "stale responses of any age".
34
+ resolved = True if value is None or value is True else NonNegativeInt(value)
35
+ elif name in ["max-age", "s-maxage", "min-fresh"]:
36
+ if value is None:
37
+ raise ValueError(f"Cache directive '{name}' requires a value.")
38
+ resolved = NonNegativeInt(value)
39
+ elif name in [
40
+ "immutable",
41
+ "must-revalidate",
42
+ "must-understand",
43
+ "no-store",
44
+ "no-transform",
45
+ "only-if-cached",
46
+ "proxy-revalidate",
47
+ "public",
48
+ ]:
49
+ # immutable is defined in RFC 8246, a proposed standard. But the directive appears to be in use.
50
+ resolved = True
51
+ elif name in ["no-cache", "private"]:
52
+ # should consider parsing value to ensure it matches #field-name.
53
+ resolved = QuotedString(value) if isinstance(value, str) else True
54
+ else:
55
+ # extension (non-standard) directive: keep any value it carries.
56
+ if isinstance(value, str):
57
+ try:
58
+ resolved = Token(value)
59
+ except ValueError:
60
+ resolved = QuotedString(value)
61
+ else:
62
+ resolved = True
63
+ object.__setattr__(self, "value", resolved)
64
+
65
+ def __str__(self):
66
+ return str(self.name) if self.value is True else f"{self.name}={self.value}"
67
+
68
+
69
+ class CacheDirectiveVisitor(NodeVisitor):
70
+ visit_quoted_string = QuotedStringVisitor()
71
+ visit_token = TokenVisitor()
72
+
73
+ def visit_cache_directive(self, node: Node) -> CacheDirective:
74
+ items = filter(None, map(self.visit, node.children))
75
+ name = next(items)
76
+ value = next(items, None)
77
+ return CacheDirective(name, value)
78
+
79
+
80
+ class CacheControlVisitor(NodeVisitor):
81
+ visit_cache_directive = CacheDirectiveVisitor()
82
+
83
+ def visit_cache_control(self, node: Node) -> list[CacheDirective]:
84
+ directives = list(filter(None, map(self.visit, node.children)))
85
+ return directives
86
+
87
+
88
+ class ExpiresVisitor(NodeVisitor):
89
+ visit_http_date = HttpDateVisitor()
90
+
91
+ def visit_expires(self, node: Node) -> datetime:
92
+ return next(filter(None, map(self.visit, node.children)))
@@ -0,0 +1,24 @@
1
+ """WWW-Authenticate header class."""
2
+
3
+ from typing import ClassVar
4
+
5
+ from abnf import Rule
6
+ from abnf.grammars import rfc9110
7
+
8
+ from http_fields.authbases import ChallengeListHeader
9
+ from http_fields.visitors.rfc9110 import (
10
+ AuthParam,
11
+ AuthParamChallenge,
12
+ TokenChallenge,
13
+ WWWAuthenticateVisitor,
14
+ )
15
+
16
+ __all__ = ["AuthParam", "AuthParamChallenge", "TokenChallenge", "WWWAuthenticate"]
17
+
18
+
19
+ class WWWAuthenticate(ChallengeListHeader):
20
+ """WWW-Authenticate header, as defined by RFC 9110."""
21
+
22
+ name: ClassVar[str] = "www-authenticate"
23
+ rule: ClassVar[Rule] = rfc9110.Rule("WWW-Authenticate")
24
+ visitor = WWWAuthenticateVisitor()
@@ -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
+ ```