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 @@
1
+
@@ -0,0 +1,195 @@
1
+ import ipaddress
2
+ import itertools
3
+
4
+ from abnf import Node, NodeVisitor, ParseError
5
+ from abnf.grammars import rfc3986
6
+
7
+
8
+ class URI(str):
9
+ """Represents an RFC 3986 URI or relative URI."""
10
+
11
+ def __new__(cls, s: str, parse: bool = True):
12
+ if isinstance(s, cls):
13
+ return s
14
+ else:
15
+ try:
16
+ return super().__new__(
17
+ cls, rfc3986.Rule("URI").parse_all(s).value if parse else s
18
+ )
19
+ except ParseError as exc:
20
+ raise ValueError(f'Invalid token value "{s}".') from exc
21
+
22
+ def pct_decoded(self):
23
+ pass
24
+
25
+
26
+ class Scheme(str):
27
+ """Represents an RFC 3986 URL scheme.
28
+ Case normalization per RFC 3986 section 6.2.2.1 is performed."""
29
+
30
+ def __new__(cls, s: str, parse: bool = True):
31
+ if isinstance(s, cls):
32
+ return s
33
+ else:
34
+ try:
35
+ return super().__new__(
36
+ cls,
37
+ (rfc3986.Rule("scheme").parse_all(s).value if parse else s).lower(),
38
+ )
39
+ except ParseError as exc:
40
+ raise ValueError(f'Invalid scheme value "{s}".') from exc
41
+
42
+ def __eq__(self, __x: object) -> bool:
43
+ # scheme comparison should be case-insensitive.
44
+ if isinstance(__x, str):
45
+ return self.casefold() == __x.casefold()
46
+ else:
47
+ return NotImplemented
48
+
49
+ def __hash__(self) -> int:
50
+ # and because we redefined __eq__, we need to redefine __hash__ for consistency.
51
+ return hash(self.casefold())
52
+
53
+
54
+ class SchemeVisitor(NodeVisitor):
55
+ def visit_scheme(self, node: Node):
56
+ return Scheme(node.value, parse=False)
57
+
58
+
59
+ class PctEncodedVisitor(NodeVisitor):
60
+ unreserved_chars = set(
61
+ itertools.chain(
62
+ range(48, 58),
63
+ range(65, 91),
64
+ range(97, 123),
65
+ [ord(x) for x in ["-", ".", "_", "~"]],
66
+ )
67
+ )
68
+
69
+ @staticmethod
70
+ def visit_hex_digit(node: Node):
71
+ # perform case normalization per RFC 3986 section 6.2.2.1,
72
+ return node.value.upper()
73
+
74
+ @staticmethod
75
+ def visit_literal(node: Node):
76
+ # node.value can only be "%", assuming correct parsing.
77
+ return node.value
78
+
79
+ def visit_pct_encoded(self, node: Node):
80
+ # perform percent-encoding normalization per RFC 3986 section 6.2.2.2,
81
+ pct_char = "".join(filter(None, map(self.visit, node.children)))
82
+ code = int(pct_char[1:], 16)
83
+ return chr(code) if code in self.unreserved_chars else pct_char
84
+
85
+
86
+ class UserInfo(str):
87
+ def __new__(cls, s: str, parse: bool = True):
88
+ if isinstance(s, cls):
89
+ return s
90
+ else:
91
+ try:
92
+ return super().__new__(
93
+ cls, rfc3986.Rule("userinfo").parse_all(s).value if parse else s
94
+ )
95
+ except ParseError as exc:
96
+ raise ValueError(f'Invalid userinfo value "{s}".') from exc
97
+
98
+
99
+ class UserInfoVisitor(NodeVisitor):
100
+ @staticmethod
101
+ def visit_userinfo(node: Node):
102
+ return UserInfo(node.value, parse=False)
103
+
104
+
105
+ class IPvFuture(str):
106
+ def __new__(cls, s: str, parse: bool = True):
107
+ if isinstance(s, cls):
108
+ return s
109
+ else:
110
+ try:
111
+ return super().__new__(
112
+ cls, rfc3986.Rule("IPvFuture").parse_all(s).value if parse else s
113
+ )
114
+ except ParseError as exc:
115
+ raise ValueError(f'Invalid IPvFuture value "{s}".') from exc
116
+
117
+
118
+ class IPLiteralVisitor(NodeVisitor):
119
+ def visit_ip_literal(self, node: Node) -> ipaddress.IPv6Address | IPvFuture:
120
+ return next(filter(None, map(self.visit, node.children)))
121
+
122
+ @staticmethod
123
+ def visit_ipv6address(node: Node):
124
+ return ipaddress.IPv6Address(node.value)
125
+
126
+ @staticmethod
127
+ def visit_ipvfuture(node: Node):
128
+ return IPvFuture(node.value, parse=False)
129
+
130
+
131
+ class IPv4AddressVisitor(NodeVisitor):
132
+ @staticmethod
133
+ def visit_ipv4address(node: Node):
134
+ return ipaddress.IPv4Address(node.value)
135
+
136
+
137
+ class RegName(str):
138
+ """Represents an RFC 3986 registered-name, more commonly known as hostname.
139
+ Case normalization per RFC 3986 section 6.2.2.1 is performed."""
140
+
141
+ def __new__(cls, s: str, parse: bool = True):
142
+ if isinstance(s, cls):
143
+ return s
144
+ else:
145
+ try:
146
+ return super().__new__(
147
+ cls,
148
+ rfc3986.Rule("reg-name").parse_all(s).value.lower() if parse else s,
149
+ )
150
+ except ParseError as exc:
151
+ raise ValueError(f'Invalid reg-name value "{s}".') from exc
152
+
153
+
154
+ class RegNameVisitor(NodeVisitor):
155
+ visit_pct_encoded = PctEncodedVisitor()
156
+
157
+ @staticmethod
158
+ def visit_unreserved(node: Node):
159
+ return node.value
160
+
161
+ @staticmethod
162
+ def visit_sub_delims(node: Node):
163
+ return node.value
164
+
165
+ def visit_reg_name(self, node: Node):
166
+ value = "".join(filter(None, map(self.visit, node.children)))
167
+ return RegName(value, parse=False)
168
+
169
+
170
+ class HostVisitor(NodeVisitor):
171
+ visit_ip_literal = IPLiteralVisitor()
172
+ visit_ipv4address = IPv4AddressVisitor()
173
+ visit_regname = RegNameVisitor()
174
+
175
+ def visit_host(
176
+ self, node: Node
177
+ ) -> ipaddress.IPv6Address | IPvFuture | ipaddress.IPv4Address | RegName:
178
+ return next(filter(None, map(self.visit, node.children)))
179
+
180
+
181
+ class Authority:
182
+ userinfo: UserInfo | None
183
+
184
+
185
+ class AuthorityVisitor(NodeVisitor):
186
+ visit_userinfo = UserInfoVisitor()
187
+ visit_host = HostVisitor()
188
+
189
+ @staticmethod
190
+ def visit_port(node: Node):
191
+ return int(node.value)
192
+
193
+ def visit_authority(self, node: Node):
194
+
195
+ return next(filter(None, map(self.visit, node.children)))
@@ -0,0 +1,62 @@
1
+ from dataclasses import dataclass
2
+
3
+ from abnf import Node, NodeVisitor
4
+ from abnf.grammars import rfc6265
5
+
6
+ from http_fields.parsedobjs import ParsedStr
7
+
8
+
9
+ class CookieName(ParsedStr):
10
+ """Represents an RFC 6265 cookie-name. Cookie names are case-sensitive."""
11
+
12
+ parser = rfc6265.Rule("cookie-name")
13
+
14
+
15
+ class CookieValue(ParsedStr):
16
+ """Represents a valid RFC 6265 cookie value."""
17
+
18
+ parser = rfc6265.Rule("cookie-value")
19
+
20
+
21
+ @dataclass(frozen=True, slots=True)
22
+ class CookiePair:
23
+ name: CookieName
24
+ value: CookieValue
25
+
26
+ def __init__(self, name: str, value: str) -> None:
27
+ assert isinstance(name, str), "name must be str."
28
+ assert isinstance(value, str), "value must be str."
29
+ object.__setattr__(self, "name", CookieName(name))
30
+ object.__setattr__(self, "value", CookieValue(value))
31
+
32
+
33
+ class CookieNameVisitor(NodeVisitor):
34
+ @staticmethod
35
+ def visit_cookie_name(node: Node) -> str:
36
+ return CookieName(node.value, parse=False)
37
+
38
+
39
+ class CookieValueVisitor(NodeVisitor):
40
+ @staticmethod
41
+ def visit_cookie_value(node: Node):
42
+ # Return the raw matched text so surrounding DQUOTEs (part of the value
43
+ # per RFC 6265) are preserved, and an empty value stays "".
44
+ return node.value
45
+
46
+
47
+ class CookiePairVisitor(NodeVisitor):
48
+ visit_cookie_name = CookieNameVisitor()
49
+ visit_cookie_value = CookieValueVisitor()
50
+
51
+ def visit_cookie_pair(self, node: Node) -> CookiePair:
52
+ # The "=" separator visits to None; an empty cookie-value visits to ""
53
+ # (falsy but valid), so filter on identity, not truthiness.
54
+ name, value = (v for v in map(self.visit, node.children) if v is not None)
55
+ return CookiePair(name=name, value=value)
56
+
57
+
58
+ class CookieStringVisitor(NodeVisitor):
59
+ visit_cookie_pair = CookiePairVisitor()
60
+
61
+ def visit_cookie_string(self, node: Node) -> list[CookiePair]:
62
+ return list(filter(None, map(self.visit, node.children)))
@@ -0,0 +1,234 @@
1
+ from dataclasses import dataclass
2
+ from typing import Any
3
+ from urllib.parse import quote
4
+
5
+ from abnf import Node, NodeVisitor
6
+ from abnf.grammars import rfc6266
7
+
8
+ from http_fields.parsedobjs import CaselessMixin, ParsedStr
9
+ from http_fields.visitors.rfc7230 import (
10
+ QuotedString,
11
+ QuotedStringVisitor,
12
+ Token,
13
+ TokenVisitor,
14
+ )
15
+
16
+
17
+ class DispositionType(CaselessMixin, ParsedStr):
18
+ parser = rfc6266.Rule("disposition-type")
19
+
20
+
21
+ class DispositionTypeVisitor(NodeVisitor):
22
+ @staticmethod
23
+ def visit_disposition_type(node: Node):
24
+ return DispositionType(node.value, parse=False)
25
+
26
+
27
+ class ValueVisitor(NodeVisitor):
28
+ visit_quoted_string = QuotedStringVisitor()
29
+ visit_token = TokenVisitor()
30
+
31
+ def visit_value(self, node: Node) -> str:
32
+ return next(filter(None, map(self.visit, node.children)))
33
+
34
+
35
+ def NotNone(x: Any) -> bool:
36
+ return x is not None
37
+
38
+
39
+ @dataclass(frozen=True, kw_only=True)
40
+ class ExtValue:
41
+ charset: str = "utf-8"
42
+ language: str = ""
43
+ value: str
44
+
45
+ def parm_value(self) -> str:
46
+ # quote() defaults to safe="/", but "/" is not an RFC 5987 attr-char and
47
+ # must be percent-encoded; pass safe="" so nothing is left unencoded.
48
+ encoded = quote(self.value, safe="", encoding=self.charset)
49
+ return f"{self.charset}'{self.language}'{encoded}"
50
+
51
+ def __str__(self):
52
+ return self.parm_value()
53
+
54
+
55
+ class ExtValueVisitor(NodeVisitor):
56
+ def visit_ext_value(self, node: Node) -> ExtValue:
57
+ items = list(filter(NotNone, map(self.visit, node.children)))
58
+ charset = items[0]
59
+ language = items[1] if len(items) == 3 else ""
60
+ # value_chars is already the fully pct-decoded byte string (visit_value_chars
61
+ # resolves pct-encoded octets); decode it once. Passing it through unquote a
62
+ # second time would re-decode any literal '%' the value legitimately contains.
63
+ value_chars = items[-1]
64
+ return ExtValue(
65
+ charset=charset,
66
+ language=language,
67
+ value=value_chars.decode(charset),
68
+ )
69
+
70
+ @staticmethod
71
+ def visit_charset(node: Node) -> str:
72
+ return node.value
73
+
74
+ @staticmethod
75
+ def visit_language(node: Node) -> str:
76
+ return node.value
77
+
78
+ @staticmethod
79
+ def visit_hexdig(node: Node) -> str:
80
+ return node.value
81
+
82
+ def visit_pct_encoded(self, node: Node) -> int:
83
+ hex = "".join(filter(None, map(self.visit, node.children)))
84
+ return int(hex, base=16)
85
+
86
+ @staticmethod
87
+ def visit_attr_char(node: Node) -> int:
88
+ return ord(node.value)
89
+
90
+ def visit_value_chars(self, node: Node) -> bytes:
91
+ return bytes(list(filter(None, map(self.visit, node.children))))
92
+
93
+
94
+ class Filename(CaselessMixin, str):
95
+ """The filename parm names 'filename', 'filename*' are case-insenstive. I tripped over this
96
+ once too often while testing, hence a class that removes the obstacle."""
97
+
98
+ def __new__(cls, s: Any, parse: bool = True):
99
+ if isinstance(s, cls):
100
+ return s
101
+ else:
102
+ # if s can't be converted to str, this should fail.
103
+ _s = str(s)
104
+ if _s.lower() not in {"filename", "filename*"}:
105
+ raise ValueError("Value must be 'filename' or 'filename*'.modulo case.")
106
+ return super().__new__(cls, _s)
107
+
108
+ def __repr__(self):
109
+ return f"{self.__class__.__name__}({str.__repr__(str(self))})"
110
+
111
+
112
+ @dataclass(frozen=True)
113
+ class FilenameParm:
114
+ name: Filename
115
+ value: Token | QuotedString | ExtValue
116
+
117
+ def __init__(self, name: str, value: str | ExtValue):
118
+ object.__setattr__(self, "name", Filename(name))
119
+ if name[-1] == "*":
120
+ object.__setattr__(
121
+ self,
122
+ "value",
123
+ value if isinstance(value, ExtValue) else ExtValue(value=value),
124
+ )
125
+ else:
126
+ if not isinstance(value, str):
127
+ raise TypeError('value associated to "filename" must be a str.')
128
+ try:
129
+ object.__setattr__(self, "value", Token(value))
130
+ except ValueError:
131
+ try:
132
+ object.__setattr__(self, "value", QuotedString(value))
133
+ except ValueError as exc:
134
+ raise ValueError(
135
+ f"Value {value} could not be parsed as a Token or QuotedString."
136
+ ) from exc
137
+
138
+ def __str__(self):
139
+ return f"{self.name}={self.value}"
140
+
141
+
142
+ class FilenameParmNodeVisitor(NodeVisitor):
143
+ visit_value = ValueVisitor()
144
+ visit_ext_value = ExtValueVisitor()
145
+
146
+ @staticmethod
147
+ def visit_literal(node: Node):
148
+ return node.value if node.value.lower() in {"filename", "filename*"} else None
149
+
150
+ def visit_filename_parm(self, node: Node):
151
+ assert len(node.children) == 3
152
+ filename = Filename(self.visit_literal(node.children[0]))
153
+ assert node.children[1].value == "="
154
+ if filename == "filename":
155
+ value = self.visit_value(node.children[2])
156
+ else:
157
+ value = self.visit_ext_value(node.children[2])
158
+ return FilenameParm(filename, value)
159
+
160
+
161
+ @dataclass(frozen=True)
162
+ class DispExtParm:
163
+ name: Token
164
+ value: Token | QuotedString | ExtValue
165
+
166
+ def __init__(self, name: str, value: str | ExtValue):
167
+ object.__setattr__(self, "name", Token(name))
168
+ if name[-1] == "*":
169
+ if isinstance(value, ExtValue):
170
+ object.__setattr__(self, "value", value)
171
+ else:
172
+ object.__setattr__(self, "value", ExtValue(value=value))
173
+ else:
174
+ if isinstance(value, str):
175
+ try:
176
+ object.__setattr__(self, "value", Token(value))
177
+ except ValueError:
178
+ try:
179
+ object.__setattr__(self, "value", QuotedString(value))
180
+ except ValueError as exc:
181
+ raise ValueError(
182
+ f'Unable to parse "{value}" as a Token or QuotedString.'
183
+ ) from exc
184
+ else:
185
+ raise TypeError(
186
+ f"Value for name {name} must be a string that parses as a "
187
+ "Token or QuotedString."
188
+ )
189
+
190
+ def __str__(self):
191
+ return f"{self.name}={self.value}"
192
+
193
+
194
+ class DispExtParmVisitor(NodeVisitor):
195
+ visit_token = TokenVisitor()
196
+ visit_value = ValueVisitor()
197
+ visit_ext_value = ExtValueVisitor()
198
+
199
+ def visit_disp_ext_parm(self, node: Node):
200
+ assert len(node.children) == 3
201
+ name = self.visit_token(node.children[0])
202
+ assert node.children[1].value == "="
203
+ # the RFC 6266 grammar is ambiguous here. The rule 'token' matches everything matched
204
+ # by 'ext-token', and the same is true for 'value' and 'ext-value'. Thus we have to
205
+ # disambiguate here by checking the name for a trailing '*', which marks an ext-thing.
206
+ # In the case of an ext-name, we reparse the captured value as an ext-value.
207
+ if name[-1] == "*" and node.children[2].name == "value":
208
+ src = node.children[2].value
209
+ ext_node = rfc6266.Rule("ext-value").parse_all(src)
210
+ value = self.visit_ext_value(ext_node)
211
+ else:
212
+ value = self.visit(node.children[2])
213
+ assert value
214
+ return DispExtParm(name, value)
215
+
216
+
217
+ class DispositionParmNodeVisitor(NodeVisitor):
218
+ visit_filename_parm = FilenameParmNodeVisitor()
219
+ visit_disp_ext_parm = DispExtParmVisitor()
220
+
221
+ def visit_disposition_parm(self, node: Node) -> tuple[str, str | ExtValue]:
222
+ parm = next(filter(None, map(self.visit, node.children)))
223
+ return parm
224
+
225
+
226
+ class ContentDispositionNodeVisitor(NodeVisitor):
227
+ visit_disposition_type = DispositionTypeVisitor()
228
+ visit_disposition_parm = DispositionParmNodeVisitor()
229
+
230
+ def visit_content_disposition(self, node: Node):
231
+ items = filter(None, map(self.visit, node.children))
232
+ disposition_type: DispositionType = next(items)
233
+ disposition_parms: list[FilenameParm | DispExtParm] = [item for item in items]
234
+ return disposition_type, disposition_parms
@@ -0,0 +1,80 @@
1
+ from abnf.grammars import rfc7230
2
+ from abnf.parser import LiteralNode, Node, NodeVisitor
3
+
4
+ from http_fields.parsedobjs import CaselessMixin, ParsedStr
5
+
6
+
7
+ class QuotedPairVisitor(NodeVisitor):
8
+ def visit_quoted_pair(self, node: Node) -> str:
9
+ assert (
10
+ isinstance(node.children[0], LiteralNode) and node.children[0].value == "\\"
11
+ )
12
+ return self.visit(node.children[1])
13
+
14
+ @staticmethod
15
+ def visit_htab(node: Node) -> str:
16
+ return node.value
17
+
18
+ @staticmethod
19
+ def visit_sp(node: Node) -> str:
20
+ return node.value
21
+
22
+ @staticmethod
23
+ def visit_vchar(node: Node) -> str:
24
+ return node.value
25
+
26
+ @staticmethod
27
+ def visit_obs_text(node: Node) -> str:
28
+ return node.value
29
+
30
+
31
+ class QuotedString(ParsedStr):
32
+ """Represents an RFC 7230 quoted-string.
33
+ A QuotedString can be created either double-quoted or not; the result will be a double-quoted
34
+ string, escaped as needed."""
35
+
36
+ parser = rfc7230.Rule("quoted-string")
37
+
38
+ def __new__(cls, s: str, parse: bool = True):
39
+ assert isinstance(s, str)
40
+ if isinstance(s, cls):
41
+ return s
42
+ else:
43
+ # if s is double-quoted, we parse as is. if not, we escape what needs to be escaped,
44
+ # wrap in double quotes, and parse that.
45
+ if s == "":
46
+ dquoted_value = '""'
47
+ elif len(s) > 1 and s[0] == '"' and s[-1] == '"':
48
+ dquoted_value = s
49
+ else:
50
+ escaped_value = "".join(f"\\{x}" if x in ["\\", '"'] else x for x in s)
51
+ dquoted_value = f'"{escaped_value}"'
52
+ return super().__new__(cls, dquoted_value)
53
+
54
+
55
+ class QuotedStringVisitor(NodeVisitor):
56
+ """Visits an RFC7230 quoted-string."""
57
+
58
+ visit_quoted_pair = QuotedPairVisitor()
59
+
60
+ def visit_quoted_string(self, node: Node) -> QuotedString:
61
+ value = "".join(x for x in map(self.visit, node.children) if x is not None)
62
+ return QuotedString(value, parse=False)
63
+
64
+ @staticmethod
65
+ def visit_qdtext(node: Node) -> str:
66
+ return node.value
67
+
68
+
69
+ class Token(CaselessMixin, ParsedStr):
70
+ """Represents an RFC 7230 token."""
71
+
72
+ parser = rfc7230.Rule("token")
73
+
74
+
75
+ class TokenVisitor(NodeVisitor):
76
+ """Visits a token node."""
77
+
78
+ @staticmethod
79
+ def visit_token(node: Node) -> Token:
80
+ return Token(node.value, parse=False)
@@ -0,0 +1,43 @@
1
+ from dataclasses import dataclass
2
+
3
+ from abnf import Node, NodeVisitor
4
+
5
+ from http_fields.visitors.rfc9110.parameters import (
6
+ Param,
7
+ ParamsInput,
8
+ as_params,
9
+ parsed_param,
10
+ )
11
+
12
+ __all__ = ["ForwardedElement", "ForwardedVisitor"]
13
+
14
+
15
+ @dataclass(frozen=True)
16
+ class ForwardedElement:
17
+ """One Forwarded element (RFC 7239): an ordered sequence of ``name=value`` pairs,
18
+ e.g. ``for=192.0.2.60;proto=http``."""
19
+
20
+ pairs: tuple[Param, ...]
21
+
22
+ def __init__(self, pairs: ParamsInput) -> None:
23
+ # Each pair is a self-validating Param; raw (name, value) tuples are coerced.
24
+ object.__setattr__(self, "pairs", as_params(pairs))
25
+
26
+ def __str__(self) -> str:
27
+ return ";".join(str(p) for p in self.pairs)
28
+
29
+
30
+ class ForwardedVisitor(NodeVisitor):
31
+ @staticmethod
32
+ def visit_forwarded_pair(node: Node) -> Param:
33
+ # forwarded-pair = token "=" value
34
+ return parsed_param(node.children[0].value, node.children[-1].value)
35
+
36
+ def visit_forwarded_element(self, node: Node) -> ForwardedElement:
37
+ pairs = [x for x in map(self.visit, node.children) if isinstance(x, Param)]
38
+ return ForwardedElement(pairs)
39
+
40
+ def visit_forwarded(self, node: Node) -> list[ForwardedElement]:
41
+ return [
42
+ x for x in map(self.visit, node.children) if isinstance(x, ForwardedElement)
43
+ ]
@@ -0,0 +1,77 @@
1
+ from dataclasses import dataclass
2
+
3
+ from abnf import Node, NodeVisitor
4
+
5
+ from http_fields.visitors.rfc9110.parameters import (
6
+ Param,
7
+ ParamsInput,
8
+ QuotedString,
9
+ Token,
10
+ as_params,
11
+ param_from_node,
12
+ parsed_param,
13
+ value_leaf,
14
+ )
15
+
16
+ __all__ = ["Preference", "PreferVisitor", "PreferenceAppliedVisitor"]
17
+
18
+
19
+ @dataclass(frozen=True)
20
+ class Preference:
21
+ """One RFC 7240 preference: a token, an optional value, and optional parameters.
22
+ Self-validating: an invalid name, value, or parameter cannot be built."""
23
+
24
+ name: Token
25
+ value: Token | QuotedString | None = None
26
+ parameters: tuple[Param, ...] = ()
27
+
28
+ def __init__(
29
+ self,
30
+ name: str,
31
+ value: str | None = None,
32
+ parameters: ParamsInput = (),
33
+ ) -> None:
34
+ object.__setattr__(self, "name", Token(name))
35
+ object.__setattr__(
36
+ self, "value", value_leaf(value) if value is not None else None
37
+ )
38
+ object.__setattr__(self, "parameters", as_params(parameters))
39
+
40
+ def __str__(self) -> str:
41
+ out = str(self.name) if self.value is None else f"{self.name}={self.value}"
42
+ for p in self.parameters:
43
+ out += f"; {p}"
44
+ return out
45
+
46
+
47
+ class PreferVisitor(NodeVisitor):
48
+ def visit_preference(self, node: Node) -> Preference:
49
+ # token [ "=" word ] *( ";" parameter )
50
+ name = node.children[0].value
51
+ value: str | None = None
52
+ params: list[Param] = []
53
+ seen_eq = False
54
+ for child in node.children[1:]:
55
+ if child.name == "parameter":
56
+ params.append(param_from_node(child))
57
+ elif child.name == "literal" and child.value == "=" and not params:
58
+ seen_eq = True
59
+ elif seen_eq and value is None and child.name not in ("BWS", "OWS"):
60
+ value = child.value
61
+ # Build the head (name + optional value) trusted, then hand its leaves to
62
+ # Preference, whose coercion passes them through unchanged.
63
+ head = parsed_param(name, value)
64
+ return Preference(head.name, head.value, params)
65
+
66
+ def visit_prefer(self, node: Node) -> list[Preference]:
67
+ return [
68
+ self.visit_preference(c) for c in node.children if c.name == "preference"
69
+ ]
70
+
71
+
72
+ class PreferenceAppliedVisitor(PreferVisitor):
73
+ # RFC 7240's abnf uses the same `preference` production for Preference-Applied.
74
+ def visit_preference_applied(self, node: Node) -> list[Preference]:
75
+ return [
76
+ self.visit_preference(c) for c in node.children if c.name == "preference"
77
+ ]