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,356 @@
|
|
|
1
|
+
"""Structured Field Values for HTTP (RFC 9651).
|
|
2
|
+
|
|
3
|
+
Provides the value model (``Item``, ``InnerList``, ``Token``, ``DisplayString``) plus parse and
|
|
4
|
+
serialize functions for the three top-level types: Item, List, and Dictionary. Bare items map to
|
|
5
|
+
native Python types: Integer -> int, Decimal -> decimal.Decimal, String -> str, Token -> Token,
|
|
6
|
+
Boolean -> bool, Byte Sequence -> bytes, Date -> datetime, Display String -> DisplayString.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import base64
|
|
12
|
+
import re
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
from datetime import datetime, timezone
|
|
15
|
+
from decimal import Decimal
|
|
16
|
+
from typing import TypeAlias
|
|
17
|
+
|
|
18
|
+
from abnf import NodeVisitor
|
|
19
|
+
from abnf.grammars import rfc9651
|
|
20
|
+
from abnf.parser import Node
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"BareItem",
|
|
24
|
+
"DisplayString",
|
|
25
|
+
"InnerList",
|
|
26
|
+
"Item",
|
|
27
|
+
"Member",
|
|
28
|
+
"Parameters",
|
|
29
|
+
"Token",
|
|
30
|
+
"parse_dictionary",
|
|
31
|
+
"parse_item",
|
|
32
|
+
"parse_list",
|
|
33
|
+
"serialize_dictionary",
|
|
34
|
+
"serialize_item",
|
|
35
|
+
"serialize_list",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class Token(str):
|
|
40
|
+
"""A Structured Fields Token (an unquoted identifier), distinct from a String."""
|
|
41
|
+
|
|
42
|
+
__slots__ = ()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class DisplayString(str):
|
|
46
|
+
"""A Structured Fields Display String (Unicode text), distinct from a String."""
|
|
47
|
+
|
|
48
|
+
__slots__ = ()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# A bare item is one of these Python types (Token/DisplayString are str subclasses).
|
|
52
|
+
BareItem: TypeAlias = (
|
|
53
|
+
int | Decimal | bool | bytes | datetime | str | Token | DisplayString
|
|
54
|
+
)
|
|
55
|
+
Parameters: TypeAlias = tuple[tuple[str, object], ...]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass(frozen=True)
|
|
59
|
+
class Item:
|
|
60
|
+
"""A bare item together with its parameters."""
|
|
61
|
+
|
|
62
|
+
value: object
|
|
63
|
+
parameters: tuple[tuple[str, object], ...] = ()
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass(frozen=True)
|
|
67
|
+
class InnerList:
|
|
68
|
+
"""An ordered list of items, with parameters, i.e. ``(a b);q=1``."""
|
|
69
|
+
|
|
70
|
+
items: tuple[Item, ...] = ()
|
|
71
|
+
parameters: tuple[tuple[str, object], ...] = field(default_factory=tuple)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# A member of a List or Dictionary is either an Item or an InnerList.
|
|
75
|
+
Member: TypeAlias = Item | InnerList
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# --- string / display-string / byte helpers -------------------------------------------------
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _unescape_string(raw: str) -> str:
|
|
82
|
+
inner = raw[1:-1] # strip the surrounding DQUOTEs
|
|
83
|
+
out: list[str] = []
|
|
84
|
+
i = 0
|
|
85
|
+
while i < len(inner):
|
|
86
|
+
if inner[i] == "\\":
|
|
87
|
+
out.append(inner[i + 1])
|
|
88
|
+
i += 2
|
|
89
|
+
else:
|
|
90
|
+
out.append(inner[i])
|
|
91
|
+
i += 1
|
|
92
|
+
return "".join(out)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _escape_string(value: str) -> str:
|
|
96
|
+
return value.replace("\\", "\\\\").replace('"', '\\"')
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _decode_displaystring(raw: str) -> DisplayString:
|
|
100
|
+
inner = raw[2:-1] # strip the leading %" and trailing "
|
|
101
|
+
buf = bytearray()
|
|
102
|
+
i = 0
|
|
103
|
+
while i < len(inner):
|
|
104
|
+
if inner[i] == "%":
|
|
105
|
+
buf.append(int(inner[i + 1 : i + 3], 16))
|
|
106
|
+
i += 3
|
|
107
|
+
else:
|
|
108
|
+
buf.append(ord(inner[i]))
|
|
109
|
+
i += 1
|
|
110
|
+
return DisplayString(buf.decode("utf-8"))
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _encode_displaystring(value: str) -> str:
|
|
114
|
+
out = ['%"']
|
|
115
|
+
for byte in value.encode("utf-8"):
|
|
116
|
+
if byte in (0x25, 0x22) or byte < 0x20 or byte > 0x7E:
|
|
117
|
+
out.append(f"%{byte:02x}")
|
|
118
|
+
else:
|
|
119
|
+
out.append(chr(byte))
|
|
120
|
+
out.append('"')
|
|
121
|
+
return "".join(out)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _serialize_decimal(value: Decimal) -> str:
|
|
125
|
+
text = f"{value:.3f}".rstrip("0")
|
|
126
|
+
return text + "0" if text.endswith(".") else text
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
# --- parsing --------------------------------------------------------------------------------
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class _StructuredFieldsVisitor(NodeVisitor):
|
|
133
|
+
@staticmethod
|
|
134
|
+
def visit_sf_integer(node: Node) -> int:
|
|
135
|
+
return int(node.value)
|
|
136
|
+
|
|
137
|
+
@staticmethod
|
|
138
|
+
def visit_sf_decimal(node: Node) -> Decimal:
|
|
139
|
+
return Decimal(node.value)
|
|
140
|
+
|
|
141
|
+
@staticmethod
|
|
142
|
+
def visit_sf_string(node: Node) -> str:
|
|
143
|
+
return _unescape_string(node.value)
|
|
144
|
+
|
|
145
|
+
@staticmethod
|
|
146
|
+
def visit_sf_token(node: Node) -> Token:
|
|
147
|
+
return Token(node.value)
|
|
148
|
+
|
|
149
|
+
@staticmethod
|
|
150
|
+
def visit_sf_boolean(node: Node) -> bool:
|
|
151
|
+
return node.value == "?1"
|
|
152
|
+
|
|
153
|
+
@staticmethod
|
|
154
|
+
def visit_sf_binary(node: Node) -> bytes:
|
|
155
|
+
return base64.b64decode(node.value.strip(":"))
|
|
156
|
+
|
|
157
|
+
@staticmethod
|
|
158
|
+
def visit_sf_date(node: Node) -> datetime:
|
|
159
|
+
return datetime.fromtimestamp(int(node.value.lstrip("@")), tz=timezone.utc)
|
|
160
|
+
|
|
161
|
+
@staticmethod
|
|
162
|
+
def visit_sf_displaystring(node: Node) -> DisplayString:
|
|
163
|
+
return _decode_displaystring(node.value)
|
|
164
|
+
|
|
165
|
+
def visit_bare_item(self, node: Node) -> object:
|
|
166
|
+
return self.visit(node.children[0])
|
|
167
|
+
|
|
168
|
+
def visit_param_value(self, node: Node) -> object:
|
|
169
|
+
return self.visit(node.children[0])
|
|
170
|
+
|
|
171
|
+
def visit_parameter(self, node: Node) -> tuple[str, object]:
|
|
172
|
+
key = ""
|
|
173
|
+
value: object = True # a bare parameter key means Boolean true
|
|
174
|
+
for child in node.children:
|
|
175
|
+
if child.name == "param-key":
|
|
176
|
+
key = child.value
|
|
177
|
+
elif child.name == "param-value":
|
|
178
|
+
value = self.visit(child)
|
|
179
|
+
return (key, value)
|
|
180
|
+
|
|
181
|
+
def visit_parameters(self, node: Node) -> tuple[tuple[str, object], ...]:
|
|
182
|
+
# RFC 9651 section 4.2.3.2: parameters are an ordered map; a duplicate key
|
|
183
|
+
# keeps the last value at the first occurrence's position.
|
|
184
|
+
params: dict[str, object] = {}
|
|
185
|
+
for c in node.children:
|
|
186
|
+
if c.name == "parameter":
|
|
187
|
+
key, value = self.visit(c)
|
|
188
|
+
params[key] = value
|
|
189
|
+
return tuple(params.items())
|
|
190
|
+
|
|
191
|
+
def visit_sf_item(self, node: Node) -> Item:
|
|
192
|
+
value: object = None
|
|
193
|
+
params: tuple[tuple[str, object], ...] = ()
|
|
194
|
+
for child in node.children:
|
|
195
|
+
if child.name == "bare-item":
|
|
196
|
+
value = self.visit(child)
|
|
197
|
+
elif child.name == "parameters":
|
|
198
|
+
params = self.visit(child)
|
|
199
|
+
return Item(value, params)
|
|
200
|
+
|
|
201
|
+
def visit_inner_list(self, node: Node) -> InnerList:
|
|
202
|
+
items = tuple(self.visit(c) for c in node.children if c.name == "sf-item")
|
|
203
|
+
params: tuple[tuple[str, object], ...] = ()
|
|
204
|
+
for child in node.children:
|
|
205
|
+
if child.name == "parameters":
|
|
206
|
+
params = self.visit(child)
|
|
207
|
+
return InnerList(items, params)
|
|
208
|
+
|
|
209
|
+
def visit_list_member(self, node: Node) -> object:
|
|
210
|
+
return self.visit(node.children[0])
|
|
211
|
+
|
|
212
|
+
def visit_sf_list(self, node: Node) -> tuple[object, ...]:
|
|
213
|
+
return tuple(self.visit(c) for c in node.children if c.name == "list-member")
|
|
214
|
+
|
|
215
|
+
def visit_member_value(self, node: Node) -> object:
|
|
216
|
+
return self.visit(node.children[0])
|
|
217
|
+
|
|
218
|
+
def visit_dict_member(self, node: Node) -> tuple[str, object]:
|
|
219
|
+
key = ""
|
|
220
|
+
member: object = None
|
|
221
|
+
params: tuple[tuple[str, object], ...] = ()
|
|
222
|
+
for child in node.children:
|
|
223
|
+
if child.name == "member-key":
|
|
224
|
+
key = child.value
|
|
225
|
+
elif child.name == "member-value":
|
|
226
|
+
member = self.visit(child)
|
|
227
|
+
elif child.name == "parameters":
|
|
228
|
+
params = self.visit(child)
|
|
229
|
+
if member is None:
|
|
230
|
+
member = Item(True, params) # a bare key means Boolean true
|
|
231
|
+
return (key, member)
|
|
232
|
+
|
|
233
|
+
def visit_sf_dictionary(self, node: Node) -> tuple[tuple[str, object], ...]:
|
|
234
|
+
# RFC 9651 section 4.2.2: a duplicate key keeps the last value at the
|
|
235
|
+
# first occurrence's position.
|
|
236
|
+
members: dict[str, object] = {}
|
|
237
|
+
for c in node.children:
|
|
238
|
+
if c.name == "dict-member":
|
|
239
|
+
key, member = self.visit(c)
|
|
240
|
+
members[key] = member
|
|
241
|
+
return tuple(members.items())
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
_VISITOR = _StructuredFieldsVisitor()
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def parse_item(value: str) -> Item:
|
|
248
|
+
"""Parse a Structured Fields Item."""
|
|
249
|
+
return _VISITOR.visit(rfc9651.Rule("sf-item").parse_all(value))
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def parse_list(value: str) -> tuple[Item | InnerList, ...]:
|
|
253
|
+
"""Parse a Structured Fields List into a tuple of Item / InnerList. An empty string is an
|
|
254
|
+
empty list."""
|
|
255
|
+
if not value.strip():
|
|
256
|
+
return ()
|
|
257
|
+
return _VISITOR.visit(rfc9651.Rule("sf-list").parse_all(value))
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def parse_dictionary(value: str) -> tuple[tuple[str, Item | InnerList], ...]:
|
|
261
|
+
"""Parse a Structured Fields Dictionary into a tuple of (key, Item / InnerList) pairs
|
|
262
|
+
(ordered). An empty string is an empty dictionary."""
|
|
263
|
+
if not value.strip():
|
|
264
|
+
return ()
|
|
265
|
+
return _VISITOR.visit(rfc9651.Rule("sf-dictionary").parse_all(value))
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
# --- serializing ----------------------------------------------------------------------------
|
|
269
|
+
|
|
270
|
+
# RFC 9651 section 4.1 requires serializers to fail on values that cannot be
|
|
271
|
+
# represented, rather than emit output that will not round-trip.
|
|
272
|
+
_INTEGER_MAX = 10**15 - 1 # section 4.1.4
|
|
273
|
+
_DECIMAL_INT_MAX = Decimal(10) ** 12 # section 4.1.5: integer part <= 12 digits
|
|
274
|
+
_TOKEN_RE = re.compile(r"^[A-Za-z*][A-Za-z0-9!#$%&'*+\-.^_`|~:/]*$") # section 4.1.7
|
|
275
|
+
_KEY_RE = re.compile(r"^[a-z*][a-z0-9_.*-]*$") # section 3.1.2 key
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _validate_key(key: object) -> str:
|
|
279
|
+
if not isinstance(key, str) or not _KEY_RE.match(key):
|
|
280
|
+
raise ValueError(f"Invalid Structured Fields key: {key!r}.")
|
|
281
|
+
return key
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def serialize_bare(value: object) -> str:
|
|
285
|
+
"""Serialize a bare item to its Structured Fields text form."""
|
|
286
|
+
if isinstance(value, bool):
|
|
287
|
+
return "?1" if value else "?0"
|
|
288
|
+
if isinstance(value, int):
|
|
289
|
+
if not -_INTEGER_MAX <= value <= _INTEGER_MAX:
|
|
290
|
+
raise ValueError(f"Integer {value} is out of range for Structured Fields.")
|
|
291
|
+
return str(value)
|
|
292
|
+
if isinstance(value, Decimal):
|
|
293
|
+
if abs(value) >= _DECIMAL_INT_MAX:
|
|
294
|
+
raise ValueError(f"Decimal {value} integer part exceeds 12 digits.")
|
|
295
|
+
return _serialize_decimal(value)
|
|
296
|
+
if isinstance(value, DisplayString):
|
|
297
|
+
return _encode_displaystring(value)
|
|
298
|
+
if isinstance(value, Token):
|
|
299
|
+
if not _TOKEN_RE.match(value):
|
|
300
|
+
raise ValueError(f"Invalid Structured Fields Token: {value!r}.")
|
|
301
|
+
return str(value)
|
|
302
|
+
if isinstance(value, str):
|
|
303
|
+
if any(ord(c) < 0x20 or ord(c) > 0x7E for c in value):
|
|
304
|
+
raise ValueError(f"String {value!r} contains characters outside %x20-7E.")
|
|
305
|
+
return '"' + _escape_string(value) + '"'
|
|
306
|
+
if isinstance(value, bytes):
|
|
307
|
+
return ":" + base64.b64encode(value).decode("ascii") + ":"
|
|
308
|
+
if isinstance(value, datetime):
|
|
309
|
+
# A naive datetime is interpreted as UTC so output does not depend on the
|
|
310
|
+
# machine's local timezone.
|
|
311
|
+
if value.tzinfo is None:
|
|
312
|
+
value = value.replace(tzinfo=timezone.utc)
|
|
313
|
+
return "@" + str(int(value.timestamp()))
|
|
314
|
+
raise TypeError(f"Cannot serialize {value!r} as a Structured Fields bare item.")
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def _serialize_params(params: tuple[tuple[str, object], ...]) -> str:
|
|
318
|
+
out: list[str] = []
|
|
319
|
+
for key, value in params:
|
|
320
|
+
_validate_key(key)
|
|
321
|
+
out.append(f";{key}" if value is True else f";{key}={serialize_bare(value)}")
|
|
322
|
+
return "".join(out)
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def serialize_item(item: Item) -> str:
|
|
326
|
+
"""Serialize an Item."""
|
|
327
|
+
return serialize_bare(item.value) + _serialize_params(item.parameters)
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def _serialize_inner_list(inner: InnerList) -> str:
|
|
331
|
+
body = " ".join(serialize_item(i) for i in inner.items)
|
|
332
|
+
return f"({body})" + _serialize_params(inner.parameters)
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def _serialize_member(member: object) -> str:
|
|
336
|
+
if isinstance(member, InnerList):
|
|
337
|
+
return _serialize_inner_list(member)
|
|
338
|
+
assert isinstance(member, Item)
|
|
339
|
+
return serialize_item(member)
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def serialize_list(members: tuple[object, ...]) -> str:
|
|
343
|
+
"""Serialize a List of Item / InnerList members."""
|
|
344
|
+
return ", ".join(_serialize_member(m) for m in members)
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def serialize_dictionary(members: tuple[tuple[str, object], ...]) -> str:
|
|
348
|
+
"""Serialize a Dictionary of (key, Item / InnerList) members."""
|
|
349
|
+
parts: list[str] = []
|
|
350
|
+
for key, member in members:
|
|
351
|
+
_validate_key(key)
|
|
352
|
+
if isinstance(member, Item) and member.value is True:
|
|
353
|
+
parts.append(key + _serialize_params(member.parameters))
|
|
354
|
+
else:
|
|
355
|
+
parts.append(f"{key}={_serialize_member(member)}")
|
|
356
|
+
return ", ".join(parts)
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""Base classes for headers whose value is a Structured Field (RFC 9651)."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import ClassVar
|
|
5
|
+
|
|
6
|
+
from abnf import ParseError, Rule
|
|
7
|
+
from abnf.grammars import rfc9651
|
|
8
|
+
from typing_extensions import Self
|
|
9
|
+
|
|
10
|
+
from http_fields.header import Header
|
|
11
|
+
from http_fields.structuredfields import (
|
|
12
|
+
InnerList,
|
|
13
|
+
Item,
|
|
14
|
+
parse_dictionary,
|
|
15
|
+
parse_list,
|
|
16
|
+
serialize_dictionary,
|
|
17
|
+
serialize_list,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True)
|
|
22
|
+
class StructuredListHeader(Header):
|
|
23
|
+
"""Base for headers that are a Structured Fields List (Cache-Status, Proxy-Status)."""
|
|
24
|
+
|
|
25
|
+
rule: ClassVar[Rule] = rfc9651.Rule("sf-list")
|
|
26
|
+
|
|
27
|
+
members: tuple[Item | InnerList, ...]
|
|
28
|
+
|
|
29
|
+
def __init__(self, *members: Item | InnerList) -> None:
|
|
30
|
+
object.__setattr__(self, "members", tuple(members))
|
|
31
|
+
# Check field types first, so a wrong-typed member gets the contract's TypeError
|
|
32
|
+
# (pointing at parse()) rather than tripping the serializer's Item assertions below.
|
|
33
|
+
self._check_field_types()
|
|
34
|
+
# Serializing validates every member (rejecting injection / out-of-range values
|
|
35
|
+
# via serialize_bare) and caches the result; a valid serialization is grammar-valid
|
|
36
|
+
# by construction, so no re-parse is needed.
|
|
37
|
+
_ = self.value
|
|
38
|
+
|
|
39
|
+
@classmethod
|
|
40
|
+
def parse(cls, value: str) -> Self:
|
|
41
|
+
cls._check_length(value)
|
|
42
|
+
try:
|
|
43
|
+
return cls(*parse_list(value))
|
|
44
|
+
except ParseError as exc:
|
|
45
|
+
raise ValueError(f'Invalid {cls.__name__} value "{value}".') from exc
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def value(self) -> str:
|
|
49
|
+
return serialize_list(self.members)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass(frozen=True)
|
|
53
|
+
class DigestHeader(Header):
|
|
54
|
+
"""Base for headers that are a Structured Fields Dictionary of byte-sequence digests
|
|
55
|
+
(Content-Digest, Repr-Digest)."""
|
|
56
|
+
|
|
57
|
+
rule: ClassVar[Rule] = rfc9651.Rule("sf-dictionary")
|
|
58
|
+
|
|
59
|
+
digests: tuple[tuple[str, bytes], ...]
|
|
60
|
+
|
|
61
|
+
def __init__(self, *digests: tuple[str, bytes]) -> None:
|
|
62
|
+
object.__setattr__(self, "digests", tuple(digests))
|
|
63
|
+
# Check field types first, so a wrong-typed member gets the contract's TypeError
|
|
64
|
+
# (pointing at parse()) rather than an unpack/serializer error below.
|
|
65
|
+
self._check_field_types()
|
|
66
|
+
# Serializing validates every dictionary key (via _validate_key) and caches the
|
|
67
|
+
# result; a valid serialization is grammar-valid by construction, so no re-parse.
|
|
68
|
+
_ = self.value
|
|
69
|
+
|
|
70
|
+
@classmethod
|
|
71
|
+
def parse(cls, value: str) -> Self:
|
|
72
|
+
cls._check_length(value)
|
|
73
|
+
try:
|
|
74
|
+
members = parse_dictionary(value)
|
|
75
|
+
except ParseError as exc:
|
|
76
|
+
raise ValueError(f'Invalid {cls.__name__} value "{value}".') from exc
|
|
77
|
+
digests: list[tuple[str, bytes]] = []
|
|
78
|
+
for key, m in members:
|
|
79
|
+
# Each digest value must be a byte sequence; anything else is
|
|
80
|
+
# malformed and must be rejected rather than silently discarded.
|
|
81
|
+
if not (isinstance(m, Item) and isinstance(m.value, bytes)):
|
|
82
|
+
raise ValueError(
|
|
83
|
+
f"{cls.__name__} member {key!r} is not a byte sequence."
|
|
84
|
+
)
|
|
85
|
+
digests.append((key, m.value))
|
|
86
|
+
return cls(*digests)
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def value(self) -> str:
|
|
90
|
+
return serialize_dictionary(tuple((key, Item(d)) for key, d in self.digests))
|
http_fields/te.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""TE header class."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import ClassVar
|
|
5
|
+
|
|
6
|
+
from abnf import Rule
|
|
7
|
+
from abnf.grammars import rfc9110
|
|
8
|
+
from typing_extensions import Self
|
|
9
|
+
|
|
10
|
+
from http_fields.header import Header
|
|
11
|
+
from http_fields.visitors.rfc9110.te import TCoding, TEVisitor
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class TE(Header):
|
|
16
|
+
"""TE header, as defined by RFC 9110: the transfer-codings the client is willing to
|
|
17
|
+
accept (plus the special ``trailers`` token), each with an optional weight."""
|
|
18
|
+
|
|
19
|
+
name: ClassVar[str] = "TE"
|
|
20
|
+
rule: ClassVar[Rule] = rfc9110.Rule("TE")
|
|
21
|
+
visitor: ClassVar[TEVisitor] = TEVisitor()
|
|
22
|
+
|
|
23
|
+
codings: tuple[TCoding, ...]
|
|
24
|
+
|
|
25
|
+
def __init__(self, *codings: TCoding) -> None:
|
|
26
|
+
# Each TCoding self-validates (Token coding, safe Weight/Parameter), so the
|
|
27
|
+
# field-type check fully guarantees a safe serialized value -- no re-parse.
|
|
28
|
+
object.__setattr__(self, "codings", tuple(codings))
|
|
29
|
+
|
|
30
|
+
@classmethod
|
|
31
|
+
def parse(cls, value: str) -> Self:
|
|
32
|
+
return cls(*cls.visitor.visit(cls._node(value)))
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def value(self) -> str:
|
|
36
|
+
return ", ".join(str(coding) for coding in self.codings)
|
http_fields/trailer.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Trailer header class."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import ClassVar
|
|
5
|
+
|
|
6
|
+
from abnf import Node, NodeVisitor, Rule
|
|
7
|
+
from abnf.grammars import rfc9110
|
|
8
|
+
from typing_extensions import Self
|
|
9
|
+
|
|
10
|
+
from http_fields.header import Header
|
|
11
|
+
from http_fields.visitors.rfc9110 import FieldName
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class TrailerVisitor(NodeVisitor):
|
|
15
|
+
def visit_trailer(self, node: Node) -> list[str]:
|
|
16
|
+
return [x for x in map(self.visit, node.children) if x is not None]
|
|
17
|
+
|
|
18
|
+
@staticmethod
|
|
19
|
+
def visit_field_name(node: Node) -> str:
|
|
20
|
+
return node.value
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True)
|
|
24
|
+
class Trailer(Header):
|
|
25
|
+
"""Trailer header, as defined by RFC 9110."""
|
|
26
|
+
|
|
27
|
+
name: ClassVar[str] = "Trailer"
|
|
28
|
+
rule: ClassVar[Rule] = rfc9110.Rule("Trailer")
|
|
29
|
+
visitor: ClassVar[TrailerVisitor] = TrailerVisitor()
|
|
30
|
+
|
|
31
|
+
field_names: tuple[FieldName, ...]
|
|
32
|
+
|
|
33
|
+
def __init__(self, *field_names: FieldName) -> None:
|
|
34
|
+
object.__setattr__(self, "field_names", tuple(field_names))
|
|
35
|
+
|
|
36
|
+
@classmethod
|
|
37
|
+
def parse(cls, value: str) -> Self:
|
|
38
|
+
return cls(
|
|
39
|
+
*(FieldName(f, parse=False) for f in cls.visitor.visit(cls._node(value)))
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def value(self) -> str:
|
|
44
|
+
return ", ".join(self.field_names)
|
http_fields/upgrade.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Upgrade header class."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import ClassVar
|
|
5
|
+
|
|
6
|
+
from abnf import Rule
|
|
7
|
+
from abnf.grammars import rfc9110
|
|
8
|
+
from typing_extensions import Self
|
|
9
|
+
|
|
10
|
+
from http_fields.header import Header
|
|
11
|
+
from http_fields.visitors.rfc9110.upgrade import Protocol, UpgradeVisitor
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class Upgrade(Header):
|
|
16
|
+
"""Upgrade header, as defined by RFC 9110: a list of protocols, e.g. ``HTTP/2, WebSocket``."""
|
|
17
|
+
|
|
18
|
+
name: ClassVar[str] = "Upgrade"
|
|
19
|
+
rule: ClassVar[Rule] = rfc9110.Rule("Upgrade")
|
|
20
|
+
visitor: ClassVar[UpgradeVisitor] = UpgradeVisitor()
|
|
21
|
+
|
|
22
|
+
protocols: tuple[Protocol, ...]
|
|
23
|
+
|
|
24
|
+
def __init__(self, *protocols: Protocol) -> None:
|
|
25
|
+
# Each Protocol self-validates on construction, so the field-type check is a
|
|
26
|
+
# complete guarantee -- no need to re-parse the serialized value here.
|
|
27
|
+
object.__setattr__(self, "protocols", tuple(protocols))
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
def parse(cls, value: str) -> Self:
|
|
31
|
+
return cls(*cls.visitor.visit(cls._node(value)))
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def value(self) -> str:
|
|
35
|
+
return ", ".join(str(protocol) for protocol in self.protocols)
|
http_fields/uriheader.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Base class for headers whose value is a single URI reference."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import ClassVar
|
|
5
|
+
|
|
6
|
+
from typing_extensions import Self
|
|
7
|
+
|
|
8
|
+
from http_fields.header import Header
|
|
9
|
+
from http_fields.parsedobjs import ParsedStr
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True)
|
|
13
|
+
class UriHeader(Header):
|
|
14
|
+
"""Base for headers whose value is a single URI reference.
|
|
15
|
+
|
|
16
|
+
Concrete subclasses supply ``name``, ``rule``, and ``uri_type`` -- a self-validating
|
|
17
|
+
ParsedStr over that grammar rule. The uri validates on construction (not only via
|
|
18
|
+
:meth:`parse`), so a raw constructor call cannot smuggle CR/LF/NUL or other invalid
|
|
19
|
+
content into the serialized header.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
uri_type: ClassVar[type[ParsedStr]]
|
|
23
|
+
|
|
24
|
+
uri: ParsedStr
|
|
25
|
+
|
|
26
|
+
def __init__(self, uri: str) -> None:
|
|
27
|
+
# The uri self-validates as a leaf; an already-parsed value passes through.
|
|
28
|
+
object.__setattr__(self, "uri", type(self).uri_type(uri))
|
|
29
|
+
|
|
30
|
+
@classmethod
|
|
31
|
+
def parse(cls, value: str) -> Self:
|
|
32
|
+
return cls(value)
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def value(self) -> str:
|
|
36
|
+
return self.uri
|
http_fields/useragent.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""User-Agent header class."""
|
|
2
|
+
|
|
3
|
+
from typing import ClassVar
|
|
4
|
+
|
|
5
|
+
from abnf import Rule
|
|
6
|
+
from abnf.grammars import rfc9110
|
|
7
|
+
|
|
8
|
+
from http_fields.productlistheader import ProductListHeader
|
|
9
|
+
from http_fields.visitors.rfc9110 import UserAgentVisitor
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class UserAgent(ProductListHeader):
|
|
13
|
+
"""User-Agent header, as defined by RFC 9110. ``items`` is a sequence of Product or
|
|
14
|
+
Comment values."""
|
|
15
|
+
|
|
16
|
+
name: ClassVar[str] = "User-Agent"
|
|
17
|
+
rule: ClassVar[Rule] = rfc9110.Rule("User-Agent")
|
|
18
|
+
visitor = UserAgentVisitor()
|
http_fields/vary.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Vary header class."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import ClassVar
|
|
5
|
+
|
|
6
|
+
from abnf import Rule
|
|
7
|
+
from abnf.grammars import rfc7231
|
|
8
|
+
from typing_extensions import Self
|
|
9
|
+
|
|
10
|
+
from http_fields.header import Header
|
|
11
|
+
from http_fields.visitors.rfc9110 import FieldName, VaryVisitor
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class Vary(Header):
|
|
16
|
+
"""Vary header, as defined by RFC 9110.
|
|
17
|
+
|
|
18
|
+
An empty ``field_names`` serializes as ``*`` (vary by anything).
|
|
19
|
+
|
|
20
|
+
Vary: accept-encoding, accept-language
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
name: ClassVar[str] = "Vary"
|
|
24
|
+
rule: ClassVar[Rule] = rfc7231.Rule("Vary")
|
|
25
|
+
visitor: ClassVar[VaryVisitor] = VaryVisitor()
|
|
26
|
+
|
|
27
|
+
field_names: tuple[FieldName, ...]
|
|
28
|
+
|
|
29
|
+
def __init__(self, *field_names: FieldName) -> None:
|
|
30
|
+
object.__setattr__(self, "field_names", tuple(field_names))
|
|
31
|
+
|
|
32
|
+
@classmethod
|
|
33
|
+
def parse(cls, value: str) -> Self:
|
|
34
|
+
return cls(
|
|
35
|
+
*(FieldName(f, parse=False) for f in cls.visitor.visit(cls._node(value)))
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def value(self) -> str:
|
|
40
|
+
return ", ".join(self.field_names) if self.field_names else "*"
|
http_fields/via.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Via header class."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import ClassVar
|
|
5
|
+
|
|
6
|
+
from abnf import Rule
|
|
7
|
+
from abnf.grammars import rfc9110
|
|
8
|
+
from typing_extensions import Self
|
|
9
|
+
|
|
10
|
+
from http_fields.header import Header
|
|
11
|
+
from http_fields.visitors.rfc9110.via import ViaElement, ViaVisitor
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class Via(Header):
|
|
16
|
+
"""Via header, as defined by RFC 9110: the intermediaries a message passed through."""
|
|
17
|
+
|
|
18
|
+
name: ClassVar[str] = "Via"
|
|
19
|
+
rule: ClassVar[Rule] = rfc9110.Rule("Via")
|
|
20
|
+
visitor: ClassVar[ViaVisitor] = ViaVisitor()
|
|
21
|
+
|
|
22
|
+
elements: tuple[ViaElement, ...]
|
|
23
|
+
|
|
24
|
+
def __init__(self, *elements: ViaElement) -> None:
|
|
25
|
+
# Each ViaElement self-validates (leaf-typed fields), so the field-type check
|
|
26
|
+
# fully guarantees a safe serialized value -- no re-parse.
|
|
27
|
+
object.__setattr__(self, "elements", tuple(elements))
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
def parse(cls, value: str) -> Self:
|
|
31
|
+
return cls(*cls.visitor.visit(cls._node(value)))
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def value(self) -> str:
|
|
35
|
+
return ", ".join(str(element) for element in self.elements)
|