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,167 @@
|
|
|
1
|
+
from datetime import date, datetime, time, timezone
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from abnf import Node, NodeVisitor
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class MonthVisitor(NodeVisitor):
|
|
8
|
+
months = {
|
|
9
|
+
"Jan": 1,
|
|
10
|
+
"Feb": 2,
|
|
11
|
+
"Mar": 3,
|
|
12
|
+
"Apr": 4,
|
|
13
|
+
"May": 5,
|
|
14
|
+
"Jun": 6,
|
|
15
|
+
"Jul": 7,
|
|
16
|
+
"Aug": 8,
|
|
17
|
+
"Sep": 9,
|
|
18
|
+
"Oct": 10,
|
|
19
|
+
"Nov": 11,
|
|
20
|
+
"Dec": 12,
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
def visit_month(self, node: Node) -> int:
|
|
24
|
+
return self.months[node.value]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class DayVisitor(NodeVisitor):
|
|
28
|
+
def visit_day(self, node: Node) -> int:
|
|
29
|
+
return int(node.value)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class YearVisitor(NodeVisitor):
|
|
33
|
+
def visit_year(self, node: Node) -> int:
|
|
34
|
+
return int(node.value)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class Date1Visitor(NodeVisitor):
|
|
38
|
+
visit_month = MonthVisitor()
|
|
39
|
+
visit_day = DayVisitor()
|
|
40
|
+
visit_year = YearVisitor()
|
|
41
|
+
|
|
42
|
+
def visit_date1(self, node: Node) -> date:
|
|
43
|
+
day: int
|
|
44
|
+
month: int
|
|
45
|
+
year: int
|
|
46
|
+
day, month, year = filter(None, map(self.visit, node.children))
|
|
47
|
+
return date(year=year, month=month, day=day)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class Date2Visitor(NodeVisitor):
|
|
51
|
+
visit_day = DayVisitor()
|
|
52
|
+
visit_month = MonthVisitor()
|
|
53
|
+
|
|
54
|
+
def visit_date2(self, node: Node) -> date:
|
|
55
|
+
day: int
|
|
56
|
+
month: int
|
|
57
|
+
digit1: str
|
|
58
|
+
digit2: str
|
|
59
|
+
day, month, digit1, digit2 = filter(None, map(self.visit, node.children))
|
|
60
|
+
year = int("20" + digit1 + digit2)
|
|
61
|
+
date2 = date(year=year, month=month, day=day)
|
|
62
|
+
|
|
63
|
+
# From RFC 2616 <https://tools.ietf.org/html/rfc2616#section-19.3>:
|
|
64
|
+
#
|
|
65
|
+
# "HTTP/1.1 clients and caches SHOULD assume that an RFC-850 date
|
|
66
|
+
# which appears to be more than 50 years in the future is in fact
|
|
67
|
+
# in the past (this helps solve the "year 2000" problem).
|
|
68
|
+
#
|
|
69
|
+
# So, on the off chance that we are parsing and RFC 850 date, we'll use this
|
|
70
|
+
# suggestion to resolve the 2-digit date.
|
|
71
|
+
|
|
72
|
+
today = datetime.now(timezone.utc).date()
|
|
73
|
+
if date2 > today.replace(year=today.year + 50):
|
|
74
|
+
date2 = date2.replace(year=date2.year - 100)
|
|
75
|
+
return date2
|
|
76
|
+
|
|
77
|
+
@staticmethod
|
|
78
|
+
def visit_digit(node: Node) -> str:
|
|
79
|
+
return node.value
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class Date3Visitor(NodeVisitor):
|
|
83
|
+
visit_month = MonthVisitor()
|
|
84
|
+
|
|
85
|
+
def visit_date3(self, node: Node) -> tuple[int, int]:
|
|
86
|
+
values: list[Any] = list(filter(None, map(self.visit, node.children)))
|
|
87
|
+
month: int = values[0]
|
|
88
|
+
day = int("".join(values[1:]))
|
|
89
|
+
return month, day
|
|
90
|
+
|
|
91
|
+
@staticmethod
|
|
92
|
+
def visit_digit(node: Node) -> str:
|
|
93
|
+
return node.value
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class TimeOfDayVisitor(NodeVisitor):
|
|
97
|
+
def visit_time_of_day(self, node: Node) -> time:
|
|
98
|
+
hour, minute, second = filter(
|
|
99
|
+
lambda x: x is not None, map(self.visit, node.children)
|
|
100
|
+
)
|
|
101
|
+
return time(hour=hour, minute=minute, second=second)
|
|
102
|
+
|
|
103
|
+
def visit_hour(self, node: Node) -> int:
|
|
104
|
+
return int(node.value)
|
|
105
|
+
|
|
106
|
+
def visit_minute(self, node: Node) -> int:
|
|
107
|
+
return int(node.value)
|
|
108
|
+
|
|
109
|
+
def visit_second(self, node: Node) -> int:
|
|
110
|
+
return int(node.value)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class IMfFixDateVisitor(NodeVisitor):
|
|
114
|
+
visit_date1 = Date1Visitor()
|
|
115
|
+
visit_time_of_day = TimeOfDayVisitor()
|
|
116
|
+
|
|
117
|
+
def visit_imf_fixdate(self, node: Node) -> datetime:
|
|
118
|
+
date_: date
|
|
119
|
+
time_of_day: time
|
|
120
|
+
date_, time_of_day = filter(None, map(self.visit, node.children))
|
|
121
|
+
return datetime.combine(date=date_, time=time_of_day).replace(
|
|
122
|
+
tzinfo=timezone.utc
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class RFC850DateVisitor(NodeVisitor):
|
|
127
|
+
visit_date2 = Date2Visitor()
|
|
128
|
+
visit_time_of_day = TimeOfDayVisitor()
|
|
129
|
+
|
|
130
|
+
def visit_rfc850_date(self, node: Node) -> datetime:
|
|
131
|
+
date_: date
|
|
132
|
+
time_of_day: time
|
|
133
|
+
date_, time_of_day = filter(None, map(self.visit, node.children))
|
|
134
|
+
return datetime.combine(date=date_, time=time_of_day).replace(
|
|
135
|
+
tzinfo=timezone.utc
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class AscTimeDateVisitor(NodeVisitor):
|
|
140
|
+
visit_date3 = Date3Visitor()
|
|
141
|
+
visit_time_of_day = TimeOfDayVisitor()
|
|
142
|
+
visit_year = YearVisitor()
|
|
143
|
+
|
|
144
|
+
def visit_asctime_date(self, node: Node) -> datetime:
|
|
145
|
+
date3: tuple[int, int]
|
|
146
|
+
time_of_day: time
|
|
147
|
+
year: int
|
|
148
|
+
date3, time_of_day, year = filter(None, map(self.visit, node.children))
|
|
149
|
+
return datetime.combine(
|
|
150
|
+
date(year=year, month=date3[0], day=date3[1]), time=time_of_day
|
|
151
|
+
).replace(tzinfo=timezone.utc)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class ObsDateVisitor(NodeVisitor):
|
|
155
|
+
visit_rfc850_date = RFC850DateVisitor()
|
|
156
|
+
visit_asctime_date = AscTimeDateVisitor()
|
|
157
|
+
|
|
158
|
+
def visit_obs_date(self, node: Node):
|
|
159
|
+
return next(filter(None, map(self.visit, node.children)))
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class HttpDateVisitor(NodeVisitor):
|
|
163
|
+
visit_imf_fixdate = IMfFixDateVisitor()
|
|
164
|
+
visit_obs_date = ObsDateVisitor()
|
|
165
|
+
|
|
166
|
+
def visit_http_date(self, node: Node) -> datetime:
|
|
167
|
+
return next(filter(None, map(self.visit, node.children)))
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from typing import Literal
|
|
2
|
+
|
|
3
|
+
from abnf import Node, NodeVisitor
|
|
4
|
+
|
|
5
|
+
import http_fields.visitors.rfc9110.entitytag as entitytag
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class IfMatchVisitor(NodeVisitor):
|
|
9
|
+
visit_entity_tag = entitytag.EntityTagVisitor()
|
|
10
|
+
|
|
11
|
+
@staticmethod
|
|
12
|
+
def visit_literal(node: Node):
|
|
13
|
+
return "*" if node.value == "*" else None
|
|
14
|
+
|
|
15
|
+
def visit_if_match(self, node: Node) -> Literal["*"] | list[entitytag.EntityTag]:
|
|
16
|
+
items = list(filter(None, map(self.visit, node.children)))
|
|
17
|
+
if not items:
|
|
18
|
+
empty_list: list[entitytag.EntityTag] = []
|
|
19
|
+
return empty_list
|
|
20
|
+
|
|
21
|
+
if items[0] == "*":
|
|
22
|
+
return "*"
|
|
23
|
+
else:
|
|
24
|
+
return items
|
|
@@ -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 IfModifiedSinceVisitor(NodeVisitor):
|
|
9
|
+
visit_http_date = httpdate.HttpDateVisitor()
|
|
10
|
+
|
|
11
|
+
def visit_if_modified_since(self, node: Node) -> datetime:
|
|
12
|
+
return next(filter(None, map(self.visit, node.children)))
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from typing import Literal
|
|
2
|
+
|
|
3
|
+
from abnf import Node, NodeVisitor
|
|
4
|
+
|
|
5
|
+
import http_fields.visitors.rfc9110.entitytag as entitytag
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class IfNoneMatchVisitor(NodeVisitor):
|
|
9
|
+
visit_entity_tag = entitytag.EntityTagVisitor()
|
|
10
|
+
|
|
11
|
+
@staticmethod
|
|
12
|
+
def visit_literal(node: Node):
|
|
13
|
+
return "*" if node.value == "*" else None
|
|
14
|
+
|
|
15
|
+
def visit_if_none_match(
|
|
16
|
+
self, node: Node
|
|
17
|
+
) -> Literal["*"] | list[entitytag.EntityTag]:
|
|
18
|
+
items = list(filter(None, map(self.visit, node.children)))
|
|
19
|
+
if not items:
|
|
20
|
+
empty_list: list[entitytag.EntityTag] = []
|
|
21
|
+
return empty_list
|
|
22
|
+
|
|
23
|
+
if items[0] == "*":
|
|
24
|
+
return "*"
|
|
25
|
+
else:
|
|
26
|
+
return items
|
|
@@ -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 IfUnmodifiedSinceVisitor(NodeVisitor):
|
|
9
|
+
visit_http_date = httpdate.HttpDateVisitor()
|
|
10
|
+
|
|
11
|
+
def visit_if_unmodified_since(self, node: Node) -> datetime:
|
|
12
|
+
return next(filter(None, map(self.visit, node.children)))
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
|
|
3
|
+
from abnf import Node, NodeVisitor
|
|
4
|
+
|
|
5
|
+
import http_fields.visitors.rfc9110.httpdate as httpdate
|
|
6
|
+
|
|
7
|
+
__all__ = ["LastModifiedVisitor"]
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class LastModifiedVisitor(NodeVisitor):
|
|
11
|
+
visit_http_date = httpdate.HttpDateVisitor()
|
|
12
|
+
|
|
13
|
+
def visit_last_modified(self, node: Node) -> datetime:
|
|
14
|
+
return next(filter(None, map(self.visit, node.children)))
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
from collections.abc import Iterable
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
|
|
4
|
+
from abnf import Node, NodeVisitor
|
|
5
|
+
|
|
6
|
+
import http_fields.visitors.rfc9110.quotedstring as quotedstring
|
|
7
|
+
import http_fields.visitors.rfc9110.token as token
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"Param",
|
|
11
|
+
"ParamsInput",
|
|
12
|
+
"Parameter",
|
|
13
|
+
"ParameterVisitor",
|
|
14
|
+
"ParametersVisitor",
|
|
15
|
+
"as_params",
|
|
16
|
+
"param_from_node",
|
|
17
|
+
"parsed_param",
|
|
18
|
+
"value_leaf",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
Token = token.Token
|
|
23
|
+
TokenVisitor = token.TokenVisitor
|
|
24
|
+
QuotedString = quotedstring.QuotedString
|
|
25
|
+
QuotedStringVisitor = quotedstring.QuotedStringVisitor
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def value_leaf(value: str) -> "Token | QuotedString":
|
|
29
|
+
"""Coerce a parameter value to the right self-validating leaf: a Token if it is one,
|
|
30
|
+
otherwise a (double-quoted, escaped) QuotedString. An existing leaf passes through."""
|
|
31
|
+
try:
|
|
32
|
+
return Token(value)
|
|
33
|
+
except ValueError:
|
|
34
|
+
return QuotedString(value)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(frozen=True)
|
|
38
|
+
class Parameter:
|
|
39
|
+
name: Token
|
|
40
|
+
value: Token | QuotedString
|
|
41
|
+
|
|
42
|
+
def __init__(self, name: str, value: str):
|
|
43
|
+
object.__setattr__(self, "name", Token(name))
|
|
44
|
+
object.__setattr__(self, "value", value_leaf(value))
|
|
45
|
+
|
|
46
|
+
def __str__(self):
|
|
47
|
+
return f"{self.name}={self.value}"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass(frozen=True)
|
|
51
|
+
class Param:
|
|
52
|
+
"""A generic ``name`` or ``name=value`` parameter: a token name with an optional
|
|
53
|
+
token / quoted-string value. Used by headers (Link, Prefer, Alt-Svc, ...) whose
|
|
54
|
+
parameters may be valueless. Self-validating: an invalid name or value cannot be built."""
|
|
55
|
+
|
|
56
|
+
name: Token
|
|
57
|
+
value: Token | QuotedString | None = None
|
|
58
|
+
|
|
59
|
+
def __init__(self, name: str, value: str | None = None) -> None:
|
|
60
|
+
object.__setattr__(self, "name", Token(name))
|
|
61
|
+
object.__setattr__(
|
|
62
|
+
self, "value", value_leaf(value) if value is not None else None
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
def __str__(self) -> str:
|
|
66
|
+
return str(self.name) if self.value is None else f"{self.name}={self.value}"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# What a value object's constructor accepts for its params: ready-made Param objects
|
|
70
|
+
# or raw ``(name[, value])`` tuples (coerced by as_params).
|
|
71
|
+
ParamsInput = Iterable[Param | tuple[str, ...]]
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def as_params(items: ParamsInput) -> tuple[Param, ...]:
|
|
75
|
+
"""Coerce an iterable of ``Param`` (kept as-is) or raw ``(name[, value])`` tuples into a
|
|
76
|
+
tuple of validated ``Param``. Lets value-object constructors accept either form."""
|
|
77
|
+
return tuple(p if isinstance(p, Param) else Param(*p) for p in items)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def parsed_param(name: str, value: str | None) -> Param:
|
|
81
|
+
"""Build a ``Param`` from already-parsed node text, wrapping the parts as leaves with
|
|
82
|
+
``parse=False`` so the parse() path does not re-parse (QuotedString still validates)."""
|
|
83
|
+
if value is None:
|
|
84
|
+
return Param(Token(name, parse=False))
|
|
85
|
+
leaf = QuotedString(value) if value[:1] == '"' else Token(value, parse=False)
|
|
86
|
+
return Param(Token(name, parse=False), leaf)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def param_from_node(node: Node) -> Param:
|
|
90
|
+
"""Build a ``Param`` from a ``token [ "=" ( token / quoted-string ) ]`` node (the shape
|
|
91
|
+
shared by Link-, Prefer-, and Alt-Svc-style parameters)."""
|
|
92
|
+
name = node.children[0].value
|
|
93
|
+
value: str | None = None
|
|
94
|
+
seen_eq = False
|
|
95
|
+
for child in node.children[1:]:
|
|
96
|
+
if child.name == "literal" and child.value == "=":
|
|
97
|
+
seen_eq = True
|
|
98
|
+
elif seen_eq and child.name not in ("BWS", "OWS"):
|
|
99
|
+
value = child.value
|
|
100
|
+
return parsed_param(name, value)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class ParameterVisitor(NodeVisitor):
|
|
104
|
+
visit_quoted_string = QuotedStringVisitor()
|
|
105
|
+
visit_token = TokenVisitor()
|
|
106
|
+
|
|
107
|
+
def visit_parameter(self, node: Node):
|
|
108
|
+
name: Token
|
|
109
|
+
value: Token | QuotedString
|
|
110
|
+
name, value = filter(None, map(self.visit, node.children))
|
|
111
|
+
return Parameter(name, value)
|
|
112
|
+
|
|
113
|
+
def visit_parameter_name(self, node: Node) -> Token:
|
|
114
|
+
return next(filter(None, map(self.visit, node.children)))
|
|
115
|
+
|
|
116
|
+
def visit_parameter_value(self, node: Node) -> Token | QuotedString:
|
|
117
|
+
return next(filter(None, map(self.visit, node.children)))
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class ParametersVisitor(NodeVisitor):
|
|
121
|
+
visit_parameter = ParameterVisitor()
|
|
122
|
+
|
|
123
|
+
def visit_parameters(self, node: Node) -> list[Parameter]:
|
|
124
|
+
return list(filter(None, map(self.visit, node.children)))
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
from abnf import Node, NodeVisitor
|
|
4
|
+
|
|
5
|
+
import http_fields.visitors.rfc9110.comment as comment
|
|
6
|
+
import http_fields.visitors.rfc9110.token as token
|
|
7
|
+
|
|
8
|
+
Token = token.Token
|
|
9
|
+
TokenVisitor = token.TokenVisitor
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
__all__ = ["Product", "ProductVisitor"]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True)
|
|
16
|
+
class Product:
|
|
17
|
+
name: Token
|
|
18
|
+
version: Token | None
|
|
19
|
+
|
|
20
|
+
def __init__(self, name: str, version: str | None = None):
|
|
21
|
+
object.__setattr__(self, "name", Token(name))
|
|
22
|
+
object.__setattr__(self, "version", Token(version) if version else None)
|
|
23
|
+
|
|
24
|
+
def __str__(self):
|
|
25
|
+
return str(self.name) + (f"/{self.version}" if self.version else "")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ProductVisitor(NodeVisitor):
|
|
29
|
+
visit_comment = comment.CommentVisitor()
|
|
30
|
+
visit_token = TokenVisitor()
|
|
31
|
+
|
|
32
|
+
def visit_product(self, node: Node):
|
|
33
|
+
items = filter(None, map(self.visit, node.children))
|
|
34
|
+
name = next(items)
|
|
35
|
+
version = next(items, None)
|
|
36
|
+
return Product(name, version)
|
|
37
|
+
|
|
38
|
+
def visit_product_version(self, node: Node) -> Token:
|
|
39
|
+
return next(filter(None, map(self.visit, node.children)))
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
from abnf import LiteralNode, Node, NodeVisitor
|
|
2
|
+
from abnf.grammars import rfc9110
|
|
3
|
+
|
|
4
|
+
from http_fields.parsedobjs import 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 9110 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 = rfc9110.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 RFC 9110 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
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
from abnf import Node, NodeVisitor
|
|
4
|
+
|
|
5
|
+
from http_fields.parsedobjs import NonNegativeInt
|
|
6
|
+
from http_fields.visitors.rfc9110.rangeunit import RangeUnit
|
|
7
|
+
|
|
8
|
+
__all__ = ["IntRange", "RangeVisitor", "SuffixRange"]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True)
|
|
12
|
+
class IntRange:
|
|
13
|
+
"""An RFC 9110 int-range, e.g. ``0-499`` or ``500-`` (open-ended)."""
|
|
14
|
+
|
|
15
|
+
first_pos: NonNegativeInt
|
|
16
|
+
last_pos: NonNegativeInt | None = None
|
|
17
|
+
|
|
18
|
+
def __init__(self, first_pos: int, last_pos: int | None = None) -> None:
|
|
19
|
+
first = NonNegativeInt(first_pos)
|
|
20
|
+
last = None if last_pos is None else NonNegativeInt(last_pos)
|
|
21
|
+
if last is not None and last < first:
|
|
22
|
+
# RFC 9110 section 14.1.2: an int-range with last-pos < first-pos is invalid.
|
|
23
|
+
raise ValueError("last-pos must not be less than first-pos.")
|
|
24
|
+
object.__setattr__(self, "first_pos", first)
|
|
25
|
+
object.__setattr__(self, "last_pos", last)
|
|
26
|
+
|
|
27
|
+
def __str__(self) -> str:
|
|
28
|
+
last = "" if self.last_pos is None else str(self.last_pos)
|
|
29
|
+
return f"{self.first_pos}-{last}"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(frozen=True)
|
|
33
|
+
class SuffixRange:
|
|
34
|
+
"""An RFC 9110 suffix-range, e.g. ``-500`` (the final N units)."""
|
|
35
|
+
|
|
36
|
+
suffix_length: NonNegativeInt
|
|
37
|
+
|
|
38
|
+
def __init__(self, suffix_length: int) -> None:
|
|
39
|
+
object.__setattr__(self, "suffix_length", NonNegativeInt(suffix_length))
|
|
40
|
+
|
|
41
|
+
def __str__(self) -> str:
|
|
42
|
+
return f"-{self.suffix_length}"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class RangeVisitor(NodeVisitor):
|
|
46
|
+
@staticmethod
|
|
47
|
+
def visit_range_unit(node: Node) -> RangeUnit:
|
|
48
|
+
return RangeUnit(node.value, parse=False)
|
|
49
|
+
|
|
50
|
+
@staticmethod
|
|
51
|
+
def visit_first_pos(node: Node) -> NonNegativeInt:
|
|
52
|
+
return NonNegativeInt(node.value)
|
|
53
|
+
|
|
54
|
+
@staticmethod
|
|
55
|
+
def visit_last_pos(node: Node) -> NonNegativeInt:
|
|
56
|
+
return NonNegativeInt(node.value)
|
|
57
|
+
|
|
58
|
+
@staticmethod
|
|
59
|
+
def visit_suffix_length(node: Node) -> NonNegativeInt:
|
|
60
|
+
return NonNegativeInt(node.value)
|
|
61
|
+
|
|
62
|
+
def visit_int_range(self, node: Node) -> IntRange:
|
|
63
|
+
items = [x for x in map(self.visit, node.children) if x is not None]
|
|
64
|
+
return IntRange(items[0], items[1] if len(items) == 2 else None)
|
|
65
|
+
|
|
66
|
+
def visit_suffix_range(self, node: Node) -> SuffixRange:
|
|
67
|
+
items = [x for x in map(self.visit, node.children) if x is not None]
|
|
68
|
+
return SuffixRange(items[0])
|
|
69
|
+
|
|
70
|
+
def visit_range_spec(self, node: Node) -> IntRange | SuffixRange:
|
|
71
|
+
# range-spec is int-range / suffix-range / other-range. This model only
|
|
72
|
+
# represents byte ranges, so reject other-range explicitly rather than
|
|
73
|
+
# letting it drop out silently (it visits to None).
|
|
74
|
+
result = self.visit(node.children[0])
|
|
75
|
+
if not isinstance(result, (IntRange, SuffixRange)):
|
|
76
|
+
raise ValueError("Unsupported range-spec (other-range).")
|
|
77
|
+
return result
|
|
78
|
+
|
|
79
|
+
def visit_range_set(self, node: Node) -> list[IntRange | SuffixRange]:
|
|
80
|
+
return [
|
|
81
|
+
x
|
|
82
|
+
for x in map(self.visit, node.children)
|
|
83
|
+
if isinstance(x, (IntRange, SuffixRange))
|
|
84
|
+
]
|
|
85
|
+
|
|
86
|
+
def visit_ranges_specifier(
|
|
87
|
+
self, node: Node
|
|
88
|
+
) -> tuple[RangeUnit, list[IntRange | SuffixRange]]:
|
|
89
|
+
range_unit = RangeUnit("bytes", parse=False)
|
|
90
|
+
ranges: list[IntRange | SuffixRange] = []
|
|
91
|
+
for child in node.children:
|
|
92
|
+
result = self.visit(child)
|
|
93
|
+
if isinstance(result, RangeUnit):
|
|
94
|
+
range_unit = result
|
|
95
|
+
elif isinstance(result, list):
|
|
96
|
+
ranges = result
|
|
97
|
+
return (range_unit, ranges)
|
|
98
|
+
|
|
99
|
+
def visit_range(self, node: Node) -> tuple[RangeUnit, list[IntRange | SuffixRange]]:
|
|
100
|
+
return next(filter(None, map(self.visit, node.children)))
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from abnf import Node, NodeVisitor
|
|
2
|
+
from abnf.grammars import rfc9110
|
|
3
|
+
|
|
4
|
+
import http_fields.visitors.rfc9110.token as token
|
|
5
|
+
from http_fields.parsedobjs import CaselessMixin, ParsedStr
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class RangeUnit(CaselessMixin, ParsedStr):
|
|
9
|
+
parser = rfc9110.Rule("range-unit")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class RangeUnitVisitor(NodeVisitor):
|
|
13
|
+
visit_token = token.TokenVisitor()
|
|
14
|
+
|
|
15
|
+
def visit_range_unit(self, node: Node):
|
|
16
|
+
item = next(filter(None, map(self.visit, node.children)))
|
|
17
|
+
return RangeUnit(item, parse=False)
|
|
@@ -0,0 +1,20 @@
|
|
|
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 RetryAfterVisitor(NodeVisitor):
|
|
9
|
+
"""NodeVisitor subclass for retry-after."""
|
|
10
|
+
|
|
11
|
+
visit_http_date = httpdate.HttpDateVisitor()
|
|
12
|
+
|
|
13
|
+
def visit_retry_after(self, node: Node) -> datetime | int:
|
|
14
|
+
return self.visit(node.children[0])
|
|
15
|
+
|
|
16
|
+
@staticmethod
|
|
17
|
+
def visit_delay_seconds(node: Node) -> int:
|
|
18
|
+
delay_seconds = int(node.value)
|
|
19
|
+
assert delay_seconds >= 0 # if not, there is a parser issue.
|
|
20
|
+
return delay_seconds
|
|
@@ -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__ = ["ServerVisitor"]
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ServerVisitor(NodeVisitor):
|
|
11
|
+
visit_comment = comment.CommentVisitor()
|
|
12
|
+
visit_product = product.ProductVisitor()
|
|
13
|
+
visit_token = token.TokenVisitor()
|
|
14
|
+
|
|
15
|
+
def visit_server(self, node: Node) -> list[product.Product | comment.Comment]:
|
|
16
|
+
return list(filter(None, map(self.visit, node.children)))
|