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,77 @@
1
+ from dataclasses import dataclass
2
+
3
+ from abnf import Node, NodeVisitor
4
+
5
+ import http_fields.visitors.rfc9110.authparam as authparam
6
+ import http_fields.visitors.rfc9110.token68 as token68
7
+ from http_fields.visitors.rfc9110.authscheme import AuthScheme
8
+
9
+
10
+ @dataclass(frozen=True)
11
+ class AuthParamCredentials:
12
+ scheme: AuthScheme
13
+ auth_params: tuple[authparam.AuthParam, ...]
14
+
15
+ def __init__(self, scheme: str, auth_params: list[authparam.AuthParam]):
16
+ object.__setattr__(self, "scheme", AuthScheme(scheme))
17
+ object.__setattr__(self, "auth_params", tuple(auth_params))
18
+
19
+ def __str__(self):
20
+ params = ",".join([str(param) for param in self.auth_params])
21
+ return f"{self.scheme}{' ' + params if params else ''}"
22
+
23
+
24
+ @dataclass(frozen=True)
25
+ class TokenCredentials:
26
+ scheme: AuthScheme
27
+ token: token68.Token68 | None
28
+
29
+ def __init__(self, scheme: str, token: str | None):
30
+ object.__setattr__(self, "scheme", AuthScheme(scheme))
31
+ object.__setattr__(self, "token", token68.Token68(token) if token else None)
32
+
33
+ def __str__(self):
34
+ return f"{self.scheme}{' ' + self.token if self.token else ''}"
35
+
36
+
37
+ class CredentialsVisitor(NodeVisitor):
38
+ visit_token68 = token68.Token68Visitor()
39
+ visit_auth_param = authparam.AuthParamVisitor()
40
+
41
+ def visit_auth_scheme(self, node: Node):
42
+ return AuthScheme(node.value)
43
+
44
+ def visit_credentials(self, node: Node):
45
+ items = filter(None, map(self.visit, node.children))
46
+ auth_scheme = next(items)
47
+ assert isinstance(auth_scheme, AuthScheme)
48
+ try:
49
+ next_item = next(items)
50
+ except StopIteration:
51
+ return TokenCredentials(scheme=auth_scheme, token=None)
52
+ else:
53
+ if isinstance(next_item, token68.Token68):
54
+ return TokenCredentials(scheme=auth_scheme, token=next_item)
55
+ elif isinstance(next_item, authparam.AuthParam):
56
+ auth_params = [next_item] + list(items)
57
+ return AuthParamCredentials(scheme=auth_scheme, auth_params=auth_params)
58
+ else: # pragma: no cover
59
+ raise AssertionError()
60
+
61
+
62
+ class AuthorizationVisitor(NodeVisitor):
63
+ """NodeVisitor subclass for authorization header value."""
64
+
65
+ visit_credentials = CredentialsVisitor()
66
+
67
+ def visit_authorization(self, node: Node):
68
+ return next(filter(None, map(self.visit, node.children)))
69
+
70
+
71
+ class ProxyAuthorizationVisitor(NodeVisitor):
72
+ """NodeVisitor subclass for the Proxy-Authorization header value."""
73
+
74
+ visit_credentials = CredentialsVisitor()
75
+
76
+ def visit_proxy_authorization(self, node: Node):
77
+ return next(filter(None, map(self.visit, node.children)))
@@ -0,0 +1,40 @@
1
+ from dataclasses import dataclass
2
+
3
+ from abnf import Node, NodeVisitor
4
+
5
+ import http_fields.visitors.rfc9110.quotedstring as quotedstring
6
+ import http_fields.visitors.rfc9110.token as token
7
+
8
+
9
+ @dataclass(frozen=True)
10
+ class AuthParam:
11
+ name: token.Token
12
+ value: token.Token | quotedstring.QuotedString
13
+
14
+ def __init__(self, name: str, value: str):
15
+ object.__setattr__(self, "name", token.Token(name))
16
+
17
+ if name.lower() == "realm":
18
+ # realm value should be quoted. See https://tools.ietf.org/html/rfc7235#section-2.2.
19
+ object.__setattr__(self, "value", quotedstring.QuotedString(value))
20
+ else:
21
+ try:
22
+ object.__setattr__(self, "value", token.Token(value))
23
+ except ValueError:
24
+ object.__setattr__(self, "value", quotedstring.QuotedString(value))
25
+
26
+ def __str__(self):
27
+ return f"{self.name}={self.value}"
28
+
29
+
30
+ class AuthParamVisitor(NodeVisitor):
31
+ visit_token = token.TokenVisitor()
32
+ visit_quoted_string = quotedstring.QuotedStringVisitor()
33
+
34
+ def visit_auth_param(self, node: Node):
35
+ name: token.Token
36
+ value: token.Token | quotedstring.QuotedString
37
+ name, value = filter(None, map(self.visit, node.children))
38
+ assert isinstance(name, token.Token)
39
+ assert isinstance(value, token.Token | quotedstring.QuotedString)
40
+ return AuthParam(name=name, value=value)
@@ -0,0 +1,17 @@
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 AuthScheme(CaselessMixin, ParsedStr):
8
+ """Represents an RFC 9110 auth-scheme. Schemes are case-insensitive
9
+ (RFC 9110 section 11.1), so two schemes differing only in case compare equal;
10
+ the original casing is preserved on serialization."""
11
+
12
+ parser = rfc9110.Rule("auth-scheme")
13
+
14
+
15
+ class AuthSchemeVisitor(NodeVisitor):
16
+ def visit_auth_scheme(self, node: Node):
17
+ return AuthScheme(node.value)
@@ -0,0 +1,57 @@
1
+ from dataclasses import dataclass
2
+
3
+ from abnf import Node, NodeVisitor
4
+
5
+ import http_fields.visitors.rfc9110.authparam as authparam
6
+ import http_fields.visitors.rfc9110.authscheme as authscheme
7
+ import http_fields.visitors.rfc9110.token68 as token68
8
+
9
+
10
+ @dataclass(frozen=True)
11
+ class TokenChallenge:
12
+ scheme: authscheme.AuthScheme
13
+ token: token68.Token68 | None
14
+
15
+ def __init__(self, scheme: str, token: str | None):
16
+ object.__setattr__(self, "scheme", authscheme.AuthScheme(scheme))
17
+ object.__setattr__(self, "token", token68.Token68(token) if token else None)
18
+
19
+ def __str__(self):
20
+ return f"{self.scheme}{' ' + self.token if self.token else ''}"
21
+
22
+
23
+ @dataclass(frozen=True)
24
+ class AuthParamChallenge:
25
+ scheme: authscheme.AuthScheme
26
+ auth_params: tuple[authparam.AuthParam, ...]
27
+
28
+ def __init__(self, scheme: str, auth_params: list[authparam.AuthParam]):
29
+ object.__setattr__(self, "scheme", authscheme.AuthScheme(scheme))
30
+ object.__setattr__(self, "auth_params", tuple(auth_params))
31
+
32
+ def __str__(self):
33
+ params = ",".join([str(param) for param in self.auth_params])
34
+ return f"{self.scheme}{' ' + params if params else ''}"
35
+
36
+
37
+ class ChallengeVisitor(NodeVisitor):
38
+ visit_token68 = token68.Token68Visitor()
39
+ visit_auth_param = authparam.AuthParamVisitor()
40
+ visit_auth_scheme = authscheme.AuthSchemeVisitor()
41
+
42
+ def visit_challenge(self, node: Node):
43
+ items = filter(None, map(self.visit, node.children))
44
+ auth_scheme = next(items)
45
+ assert isinstance(auth_scheme, authscheme.AuthScheme)
46
+ try:
47
+ next_item = next(items)
48
+ except StopIteration:
49
+ return TokenChallenge(scheme=auth_scheme, token=None)
50
+ else:
51
+ if isinstance(next_item, token68.Token68):
52
+ return TokenChallenge(scheme=auth_scheme, token=next_item)
53
+ elif isinstance(next_item, authparam.AuthParam):
54
+ auth_params = [next_item] + list(items)
55
+ return AuthParamChallenge(scheme=auth_scheme, auth_params=auth_params)
56
+ else: # pragma: no cover
57
+ raise AssertionError()
@@ -0,0 +1,99 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+
5
+ from abnf import Node, NodeVisitor, ParseError
6
+ from abnf.grammars import rfc9110
7
+
8
+ import http_fields.visitors.rfc9110.quotedstring as quotedstring
9
+
10
+
11
+ def _escape_ctext(text: str) -> str:
12
+ # ctext excludes "(", ")" and "\"; these appear in parsed text only because a
13
+ # quoted-pair was unescaped, so re-escape them to keep the comment parseable.
14
+ return "".join("\\" + c if c in "()\\" else c for c in text)
15
+
16
+
17
+ @dataclass(frozen=True)
18
+ class Comment:
19
+ """An RFC 9110 comment: a parenthesised sequence of text runs and nested comments.
20
+
21
+ Frozen so it stays immutable (and stably hashable) while embedded in a frozen
22
+ User-Agent/Server/Via header. ``__eq__``/``__hash__``/``__repr__`` are generated;
23
+ ``__str__`` re-escapes text runs so the serialized comment round-trips.
24
+
25
+ Each string argument is parsed as comment *content* (``*( ctext / quoted-pair /
26
+ comment )``): embedded ``(...)`` become nested Comment objects, ``\\)`` is a
27
+ quoted-pair, and invalid content (CR/LF/NUL or an unescaped/unbalanced parenthesis)
28
+ raises ``ValueError``. So ``Comment(s)`` accepts exactly what the parser produces.
29
+ """
30
+
31
+ items: tuple[str | Comment, ...] = ()
32
+
33
+ def __init__(self, *items: str | Comment) -> None:
34
+ parsed: list[str | Comment] = []
35
+ for item in items:
36
+ if isinstance(item, Comment):
37
+ parsed.append(item)
38
+ else:
39
+ parsed.extend(_parse_content(item))
40
+ object.__setattr__(self, "items", tuple(parsed))
41
+
42
+ @classmethod
43
+ def _from_items(cls, items: tuple[str | Comment, ...]) -> Comment:
44
+ # Build from already-structured, already-unescaped items (the visitor's
45
+ # output), bypassing the string-parsing done by __init__.
46
+ obj = object.__new__(cls)
47
+ object.__setattr__(obj, "items", items)
48
+ return obj
49
+
50
+ def __str__(self) -> str:
51
+ return (
52
+ "("
53
+ + "".join(
54
+ str(item) if isinstance(item, Comment) else _escape_ctext(str(item))
55
+ for item in self.items
56
+ )
57
+ + ")"
58
+ )
59
+
60
+
61
+ def _parse_content(content: str) -> tuple[str | Comment, ...]:
62
+ """Parse a comment-content string into its (text-run / nested-Comment) items."""
63
+ try:
64
+ node = rfc9110.Rule("comment").parse_all(f"({content})")
65
+ except ParseError as exc:
66
+ raise ValueError(f"Invalid comment content: {content!r}.") from exc
67
+ return _CONTENT_VISITOR.visit(node).items
68
+
69
+
70
+ class CommentVisitor(NodeVisitor):
71
+ visit_quoted_pair = quotedstring.QuotedPairVisitor()
72
+
73
+ @staticmethod
74
+ def visit_ctext(node: Node):
75
+ return node.value
76
+
77
+ def visit_comment(self, node: Node):
78
+ items = filter(None, map(self.visit, node.children))
79
+ comment_items: list[str | Comment] = []
80
+
81
+ text: list[str] = []
82
+ for item in items:
83
+ if isinstance(item, str):
84
+ text.append(item)
85
+ elif isinstance(item, Comment):
86
+ comment_items.append("".join(text))
87
+ text = []
88
+ comment_items.append(item)
89
+ else: # pragma: no cover
90
+ raise AssertionError()
91
+ if text:
92
+ comment_items.append("".join(text))
93
+ # Build directly from the (already-unescaped) items; passing them back
94
+ # through Comment.__init__ would try to re-parse them as content. This is the
95
+ # module's own internal factory (same file), not cross-module private access.
96
+ return Comment._from_items(tuple(comment_items)) # pyright: ignore[reportPrivateUsage]
97
+
98
+
99
+ _CONTENT_VISITOR = CommentVisitor()
@@ -0,0 +1,13 @@
1
+ from abnf import Node, NodeVisitor
2
+
3
+ from http_fields.visitors.rfc9110.token import Token, TokenVisitor
4
+
5
+
6
+ class ConnectionVisitor(NodeVisitor):
7
+ visit_token = TokenVisitor()
8
+
9
+ def visit_connection_option(self, node: Node) -> list[Token]:
10
+ return next(filter(None, map(self.visit, node.children)))
11
+
12
+ def visit_connection(self, node: Node) -> list[Token]:
13
+ return list(filter(None, map(self.visit, node.children)))
@@ -0,0 +1,21 @@
1
+ from abnf import Node, NodeVisitor
2
+ from abnf.grammars import rfc9110
3
+
4
+ from http_fields.parsedobjs import CaselessMixin, ParsedStr
5
+
6
+ __all__ = ["ContentLanguageVisitor", "LanguageTag"]
7
+
8
+
9
+ class LanguageTag(CaselessMixin, ParsedStr):
10
+ """Represents an RFC 9110 language-tag (BCP 47), matched case-insensitively."""
11
+
12
+ parser = rfc9110.Rule("language-tag")
13
+
14
+
15
+ class ContentLanguageVisitor(NodeVisitor):
16
+ @staticmethod
17
+ def visit_language_tag(node: Node) -> str:
18
+ return LanguageTag(node.value, parse=False)
19
+
20
+ def visit_content_language(self, node: Node) -> list[str]:
21
+ return [x for x in map(self.visit, node.children) if x is not None]
@@ -0,0 +1,9 @@
1
+ from abnf import Node, NodeVisitor
2
+
3
+ from http_fields.parsedobjs import NonNegativeInt
4
+
5
+
6
+ class ContentLengthVisitor(NodeVisitor):
7
+ @staticmethod
8
+ def visit_content_length(node: Node):
9
+ return NonNegativeInt(node.value)
@@ -0,0 +1,70 @@
1
+ from dataclasses import dataclass
2
+ from typing import Literal
3
+
4
+ from abnf import Node, NodeVisitor
5
+
6
+ from http_fields.parsedobjs import NonNegativeInt
7
+ from http_fields.visitors.rfc9110.rangeunit import RangeUnit, RangeUnitVisitor
8
+
9
+
10
+ @dataclass(frozen=True)
11
+ class RangeResp:
12
+ first_pos: NonNegativeInt
13
+ last_pos: NonNegativeInt
14
+ complete_length: NonNegativeInt | None
15
+
16
+
17
+ @dataclass(frozen=True)
18
+ class UnsatisfiedRange:
19
+ complete_length: NonNegativeInt | None
20
+
21
+
22
+ class ContentRangeVisitor(NodeVisitor):
23
+ visit_range_unit = RangeUnitVisitor()
24
+
25
+ @staticmethod
26
+ def visit_literal(node: Node):
27
+ return "*" if node.value == "*" else None
28
+
29
+ @staticmethod
30
+ def visit_complete_length(node: Node):
31
+ return NonNegativeInt(node.value)
32
+
33
+ def visit_unsatisfied_range(self, node: Node) -> UnsatisfiedRange:
34
+ item = next(x for x in map(self.visit, node.children) if x is not None)
35
+ return UnsatisfiedRange(item if isinstance(item, NonNegativeInt) else None)
36
+
37
+ @staticmethod
38
+ def visit_first_pos(node: Node):
39
+ return NonNegativeInt(node.value)
40
+
41
+ @staticmethod
42
+ def visit_last_pos(node: Node):
43
+ return NonNegativeInt(node.value)
44
+
45
+ def visit_incl_range(self, node: Node):
46
+ first_pos: NonNegativeInt
47
+ last_pos: NonNegativeInt
48
+ first_pos, last_pos = (
49
+ x for x in map(self.visit, node.children) if x is not None
50
+ )
51
+ return (first_pos, last_pos)
52
+
53
+ def visit_range_resp(self, node: Node):
54
+ incl_range: tuple[NonNegativeInt, NonNegativeInt]
55
+ complete_length: NonNegativeInt | Literal["*"]
56
+ incl_range, complete_length = (
57
+ x for x in map(self.visit, node.children) if x is not None
58
+ )
59
+ return RangeResp(
60
+ *incl_range,
61
+ complete_length=complete_length
62
+ if isinstance(complete_length, NonNegativeInt)
63
+ else None,
64
+ )
65
+
66
+ def visit_content_range(self, node: Node):
67
+ range_unit: RangeUnit
68
+ range: RangeResp | UnsatisfiedRange
69
+ range_unit, range = (x for x in map(self.visit, node.children) if x is not None)
70
+ return range_unit, range
@@ -0,0 +1,52 @@
1
+ from dataclasses import dataclass
2
+
3
+ from abnf import Node, NodeVisitor
4
+
5
+ import http_fields.visitors.rfc9110.parameters as parameters
6
+ import http_fields.visitors.rfc9110.token as token
7
+
8
+ # NB: do not alias the "type" grammar module as `type` -- it would shadow the
9
+ # builtin, which MediaType's generated (frozen) __setattr__ (`type(self)`) needs.
10
+ import http_fields.visitors.rfc9110.type as type_grammar
11
+
12
+
13
+ @dataclass(frozen=True)
14
+ class MediaType:
15
+ type: token.Token
16
+ subtype: token.Token
17
+ params: tuple[parameters.Parameter, ...]
18
+
19
+ def __init__(
20
+ self,
21
+ type: str,
22
+ subtype: str,
23
+ params: "list[parameters.Parameter] | tuple[parameters.Parameter, ...] | None" = None,
24
+ ):
25
+ object.__setattr__(self, "type", token.Token(type))
26
+ object.__setattr__(self, "subtype", token.Token(subtype))
27
+ object.__setattr__(self, "params", tuple(params) if params else ())
28
+
29
+ def __str__(self) -> str:
30
+ return f"{self.type}/{self.subtype}" + (
31
+ (";" + ";".join(str(p) for p in self.params)) if self.params else ""
32
+ )
33
+
34
+
35
+ class MediaTypeVisitor(NodeVisitor):
36
+ visit_type = type_grammar.TypeVisitor()
37
+ visit_subtype = type_grammar.SubtypeVisitor()
38
+ visit_parameters = parameters.ParametersVisitor()
39
+
40
+ def visit_media_type(self, node: Node) -> MediaType:
41
+ items = filter(None, map(self.visit, node.children))
42
+ type = next(items)
43
+ subtype = next(items)
44
+ params: list[parameters.Parameter] = next(items, [])
45
+ return MediaType(type, subtype, params)
46
+
47
+
48
+ class ContentTypeVisitor(NodeVisitor):
49
+ visit_media_type = MediaTypeVisitor()
50
+
51
+ def visit_content_type(self, node: Node):
52
+ return next(filter(None, map(self.visit, node.children)))
@@ -0,0 +1,12 @@
1
+ from datetime import datetime
2
+
3
+ from abnf import Node, NodeVisitor
4
+
5
+ import http_fields.visitors.rfc9110.httpdate as httpdate
6
+
7
+
8
+ class DateVisitor(NodeVisitor):
9
+ visit_http_date = httpdate.HttpDateVisitor()
10
+
11
+ def visit_date(self, node: Node) -> datetime:
12
+ return next(filter(None, map(self.visit, node.children)))
@@ -0,0 +1,66 @@
1
+ from dataclasses import dataclass
2
+
3
+ from abnf import Node, NodeVisitor, ParseError
4
+ from abnf.grammars import rfc9110
5
+
6
+
7
+ @dataclass(frozen=True)
8
+ class EntityTag:
9
+ """An RFC 9110 entity-tag. ``__eq__``/``__hash__`` are exact (weak flag + opaque
10
+ tag); use :meth:`compare` for the RFC's weak/strong comparison semantics."""
11
+
12
+ opaque_tag: str
13
+ weak: bool = False
14
+
15
+ def __init__(self, tag: str, weak: bool = False) -> None:
16
+ # Double-quote the tag if it is not already, then validate.
17
+ opaque_tag = (
18
+ f'"{tag}"' if not tag or (tag[0] != '"' and tag[-1] != '"') else tag
19
+ )
20
+ entity_tag = f"W/{opaque_tag}" if weak else opaque_tag
21
+ try:
22
+ rfc9110.Rule("entity-tag").parse_all(entity_tag)
23
+ except ParseError as exc:
24
+ raise ValueError("Invalid character in value.") from exc
25
+ object.__setattr__(self, "opaque_tag", opaque_tag)
26
+ object.__setattr__(self, "weak", weak)
27
+
28
+ def __str__(self) -> str:
29
+ return f"W/{self.opaque_tag}" if self.weak else self.opaque_tag
30
+
31
+ def compare(self, __o: object, *, weak: bool) -> bool:
32
+ if not isinstance(__o, self.__class__):
33
+ raise TypeError(
34
+ f"EntityTag is not comparable to an instance of {__o.__class__.__name__}."
35
+ )
36
+
37
+ if weak:
38
+ return self.opaque_tag == __o.opaque_tag
39
+ else:
40
+ return not (self.weak or __o.weak) and self.opaque_tag == __o.opaque_tag
41
+
42
+
43
+ class EntityTagVisitor(NodeVisitor):
44
+ def visit_entity_tag(self, node: Node) -> EntityTag:
45
+ # Match children by name: an empty opaque-tag ("") and an absent weak
46
+ # flag are both falsy, so a truthiness filter would lose them and shift
47
+ # the positional unpacking.
48
+ weak = False
49
+ tag = ""
50
+ for child in node.children:
51
+ if child.name == "weak":
52
+ weak = self.visit(child)
53
+ elif child.name == "opaque-tag":
54
+ tag = self.visit(child)
55
+ return EntityTag(tag, weak=weak)
56
+
57
+ def visit_opaque_tag(self, node: Node) -> str:
58
+ return "".join(filter(None, map(self.visit, node.children)))
59
+
60
+ @staticmethod
61
+ def visit_weak(node: Node):
62
+ return node.value != ""
63
+
64
+ @staticmethod
65
+ def visit_etagc(node: Node) -> str:
66
+ return node.value
@@ -0,0 +1,12 @@
1
+ from abnf import Node, NodeVisitor
2
+
3
+ import http_fields.visitors.rfc9110.entitytag as entitytag
4
+
5
+
6
+ class ETagVisitor(NodeVisitor):
7
+ """Helper class for parsing ETag header value."""
8
+
9
+ visit_entity_tag = entitytag.EntityTagVisitor()
10
+
11
+ def visit_etag(self, node: Node) -> entitytag.EntityTag:
12
+ return next(filter(None, map(self.visit, node.children)))
@@ -0,0 +1,15 @@
1
+ from abnf.grammars import rfc9110
2
+
3
+ from http_fields.parsedobjs import CaselessMixin, ParsedStr
4
+
5
+
6
+ class FieldName(CaselessMixin, ParsedStr):
7
+ """Represents an RFC 9110 field name."""
8
+
9
+ parser = rfc9110.Rule("field-name")
10
+
11
+
12
+ class FieldValue(ParsedStr):
13
+ """Represents an RFC 9110 field value."""
14
+
15
+ parser = rfc9110.Rule("field-value")
@@ -0,0 +1,32 @@
1
+ from abnf import Node, NodeVisitor
2
+ from abnf.grammars import rfc9110
3
+
4
+ from http_fields.parsedobjs import CaselessMixin, NonNegativeInt, ParsedStr
5
+
6
+
7
+ class Hostname(CaselessMixin, ParsedStr):
8
+ """An RFC 9110 uri-host. Compares case-insensitively while preserving the original
9
+ text; self-validating."""
10
+
11
+ parser = rfc9110.Rule("uri-host")
12
+
13
+
14
+ class HostVisitor(NodeVisitor):
15
+ def visit_host(self, node: Node) -> tuple[Hostname, NonNegativeInt | None]:
16
+ # Match children by name: an empty uri-host ("") is valid (reg-name is
17
+ # *(...)), and the ":" separator visits to None, so positional/truthiness
18
+ # handling would mis-assign the port as the hostname. The node is already
19
+ # parsed, so wrap its text as leaves untrusted-free (parse=False).
20
+ hostname = Hostname("", parse=False)
21
+ port: NonNegativeInt | None = None
22
+ for child in node.children:
23
+ if child.name == "uri-host":
24
+ hostname = Hostname(child.value, parse=False)
25
+ elif child.name == "port":
26
+ port = self.visit(child)
27
+ return (hostname, port)
28
+
29
+ @staticmethod
30
+ def visit_port(node: Node) -> NonNegativeInt | None:
31
+ # port = *DIGIT; an empty port ("example.com:") means no port.
32
+ return NonNegativeInt(node.value) if node.value else None