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.
- http_fields/__init__.py +193 -0
- http_fields/accept.py +37 -0
- http_fields/acceptcharset.py +48 -0
- http_fields/acceptencoding.py +42 -0
- http_fields/acceptlanguage.py +47 -0
- http_fields/acceptranges.py +35 -0
- http_fields/accesscontrol.py +197 -0
- http_fields/age.py +34 -0
- http_fields/allow.py +46 -0
- http_fields/altsvc.py +70 -0
- http_fields/authbases.py +79 -0
- http_fields/authenticationinfo.py +19 -0
- http_fields/authorization.py +23 -0
- http_fields/cachecontrol.py +119 -0
- http_fields/cachestatus.py +12 -0
- http_fields/connection.py +35 -0
- http_fields/contentdigest.py +19 -0
- http_fields/contentdisposition.py +66 -0
- http_fields/contentencoding.py +45 -0
- http_fields/contentlanguage.py +38 -0
- http_fields/contentlength.py +34 -0
- http_fields/contentlocation.py +24 -0
- http_fields/contentrange.py +74 -0
- http_fields/contenttype.py +104 -0
- http_fields/cookie.py +38 -0
- http_fields/date.py +17 -0
- http_fields/dateheader.py +32 -0
- http_fields/entitytaglist.py +39 -0
- http_fields/etag.py +46 -0
- http_fields/expect.py +52 -0
- http_fields/expires.py +17 -0
- http_fields/forwarded.py +36 -0
- http_fields/fromheader.py +42 -0
- http_fields/header.py +250 -0
- http_fields/host.py +44 -0
- http_fields/ifmatch.py +24 -0
- http_fields/ifmodifiedsince.py +17 -0
- http_fields/ifnonematch.py +24 -0
- http_fields/ifrange.py +49 -0
- http_fields/ifunmodifiedsince.py +17 -0
- http_fields/lastmodified.py +17 -0
- http_fields/link.py +35 -0
- http_fields/location.py +24 -0
- http_fields/maxforwards.py +32 -0
- http_fields/origin.py +42 -0
- http_fields/parsedobjs.py +62 -0
- http_fields/prefer.py +64 -0
- http_fields/priority.py +55 -0
- http_fields/productlistheader.py +33 -0
- http_fields/proxyauthenticate.py +17 -0
- http_fields/proxyauthenticationinfo.py +17 -0
- http_fields/proxyauthorization.py +17 -0
- http_fields/proxystatus.py +12 -0
- http_fields/py.typed +0 -0
- http_fields/range.py +37 -0
- http_fields/referer.py +24 -0
- http_fields/retryafter.py +45 -0
- http_fields/server.py +18 -0
- http_fields/setcookie.py +419 -0
- http_fields/stricttransportsecurity.py +96 -0
- http_fields/structuredfields.py +356 -0
- http_fields/structuredheaders.py +90 -0
- http_fields/te.py +36 -0
- http_fields/trailer.py +44 -0
- http_fields/upgrade.py +35 -0
- http_fields/uriheader.py +36 -0
- http_fields/useragent.py +18 -0
- http_fields/vary.py +40 -0
- http_fields/via.py +35 -0
- http_fields/visitors/__init__.py +1 -0
- http_fields/visitors/rfc3986.py +195 -0
- http_fields/visitors/rfc6265.py +62 -0
- http_fields/visitors/rfc6266.py +234 -0
- http_fields/visitors/rfc7230.py +80 -0
- http_fields/visitors/rfc7239.py +43 -0
- http_fields/visitors/rfc7240.py +77 -0
- http_fields/visitors/rfc7838.py +77 -0
- http_fields/visitors/rfc8288.py +56 -0
- http_fields/visitors/rfc9110/__init__.py +117 -0
- http_fields/visitors/rfc9110/_base.py +71 -0
- http_fields/visitors/rfc9110/accept.py +135 -0
- http_fields/visitors/rfc9110/acceptcharset.py +26 -0
- http_fields/visitors/rfc9110/acceptencoding.py +76 -0
- http_fields/visitors/rfc9110/acceptlanguage.py +55 -0
- http_fields/visitors/rfc9110/acceptranges.py +13 -0
- http_fields/visitors/rfc9110/allow.py +13 -0
- http_fields/visitors/rfc9110/authenticationinfo.py +19 -0
- http_fields/visitors/rfc9110/authorization.py +77 -0
- http_fields/visitors/rfc9110/authparam.py +40 -0
- http_fields/visitors/rfc9110/authscheme.py +17 -0
- http_fields/visitors/rfc9110/challenge.py +57 -0
- http_fields/visitors/rfc9110/comment.py +99 -0
- http_fields/visitors/rfc9110/connection.py +13 -0
- http_fields/visitors/rfc9110/contentlanguage.py +21 -0
- http_fields/visitors/rfc9110/contentlength.py +9 -0
- http_fields/visitors/rfc9110/contentrange.py +70 -0
- http_fields/visitors/rfc9110/contenttype.py +52 -0
- http_fields/visitors/rfc9110/date.py +12 -0
- http_fields/visitors/rfc9110/entitytag.py +66 -0
- http_fields/visitors/rfc9110/etag.py +12 -0
- http_fields/visitors/rfc9110/field.py +15 -0
- http_fields/visitors/rfc9110/host.py +32 -0
- http_fields/visitors/rfc9110/httpdate.py +167 -0
- http_fields/visitors/rfc9110/ifmatch.py +24 -0
- http_fields/visitors/rfc9110/ifmodifiedsince.py +12 -0
- http_fields/visitors/rfc9110/ifnonematch.py +26 -0
- http_fields/visitors/rfc9110/ifunmodifiedsince.py +12 -0
- http_fields/visitors/rfc9110/lastmodified.py +14 -0
- http_fields/visitors/rfc9110/location.py +9 -0
- http_fields/visitors/rfc9110/parameters.py +124 -0
- http_fields/visitors/rfc9110/product.py +39 -0
- http_fields/visitors/rfc9110/quotedstring.py +66 -0
- http_fields/visitors/rfc9110/range.py +100 -0
- http_fields/visitors/rfc9110/rangeunit.py +17 -0
- http_fields/visitors/rfc9110/retryafter.py +20 -0
- http_fields/visitors/rfc9110/server.py +16 -0
- http_fields/visitors/rfc9110/te.py +83 -0
- http_fields/visitors/rfc9110/token.py +18 -0
- http_fields/visitors/rfc9110/token68.py +18 -0
- http_fields/visitors/rfc9110/type.py +17 -0
- http_fields/visitors/rfc9110/upgrade.py +57 -0
- http_fields/visitors/rfc9110/useragent.py +16 -0
- http_fields/visitors/rfc9110/vary.py +10 -0
- http_fields/visitors/rfc9110/via.py +86 -0
- http_fields/visitors/rfc9110/weight.py +40 -0
- http_fields/visitors/rfc9110/wwwauthenticate.py +21 -0
- http_fields/visitors/rfc9111.py +92 -0
- http_fields/wwwauthenticate.py +24 -0
- http_fields-0.1.1.dist-info/METADATA +88 -0
- http_fields-0.1.1.dist-info/RECORD +132 -0
- http_fields-0.1.1.dist-info/WHEEL +4 -0
- 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,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
|
+
```
|