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,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,9 @@
1
+ from abnf import Node, NodeVisitor
2
+
3
+ __all__ = ["LocationVisitor"]
4
+
5
+
6
+ class LocationVisitor(NodeVisitor):
7
+ def visit_location(self, node: Node):
8
+ # in the future, this might be replaced with a more detailed uri object.
9
+ return node.value
@@ -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)))