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
http_fields/host.py ADDED
@@ -0,0 +1,44 @@
1
+ """Host 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.parsedobjs import NonNegativeInt
12
+ from http_fields.visitors.rfc9110 import Hostname, HostVisitor
13
+
14
+
15
+ @dataclass(frozen=True)
16
+ class Host(Header):
17
+ """Host header, as defined by RFC 9110.
18
+
19
+ Host: www.example.com
20
+ """
21
+
22
+ name: ClassVar[str] = "host"
23
+ rule: ClassVar[Rule] = rfc9110.Rule("Host")
24
+ visitor: ClassVar[HostVisitor] = HostVisitor()
25
+
26
+ hostname: Hostname
27
+ port: NonNegativeInt | None = None
28
+
29
+ def __init__(self, hostname: str, port: int | None = None) -> None:
30
+ # hostname/port self-validate as leaves (an already-parsed value passes through),
31
+ # so a valid uri-host and numeric port serialize to a valid Host -- no re-parse.
32
+ object.__setattr__(self, "hostname", Hostname(hostname))
33
+ object.__setattr__(
34
+ self, "port", NonNegativeInt(port) if port is not None else None
35
+ )
36
+
37
+ @classmethod
38
+ def parse(cls, value: str) -> Self:
39
+ hostname, port = cls.visitor.visit(cls._node(value))
40
+ return cls(hostname, port)
41
+
42
+ @property
43
+ def value(self) -> str:
44
+ return self.hostname + (f":{self.port}" if self.port is not None else "")
http_fields/ifmatch.py ADDED
@@ -0,0 +1,24 @@
1
+ """If-Match 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.entitytaglist import EntityTagListHeader
9
+ from http_fields.visitors.rfc9110 import EntityTag, IfMatchVisitor
10
+
11
+
12
+ class IfMatch(EntityTagListHeader):
13
+ """If-Match header, as defined by RFC 9110. ``wildcard`` (``*``) matches any current
14
+ representation."""
15
+
16
+ name: ClassVar[str] = "If-Match"
17
+ rule: ClassVar[Rule] = rfc9110.Rule("If-Match")
18
+ visitor = IfMatchVisitor()
19
+
20
+ def matches(self, entity_tag: EntityTag) -> bool:
21
+ """Return True if ``entity_tag`` strongly matches, or this header is ``*``."""
22
+ if self.wildcard:
23
+ return True
24
+ return any(entity_tag.compare(tag, weak=False) for tag in self.entity_tags)
@@ -0,0 +1,17 @@
1
+ """IfModifiedSince 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.dateheader import DateHeader
9
+ from http_fields.visitors.rfc9110 import IfModifiedSinceVisitor
10
+
11
+
12
+ class IfModifiedSince(DateHeader):
13
+ """If-Modified-Since header, as defined by RFC 9110."""
14
+
15
+ name: ClassVar[str] = "If-Modified-Since"
16
+ rule: ClassVar[Rule] = rfc9110.Rule("If-Modified-Since")
17
+ visitor = IfModifiedSinceVisitor()
@@ -0,0 +1,24 @@
1
+ """If-None-Match 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.entitytaglist import EntityTagListHeader
9
+ from http_fields.visitors.rfc9110 import EntityTag, IfNoneMatchVisitor
10
+
11
+
12
+ class IfNoneMatch(EntityTagListHeader):
13
+ """If-None-Match header, as defined by RFC 9110. ``wildcard`` (``*``) matches if any
14
+ current representation exists."""
15
+
16
+ name: ClassVar[str] = "If-None-Match"
17
+ rule: ClassVar[Rule] = rfc9110.Rule("If-None-Match")
18
+ visitor = IfNoneMatchVisitor()
19
+
20
+ def matches(self, entity_tag: EntityTag) -> bool:
21
+ """Return True if ``entity_tag`` weakly matches none of the tags; False if ``*``."""
22
+ if self.wildcard:
23
+ return False
24
+ return not any(entity_tag.compare(tag, weak=True) for tag in self.entity_tags)
http_fields/ifrange.py ADDED
@@ -0,0 +1,49 @@
1
+ """If-Range header class."""
2
+
3
+ from dataclasses import dataclass
4
+ from datetime import datetime
5
+ from typing import ClassVar
6
+
7
+ from abnf import Node, NodeVisitor, Rule
8
+ from abnf.grammars import rfc9110
9
+ from typing_extensions import Self
10
+
11
+ from http_fields.header import Header
12
+ from http_fields.visitors.rfc9110 import EntityTag, HttpDateVisitor, imf_fixdate
13
+ from http_fields.visitors.rfc9110.entitytag import EntityTagVisitor
14
+
15
+
16
+ class IfRangeVisitor(NodeVisitor):
17
+ visit_entity_tag = EntityTagVisitor()
18
+ visit_http_date = HttpDateVisitor()
19
+
20
+ def visit_if_range(self, node: Node) -> EntityTag | datetime:
21
+ return next(filter(None, map(self.visit, node.children)))
22
+
23
+
24
+ @dataclass(frozen=True)
25
+ class IfRange(Header):
26
+ """If-Range header, as defined by RFC 9110. The condition is an entity-tag or an
27
+ HTTP-date."""
28
+
29
+ name: ClassVar[str] = "If-Range"
30
+ rule: ClassVar[Rule] = rfc9110.Rule("If-Range")
31
+ visitor: ClassVar[IfRangeVisitor] = IfRangeVisitor()
32
+
33
+ condition: EntityTag | datetime
34
+
35
+ def __init__(self, condition: EntityTag | datetime) -> None:
36
+ # Runtime guard for callers that ignore the type annotation.
37
+ if not isinstance(condition, (EntityTag, datetime)): # pyright: ignore[reportUnnecessaryIsInstance]
38
+ raise TypeError("condition must be an EntityTag or datetime.")
39
+ object.__setattr__(self, "condition", condition)
40
+
41
+ @classmethod
42
+ def parse(cls, value: str) -> Self:
43
+ return cls(cls.visitor.visit(cls._node(value)))
44
+
45
+ @property
46
+ def value(self) -> str:
47
+ if isinstance(self.condition, datetime):
48
+ return imf_fixdate(self.condition)
49
+ return str(self.condition)
@@ -0,0 +1,17 @@
1
+ """IfUnmodifiedSince 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.dateheader import DateHeader
9
+ from http_fields.visitors.rfc9110 import IfUnmodifiedSinceVisitor
10
+
11
+
12
+ class IfUnmodifiedSince(DateHeader):
13
+ """If-Unmodified-Since header, as defined by RFC 9110."""
14
+
15
+ name: ClassVar[str] = "If-Unmodified-Since"
16
+ rule: ClassVar[Rule] = rfc9110.Rule("If-Unmodified-Since")
17
+ visitor = IfUnmodifiedSinceVisitor()
@@ -0,0 +1,17 @@
1
+ """LastModified 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.dateheader import DateHeader
9
+ from http_fields.visitors.rfc9110 import LastModifiedVisitor
10
+
11
+
12
+ class LastModified(DateHeader):
13
+ """Last-Modified header, as defined by RFC 9110."""
14
+
15
+ name: ClassVar[str] = "last-modified"
16
+ rule: ClassVar[Rule] = rfc9110.Rule("Last-Modified")
17
+ visitor = LastModifiedVisitor()
http_fields/link.py ADDED
@@ -0,0 +1,35 @@
1
+ """Link header class."""
2
+
3
+ from dataclasses import dataclass
4
+ from typing import ClassVar
5
+
6
+ from abnf import Rule
7
+ from abnf.grammars import rfc8288
8
+ from typing_extensions import Self
9
+
10
+ from http_fields.header import Header
11
+ from http_fields.visitors.rfc8288 import LinkValue, LinkVisitor
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class Link(Header):
16
+ """Link header, as defined by RFC 8288 (Web Linking)."""
17
+
18
+ name: ClassVar[str] = "Link"
19
+ rule: ClassVar[Rule] = rfc8288.Rule("Link")
20
+ visitor: ClassVar[LinkVisitor] = LinkVisitor()
21
+
22
+ links: tuple[LinkValue, ...]
23
+
24
+ def __init__(self, *links: LinkValue) -> None:
25
+ # Each LinkValue self-validates (URIReference target, Param params), so the
26
+ # field-type check fully guarantees a safe serialized value -- no re-parse.
27
+ object.__setattr__(self, "links", tuple(links))
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(link) for link in self.links)
@@ -0,0 +1,24 @@
1
+ """Location 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.parsedobjs import ParsedStr
9
+ from http_fields.uriheader import UriHeader
10
+
11
+
12
+ class LocationUri(ParsedStr):
13
+ """A Location URI reference (RFC 9110). Self-validating."""
14
+
15
+ parser = rfc9110.Rule("Location")
16
+
17
+
18
+ class Location(UriHeader):
19
+ """Location header, as defined by RFC 9110."""
20
+
21
+ name: ClassVar[str] = "Location"
22
+ rule: ClassVar[Rule] = rfc9110.Rule("Location")
23
+ uri_type: ClassVar[type[ParsedStr]] = LocationUri
24
+ uri: LocationUri
@@ -0,0 +1,32 @@
1
+ """Max-Forwards 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.parsedobjs import NonNegativeInt
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class MaxForwards(Header):
16
+ """Max-Forwards header, as defined by RFC 9110."""
17
+
18
+ name: ClassVar[str] = "Max-Forwards"
19
+ rule: ClassVar[Rule] = rfc9110.Rule("Max-Forwards")
20
+
21
+ forwards: NonNegativeInt
22
+
23
+ def __init__(self, forwards: int) -> None:
24
+ object.__setattr__(self, "forwards", NonNegativeInt(forwards))
25
+
26
+ @classmethod
27
+ def parse(cls, value: str) -> Self:
28
+ return cls(NonNegativeInt(cls._node(value).value))
29
+
30
+ @property
31
+ def value(self) -> str:
32
+ return str(self.forwards)
http_fields/origin.py ADDED
@@ -0,0 +1,42 @@
1
+ """Origin header class."""
2
+
3
+ from dataclasses import dataclass
4
+ from typing import ClassVar
5
+
6
+ from abnf import Rule
7
+ from abnf.grammars import cors
8
+ from typing_extensions import Self
9
+
10
+ from http_fields.header import Header
11
+ from http_fields.parsedobjs import ParsedStr
12
+
13
+
14
+ class OriginValue(ParsedStr):
15
+ """An Origin value (``scheme://host[:port]`` or ``null``). Self-validating."""
16
+
17
+ parser = cors.Rule("Origin")
18
+
19
+
20
+ @dataclass(frozen=True)
21
+ class Origin(Header):
22
+ """Origin header, as defined by RFC 6454 / the Fetch standard.
23
+
24
+ The origin (``scheme://host[:port]`` or ``null``) is validated on construction.
25
+ """
26
+
27
+ name: ClassVar[str] = "origin"
28
+ rule: ClassVar[Rule] = cors.Rule("Origin")
29
+
30
+ origin: OriginValue
31
+
32
+ def __init__(self, origin: str) -> None:
33
+ # The origin self-validates as a leaf; build from a string via Origin(...).
34
+ object.__setattr__(self, "origin", OriginValue(origin))
35
+
36
+ @classmethod
37
+ def parse(cls, value: str) -> Self:
38
+ return cls(value)
39
+
40
+ @property
41
+ def value(self) -> str:
42
+ return self.origin
@@ -0,0 +1,62 @@
1
+ """A few classes that parse or otherwise check values."""
2
+
3
+ from typing import Any
4
+
5
+ from abnf import ParseError, Rule
6
+
7
+
8
+ class ParsedStr(str):
9
+ """str subclass that supports parsing at object creation. Subclasses of ParsedStr
10
+ supply an abnf Rule used to parse the passed string."""
11
+
12
+ parser: Rule
13
+
14
+ def __new__(cls, s: Any, parse: bool = True):
15
+ if isinstance(s, cls):
16
+ return s
17
+ else:
18
+ # if s can't be converted to str, this should fail.
19
+ _s = str(s)
20
+ try:
21
+ return super().__new__(
22
+ cls, cls.parser.parse_all(_s).value if parse else _s
23
+ )
24
+ except ParseError as exc:
25
+ raise ValueError(f'Invalid {cls.parser.name} "{s}".') from exc
26
+
27
+ def __repr__(self):
28
+ return f"{self.__class__.__name__}({str.__repr__(str(self))})"
29
+
30
+
31
+ class CaselessMixin:
32
+ """Used to define case-insensitive matching for str subclasses."""
33
+
34
+ def __eq__(self, __o: object) -> bool:
35
+ # comparison should be case-insensitive.
36
+ return (
37
+ self.casefold() == __o.casefold()
38
+ if isinstance(self, str) and isinstance(__o, str)
39
+ else NotImplemented
40
+ )
41
+
42
+ def __hash__(self) -> int:
43
+ # and because we redefined __eq__, we need to redefine __hash__ for consistency.
44
+ assert isinstance(self, str)
45
+ return hash(self.casefold())
46
+
47
+
48
+ class NonNegativeInt(int):
49
+ """Non-negative int type."""
50
+
51
+ def __new__(cls, value: Any):
52
+ if isinstance(value, cls):
53
+ return value
54
+ else:
55
+ if isinstance(value, float) and not value.is_integer():
56
+ # int(3.7) would silently truncate; reject non-integral input.
57
+ raise ValueError("Value must be a whole number.")
58
+ val = super().__new__(cls, value)
59
+ if val >= 0:
60
+ return val
61
+ else:
62
+ raise ValueError("Value must be non-negative.")
http_fields/prefer.py ADDED
@@ -0,0 +1,64 @@
1
+ """Prefer and Preference-Applied header classes."""
2
+
3
+ from dataclasses import dataclass
4
+ from typing import ClassVar
5
+
6
+ from abnf import Rule
7
+ from abnf.grammars import rfc7240
8
+ from typing_extensions import Self
9
+
10
+ from http_fields.header import Header
11
+ from http_fields.visitors.rfc7240 import (
12
+ Preference,
13
+ PreferenceAppliedVisitor,
14
+ PreferVisitor,
15
+ )
16
+
17
+
18
+ @dataclass(frozen=True)
19
+ class Prefer(Header):
20
+ """Prefer header, as defined by RFC 7240."""
21
+
22
+ name: ClassVar[str] = "Prefer"
23
+ rule: ClassVar[Rule] = rfc7240.Rule("Prefer")
24
+ visitor: ClassVar[PreferVisitor] = PreferVisitor()
25
+
26
+ preferences: tuple[Preference, ...]
27
+
28
+ def __init__(self, *preferences: Preference) -> None:
29
+ # Each Preference self-validates (Token name, leaf value, Param parameters), so
30
+ # the field-type check fully guarantees a safe serialized value -- no re-parse.
31
+ object.__setattr__(self, "preferences", tuple(preferences))
32
+
33
+ @classmethod
34
+ def parse(cls, value: str) -> Self:
35
+ # the grammar rule matches the whole header line, so prepend the field name.
36
+ return cls(*cls.visitor.visit(cls._prefixed_node(value)))
37
+
38
+ @property
39
+ def value(self) -> str:
40
+ return ", ".join(str(pref) for pref in self.preferences)
41
+
42
+
43
+ @dataclass(frozen=True)
44
+ class PreferenceApplied(Header):
45
+ """Preference-Applied header, as defined by RFC 7240."""
46
+
47
+ name: ClassVar[str] = "Preference-Applied"
48
+ rule: ClassVar[Rule] = rfc7240.Rule("Preference-Applied")
49
+ visitor: ClassVar[PreferenceAppliedVisitor] = PreferenceAppliedVisitor()
50
+
51
+ preferences: tuple[Preference, ...]
52
+
53
+ def __init__(self, *preferences: Preference) -> None:
54
+ # Each Preference self-validates (Token name, leaf value, Param parameters), so
55
+ # the field-type check fully guarantees a safe serialized value -- no re-parse.
56
+ object.__setattr__(self, "preferences", tuple(preferences))
57
+
58
+ @classmethod
59
+ def parse(cls, value: str) -> Self:
60
+ return cls(*cls.visitor.visit(cls._prefixed_node(value)))
61
+
62
+ @property
63
+ def value(self) -> str:
64
+ return ", ".join(str(pref) for pref in self.preferences)
@@ -0,0 +1,55 @@
1
+ """Priority header class."""
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 Item, parse_dictionary, serialize_dictionary
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class Priority(Header):
16
+ """Priority header, as defined by RFC 9218: a Structured Fields Dictionary with an
17
+ ``urgency`` (0-7, default 3) and an ``incremental`` flag (default False). Default values
18
+ are omitted from the serialized value."""
19
+
20
+ name: ClassVar[str] = "priority"
21
+ rule: ClassVar[Rule] = rfc9651.Rule("sf-dictionary")
22
+
23
+ urgency: int = 3
24
+ incremental: bool = False
25
+
26
+ @classmethod
27
+ def parse(cls, value: str) -> Self:
28
+ cls._check_length(value)
29
+ try:
30
+ members = dict(parse_dictionary(value))
31
+ except ParseError as exc:
32
+ raise ValueError(f'Invalid {cls.__name__} value "{value}".') from exc
33
+ urgency = 3
34
+ incremental = False
35
+ u = members.get("u")
36
+ if (
37
+ isinstance(u, Item)
38
+ and isinstance(u.value, int)
39
+ and not isinstance(u.value, bool)
40
+ and 0 <= u.value <= 7
41
+ ):
42
+ urgency = u.value
43
+ i = members.get("i")
44
+ if isinstance(i, Item) and isinstance(i.value, bool):
45
+ incremental = i.value
46
+ return cls(urgency, incremental)
47
+
48
+ @property
49
+ def value(self) -> str:
50
+ members: list[tuple[str, Item]] = []
51
+ if self.urgency != 3:
52
+ members.append(("u", Item(self.urgency)))
53
+ if self.incremental:
54
+ members.append(("i", Item(True)))
55
+ return serialize_dictionary(tuple(members))
@@ -0,0 +1,33 @@
1
+ """Base class for headers that are a list of products and comments."""
2
+
3
+ from dataclasses import dataclass
4
+ from typing import ClassVar
5
+
6
+ from abnf import NodeVisitor
7
+ from typing_extensions import Self
8
+
9
+ from http_fields.header import Header
10
+ from http_fields.visitors.rfc9110 import Comment, Product
11
+
12
+
13
+ @dataclass(frozen=True)
14
+ class ProductListHeader(Header):
15
+ """Base for headers that are a whitespace-separated list of Product/Comment items
16
+ (User-Agent, Server). Concrete subclasses supply ``name``/``rule``/``visitor``."""
17
+
18
+ visitor: ClassVar[NodeVisitor]
19
+
20
+ items: tuple[Product | Comment, ...]
21
+
22
+ def __init__(self, *items: Product | Comment) -> None:
23
+ object.__setattr__(self, "items", tuple(items))
24
+
25
+ @classmethod
26
+ def parse(cls, value: str) -> Self:
27
+ return cls(*cls.visitor.visit(cls._node(value)))
28
+
29
+ @property
30
+ def value(self) -> str:
31
+ # the grammar requires at least one SP/TAB between items, so a source with multiple
32
+ # spaces between items is not reassembled identically.
33
+ return " ".join(str(item) for item in self.items)
@@ -0,0 +1,17 @@
1
+ """Proxy-Authenticate 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.authbases import ChallengeListHeader
9
+ from http_fields.visitors.rfc9110 import ProxyAuthenticateVisitor
10
+
11
+
12
+ class ProxyAuthenticate(ChallengeListHeader):
13
+ """Proxy-Authenticate header, as defined by RFC 9110."""
14
+
15
+ name: ClassVar[str] = "proxy-authenticate"
16
+ rule: ClassVar[Rule] = rfc9110.Rule("Proxy-Authenticate")
17
+ visitor = ProxyAuthenticateVisitor()
@@ -0,0 +1,17 @@
1
+ """Proxy-Authentication-Info 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.authbases import AuthParamsHeader
9
+ from http_fields.visitors.rfc9110 import ProxyAuthenticationInfoVisitor
10
+
11
+
12
+ class ProxyAuthenticationInfo(AuthParamsHeader):
13
+ """Proxy-Authentication-Info header, as defined by RFC 9110."""
14
+
15
+ name: ClassVar[str] = "proxy-authentication-info"
16
+ rule: ClassVar[Rule] = rfc9110.Rule("Proxy-Authentication-Info")
17
+ visitor = ProxyAuthenticationInfoVisitor()
@@ -0,0 +1,17 @@
1
+ """Proxy-Authorization 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.authbases import CredentialsHeader
9
+ from http_fields.visitors.rfc9110 import ProxyAuthorizationVisitor
10
+
11
+
12
+ class ProxyAuthorization(CredentialsHeader):
13
+ """Proxy-Authorization header, as defined by RFC 9110."""
14
+
15
+ name: ClassVar[str] = "proxy-authorization"
16
+ rule: ClassVar[Rule] = rfc9110.Rule("Proxy-Authorization")
17
+ visitor = ProxyAuthorizationVisitor()
@@ -0,0 +1,12 @@
1
+ """Proxy-Status header class."""
2
+
3
+ from typing import ClassVar
4
+
5
+ from http_fields.structuredheaders import StructuredListHeader
6
+
7
+
8
+ class ProxyStatus(StructuredListHeader):
9
+ """Proxy-Status header, as defined by RFC 9209. Each ``members`` item identifies an
10
+ intermediary (its bare value) and carries parameters such as ``error`` and ``next-hop``."""
11
+
12
+ name: ClassVar[str] = "proxy-status"
http_fields/py.typed ADDED
File without changes
http_fields/range.py ADDED
@@ -0,0 +1,37 @@
1
+ """Range 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 import RangeUnit
12
+ from http_fields.visitors.rfc9110.range import IntRange, RangeVisitor, SuffixRange
13
+
14
+
15
+ @dataclass(frozen=True)
16
+ class Range(Header):
17
+ """Range header, as defined by RFC 9110, e.g. ``bytes=0-499,-500``."""
18
+
19
+ name: ClassVar[str] = "Range"
20
+ rule: ClassVar[Rule] = rfc9110.Rule("Range")
21
+ visitor: ClassVar[RangeVisitor] = RangeVisitor()
22
+
23
+ range_unit: RangeUnit
24
+ ranges: tuple[IntRange | SuffixRange, ...]
25
+
26
+ def __init__(self, range_unit: str, *ranges: IntRange | SuffixRange) -> None:
27
+ object.__setattr__(self, "range_unit", RangeUnit(range_unit))
28
+ object.__setattr__(self, "ranges", tuple(ranges))
29
+
30
+ @classmethod
31
+ def parse(cls, value: str) -> Self:
32
+ range_unit, ranges = cls.visitor.visit(cls._node(value))
33
+ return cls(range_unit, *ranges)
34
+
35
+ @property
36
+ def value(self) -> str:
37
+ return f"{self.range_unit}={','.join(str(r) for r in self.ranges)}"