http-fields 0.1.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. http_fields/__init__.py +193 -0
  2. http_fields/accept.py +37 -0
  3. http_fields/acceptcharset.py +48 -0
  4. http_fields/acceptencoding.py +42 -0
  5. http_fields/acceptlanguage.py +47 -0
  6. http_fields/acceptranges.py +35 -0
  7. http_fields/accesscontrol.py +197 -0
  8. http_fields/age.py +34 -0
  9. http_fields/allow.py +46 -0
  10. http_fields/altsvc.py +70 -0
  11. http_fields/authbases.py +79 -0
  12. http_fields/authenticationinfo.py +19 -0
  13. http_fields/authorization.py +23 -0
  14. http_fields/cachecontrol.py +119 -0
  15. http_fields/cachestatus.py +12 -0
  16. http_fields/connection.py +35 -0
  17. http_fields/contentdigest.py +19 -0
  18. http_fields/contentdisposition.py +66 -0
  19. http_fields/contentencoding.py +45 -0
  20. http_fields/contentlanguage.py +38 -0
  21. http_fields/contentlength.py +34 -0
  22. http_fields/contentlocation.py +24 -0
  23. http_fields/contentrange.py +74 -0
  24. http_fields/contenttype.py +104 -0
  25. http_fields/cookie.py +38 -0
  26. http_fields/date.py +17 -0
  27. http_fields/dateheader.py +32 -0
  28. http_fields/entitytaglist.py +39 -0
  29. http_fields/etag.py +46 -0
  30. http_fields/expect.py +52 -0
  31. http_fields/expires.py +17 -0
  32. http_fields/forwarded.py +36 -0
  33. http_fields/fromheader.py +42 -0
  34. http_fields/header.py +250 -0
  35. http_fields/host.py +44 -0
  36. http_fields/ifmatch.py +24 -0
  37. http_fields/ifmodifiedsince.py +17 -0
  38. http_fields/ifnonematch.py +24 -0
  39. http_fields/ifrange.py +49 -0
  40. http_fields/ifunmodifiedsince.py +17 -0
  41. http_fields/lastmodified.py +17 -0
  42. http_fields/link.py +35 -0
  43. http_fields/location.py +24 -0
  44. http_fields/maxforwards.py +32 -0
  45. http_fields/origin.py +42 -0
  46. http_fields/parsedobjs.py +62 -0
  47. http_fields/prefer.py +64 -0
  48. http_fields/priority.py +55 -0
  49. http_fields/productlistheader.py +33 -0
  50. http_fields/proxyauthenticate.py +17 -0
  51. http_fields/proxyauthenticationinfo.py +17 -0
  52. http_fields/proxyauthorization.py +17 -0
  53. http_fields/proxystatus.py +12 -0
  54. http_fields/py.typed +0 -0
  55. http_fields/range.py +37 -0
  56. http_fields/referer.py +24 -0
  57. http_fields/retryafter.py +45 -0
  58. http_fields/server.py +18 -0
  59. http_fields/setcookie.py +419 -0
  60. http_fields/stricttransportsecurity.py +96 -0
  61. http_fields/structuredfields.py +356 -0
  62. http_fields/structuredheaders.py +90 -0
  63. http_fields/te.py +36 -0
  64. http_fields/trailer.py +44 -0
  65. http_fields/upgrade.py +35 -0
  66. http_fields/uriheader.py +36 -0
  67. http_fields/useragent.py +18 -0
  68. http_fields/vary.py +40 -0
  69. http_fields/via.py +35 -0
  70. http_fields/visitors/__init__.py +1 -0
  71. http_fields/visitors/rfc3986.py +195 -0
  72. http_fields/visitors/rfc6265.py +62 -0
  73. http_fields/visitors/rfc6266.py +234 -0
  74. http_fields/visitors/rfc7230.py +80 -0
  75. http_fields/visitors/rfc7239.py +43 -0
  76. http_fields/visitors/rfc7240.py +77 -0
  77. http_fields/visitors/rfc7838.py +77 -0
  78. http_fields/visitors/rfc8288.py +56 -0
  79. http_fields/visitors/rfc9110/__init__.py +117 -0
  80. http_fields/visitors/rfc9110/_base.py +71 -0
  81. http_fields/visitors/rfc9110/accept.py +135 -0
  82. http_fields/visitors/rfc9110/acceptcharset.py +26 -0
  83. http_fields/visitors/rfc9110/acceptencoding.py +76 -0
  84. http_fields/visitors/rfc9110/acceptlanguage.py +55 -0
  85. http_fields/visitors/rfc9110/acceptranges.py +13 -0
  86. http_fields/visitors/rfc9110/allow.py +13 -0
  87. http_fields/visitors/rfc9110/authenticationinfo.py +19 -0
  88. http_fields/visitors/rfc9110/authorization.py +77 -0
  89. http_fields/visitors/rfc9110/authparam.py +40 -0
  90. http_fields/visitors/rfc9110/authscheme.py +17 -0
  91. http_fields/visitors/rfc9110/challenge.py +57 -0
  92. http_fields/visitors/rfc9110/comment.py +99 -0
  93. http_fields/visitors/rfc9110/connection.py +13 -0
  94. http_fields/visitors/rfc9110/contentlanguage.py +21 -0
  95. http_fields/visitors/rfc9110/contentlength.py +9 -0
  96. http_fields/visitors/rfc9110/contentrange.py +70 -0
  97. http_fields/visitors/rfc9110/contenttype.py +52 -0
  98. http_fields/visitors/rfc9110/date.py +12 -0
  99. http_fields/visitors/rfc9110/entitytag.py +66 -0
  100. http_fields/visitors/rfc9110/etag.py +12 -0
  101. http_fields/visitors/rfc9110/field.py +15 -0
  102. http_fields/visitors/rfc9110/host.py +32 -0
  103. http_fields/visitors/rfc9110/httpdate.py +167 -0
  104. http_fields/visitors/rfc9110/ifmatch.py +24 -0
  105. http_fields/visitors/rfc9110/ifmodifiedsince.py +12 -0
  106. http_fields/visitors/rfc9110/ifnonematch.py +26 -0
  107. http_fields/visitors/rfc9110/ifunmodifiedsince.py +12 -0
  108. http_fields/visitors/rfc9110/lastmodified.py +14 -0
  109. http_fields/visitors/rfc9110/location.py +9 -0
  110. http_fields/visitors/rfc9110/parameters.py +124 -0
  111. http_fields/visitors/rfc9110/product.py +39 -0
  112. http_fields/visitors/rfc9110/quotedstring.py +66 -0
  113. http_fields/visitors/rfc9110/range.py +100 -0
  114. http_fields/visitors/rfc9110/rangeunit.py +17 -0
  115. http_fields/visitors/rfc9110/retryafter.py +20 -0
  116. http_fields/visitors/rfc9110/server.py +16 -0
  117. http_fields/visitors/rfc9110/te.py +83 -0
  118. http_fields/visitors/rfc9110/token.py +18 -0
  119. http_fields/visitors/rfc9110/token68.py +18 -0
  120. http_fields/visitors/rfc9110/type.py +17 -0
  121. http_fields/visitors/rfc9110/upgrade.py +57 -0
  122. http_fields/visitors/rfc9110/useragent.py +16 -0
  123. http_fields/visitors/rfc9110/vary.py +10 -0
  124. http_fields/visitors/rfc9110/via.py +86 -0
  125. http_fields/visitors/rfc9110/weight.py +40 -0
  126. http_fields/visitors/rfc9110/wwwauthenticate.py +21 -0
  127. http_fields/visitors/rfc9111.py +92 -0
  128. http_fields/wwwauthenticate.py +24 -0
  129. http_fields-0.1.1.dist-info/METADATA +88 -0
  130. http_fields-0.1.1.dist-info/RECORD +132 -0
  131. http_fields-0.1.1.dist-info/WHEEL +4 -0
  132. http_fields-0.1.1.dist-info/licenses/LICENSE.txt +16 -0
@@ -0,0 +1,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)
@@ -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
@@ -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)