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 @@
|
|
|
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
|
+
]
|