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,104 @@
1
+ """Content-Type 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 ContentTypeVisitor, MediaType, Parameter
12
+ from http_fields.visitors.rfc9110.quotedstring import QuotedString
13
+
14
+
15
+ def _unquote(value: str) -> str:
16
+ """Return the logical value of a parameter, stripping the surrounding DQUOTEs
17
+ and unescaping quoted-pairs when it is a quoted-string."""
18
+ if not isinstance(value, QuotedString):
19
+ return str(value)
20
+ inner = str(value)[1:-1]
21
+ out: list[str] = []
22
+ i = 0
23
+ while i < len(inner):
24
+ if inner[i] == "\\" and i + 1 < len(inner):
25
+ out.append(inner[i + 1])
26
+ i += 2
27
+ else:
28
+ out.append(inner[i])
29
+ i += 1
30
+ return "".join(out)
31
+
32
+
33
+ @dataclass(frozen=True)
34
+ class ContentType(Header):
35
+ """Content-Type header, as defined by RFC 9110."""
36
+
37
+ name: ClassVar[str] = "Content-Type"
38
+ rule: ClassVar[Rule] = rfc9110.Rule("Content-Type")
39
+ visitor: ClassVar[ContentTypeVisitor] = ContentTypeVisitor()
40
+
41
+ media_type: MediaType
42
+
43
+ @classmethod
44
+ def parse(cls, value: str) -> Self:
45
+ return cls(cls.visitor.visit(cls._node(value)))
46
+
47
+ @classmethod
48
+ def of(
49
+ cls,
50
+ type: str,
51
+ subtype: str,
52
+ *,
53
+ charset: str | None = None,
54
+ boundary: str | None = None,
55
+ params: list[tuple[str, str]] | None = None,
56
+ ) -> Self:
57
+ """Build a Content-Type from its pieces. ``charset``/``boundary`` take precedence
58
+ over any same-named entry in ``params``."""
59
+ all_params: list[Parameter] = [
60
+ p
61
+ for p in (
62
+ Parameter("charset", charset) if charset else None,
63
+ Parameter("boundary", boundary) if boundary else None,
64
+ )
65
+ if p is not None
66
+ ]
67
+ for name, val in params or []:
68
+ param = Parameter(name, val)
69
+ if (param.name == "charset" and charset) or (
70
+ param.name == "boundary" and boundary
71
+ ):
72
+ continue
73
+ all_params.append(param)
74
+ return cls(MediaType(type, subtype, all_params))
75
+
76
+ @property
77
+ def type(self) -> str:
78
+ return self.media_type.type
79
+
80
+ @property
81
+ def subtype(self) -> str:
82
+ return self.media_type.subtype
83
+
84
+ @property
85
+ def params(self) -> list[Parameter]:
86
+ return list(self.media_type.params)
87
+
88
+ @property
89
+ def charset(self) -> str | None:
90
+ for param in self.media_type.params:
91
+ if param.name == "charset":
92
+ return _unquote(param.value)
93
+ return None
94
+
95
+ @property
96
+ def boundary(self) -> str | None:
97
+ for param in self.media_type.params:
98
+ if param.name == "boundary":
99
+ return _unquote(param.value)
100
+ return None
101
+
102
+ @property
103
+ def value(self) -> str:
104
+ return str(self.media_type)
http_fields/cookie.py ADDED
@@ -0,0 +1,38 @@
1
+ """Cookie header class."""
2
+
3
+ from dataclasses import dataclass
4
+ from typing import ClassVar
5
+
6
+ from abnf import Rule
7
+ from abnf.grammars import rfc6265
8
+ from typing_extensions import Self
9
+
10
+ from http_fields.header import Header
11
+ from http_fields.visitors.rfc6265 import CookiePair, CookieStringVisitor
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class Cookie(Header):
16
+ """Cookie header, as defined by RFC 6265."""
17
+
18
+ name: ClassVar[str] = "Cookie"
19
+ rule: ClassVar[Rule] = rfc6265.Rule("cookie-string")
20
+ visitor: ClassVar[CookieStringVisitor] = CookieStringVisitor()
21
+
22
+ pairs: tuple[CookiePair, ...]
23
+
24
+ def __init__(self, *pairs: CookiePair | tuple[str, str]) -> None:
25
+ object.__setattr__(
26
+ self,
27
+ "pairs",
28
+ tuple(p if isinstance(p, CookiePair) else CookiePair(*p) for p in pairs),
29
+ )
30
+
31
+ @classmethod
32
+ def parse(cls, value: str) -> Self:
33
+ # cookies are often sent with an invalid trailing semicolon; strip it before parsing.
34
+ return cls(*cls.visitor.visit(cls._node(value.rstrip(";"))))
35
+
36
+ @property
37
+ def value(self) -> str:
38
+ return "; ".join(f"{pair.name}={pair.value}" for pair in self.pairs)
http_fields/date.py ADDED
@@ -0,0 +1,17 @@
1
+ """Date 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 DateVisitor
10
+
11
+
12
+ class Date(DateHeader):
13
+ """Date header, as defined by RFC 9110."""
14
+
15
+ name: ClassVar[str] = "date"
16
+ rule: ClassVar[Rule] = rfc9110.Rule("Date")
17
+ visitor = DateVisitor()
@@ -0,0 +1,32 @@
1
+ """Base class for headers whose value is a single HTTP-date."""
2
+
3
+ from dataclasses import dataclass
4
+ from datetime import datetime
5
+ from typing import ClassVar
6
+
7
+ from abnf import NodeVisitor
8
+ from typing_extensions import Self
9
+
10
+ from http_fields.header import Header
11
+ from http_fields.visitors.rfc9110 import imf_fixdate
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class DateHeader(Header):
16
+ """Base for headers whose value is a single HTTP-date (RFC 9110 section 5.6.7).
17
+
18
+ Concrete subclasses supply ``name``, ``rule``, and ``visitor`` as ClassVars; the visitor
19
+ must return a :class:`datetime.datetime`.
20
+ """
21
+
22
+ visitor: ClassVar[NodeVisitor]
23
+
24
+ date: datetime
25
+
26
+ @classmethod
27
+ def parse(cls, value: str) -> Self:
28
+ return cls(cls.visitor.visit(cls._node(value)))
29
+
30
+ @property
31
+ def value(self) -> str:
32
+ return imf_fixdate(self.date)
@@ -0,0 +1,39 @@
1
+ """Base class for headers that are a list of entity-tags or ``*``."""
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 EntityTag
11
+
12
+
13
+ @dataclass(frozen=True)
14
+ class EntityTagListHeader(Header):
15
+ """Base for If-Match / If-None-Match: a list of entity-tags, or ``*`` (``wildcard``).
16
+
17
+ Concrete subclasses supply ``name``/``rule``/``visitor`` (the visitor returns either the
18
+ string ``"*"`` or a list of :class:`EntityTag`) and their own ``matches()`` semantics.
19
+ """
20
+
21
+ visitor: ClassVar[NodeVisitor]
22
+
23
+ entity_tags: tuple[EntityTag, ...]
24
+ wildcard: bool
25
+
26
+ def __init__(self, *entity_tags: EntityTag, wildcard: bool = False) -> None:
27
+ object.__setattr__(self, "entity_tags", tuple(entity_tags))
28
+ object.__setattr__(self, "wildcard", wildcard)
29
+
30
+ @classmethod
31
+ def parse(cls, value: str) -> Self:
32
+ result = cls.visitor.visit(cls._node(value))
33
+ if result == "*":
34
+ return cls(wildcard=True)
35
+ return cls(*result)
36
+
37
+ @property
38
+ def value(self) -> str:
39
+ return "*" if self.wildcard else ", ".join(str(t) for t in self.entity_tags)
http_fields/etag.py ADDED
@@ -0,0 +1,46 @@
1
+ """ETag 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 EntityTag, ETagVisitor
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class ETag(Header):
16
+ """ETag header, as defined by RFC 9110."""
17
+
18
+ name: ClassVar[str] = "ETag"
19
+ rule: ClassVar[Rule] = rfc9110.Rule("ETag")
20
+ visitor: ClassVar[ETagVisitor] = ETagVisitor()
21
+
22
+ entity_tag: EntityTag
23
+
24
+ @classmethod
25
+ def parse(cls, value: str) -> Self:
26
+ return cls(cls.visitor.visit(cls._node(value)))
27
+
28
+ @classmethod
29
+ def from_tag(cls, tag: str, weak: bool = False) -> Self:
30
+ """Build an ETag from an opaque tag and optional weak flag."""
31
+ return cls(EntityTag(tag, weak=weak))
32
+
33
+ @property
34
+ def value(self) -> str:
35
+ return str(self.entity_tag)
36
+
37
+ def matches(self, entity_tag: EntityTag | None, weak: bool = False) -> bool:
38
+ """Return True if this ETag matches ``entity_tag`` per RFC 9110 comparison.
39
+
40
+ Comparison against None is supported and returns False.
41
+ """
42
+ return (
43
+ self.entity_tag.compare(entity_tag, weak=weak)
44
+ if isinstance(entity_tag, EntityTag)
45
+ else False
46
+ )
http_fields/expect.py ADDED
@@ -0,0 +1,52 @@
1
+ """Expect 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.parsedobjs import ParsedStr
12
+
13
+
14
+ class Expectation(ParsedStr):
15
+ """An RFC 9110 expectation (e.g. ``100-continue``). Self-validating."""
16
+
17
+ parser = rfc9110.Rule("expectation")
18
+
19
+
20
+ class ExpectVisitor(NodeVisitor):
21
+ @staticmethod
22
+ def visit_expectation(node: Node) -> Expectation:
23
+ # The node is already parsed; skip re-validation.
24
+ return Expectation(node.value, parse=False)
25
+
26
+ def visit_expect(self, node: Node) -> list[Expectation]:
27
+ return [x for x in map(self.visit, node.children) if x is not None]
28
+
29
+
30
+ @dataclass(frozen=True)
31
+ class Expect(Header):
32
+ """Expect header, as defined by RFC 9110 (in practice almost always ``100-continue``)."""
33
+
34
+ name: ClassVar[str] = "Expect"
35
+ rule: ClassVar[Rule] = rfc9110.Rule("Expect")
36
+ visitor: ClassVar[ExpectVisitor] = ExpectVisitor()
37
+
38
+ expectations: tuple[Expectation, ...]
39
+
40
+ def __init__(self, *expectations: Expectation) -> None:
41
+ # Store already-parsed Expectation values (each validated itself on creation).
42
+ # The base runtime check enforces the field type; build from a string with
43
+ # Expect.parse().
44
+ object.__setattr__(self, "expectations", tuple(expectations))
45
+
46
+ @classmethod
47
+ def parse(cls, value: str) -> Self:
48
+ return cls(*cls.visitor.visit(cls._node(value)))
49
+
50
+ @property
51
+ def value(self) -> str:
52
+ return ", ".join(self.expectations)
http_fields/expires.py ADDED
@@ -0,0 +1,17 @@
1
+ """Expires header class."""
2
+
3
+ from typing import ClassVar
4
+
5
+ from abnf import Rule
6
+ from abnf.grammars import rfc9111
7
+
8
+ from http_fields.dateheader import DateHeader
9
+ from http_fields.visitors.rfc9111 import ExpiresVisitor
10
+
11
+
12
+ class Expires(DateHeader):
13
+ """Expires header, as defined by RFC 9111."""
14
+
15
+ name: ClassVar[str] = "expires"
16
+ rule: ClassVar[Rule] = rfc9111.Rule("Expires")
17
+ visitor = ExpiresVisitor()
@@ -0,0 +1,36 @@
1
+ """Forwarded header class."""
2
+
3
+ from dataclasses import dataclass
4
+ from typing import ClassVar
5
+
6
+ from abnf import Rule
7
+ from abnf.grammars import rfc7239
8
+ from typing_extensions import Self
9
+
10
+ from http_fields.header import Header
11
+ from http_fields.visitors.rfc7239 import ForwardedElement, ForwardedVisitor
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class Forwarded(Header):
16
+ """Forwarded header, as defined by RFC 7239 (the standardized replacement for the de-facto
17
+ ``X-Forwarded-*`` headers)."""
18
+
19
+ name: ClassVar[str] = "Forwarded"
20
+ rule: ClassVar[Rule] = rfc7239.Rule("Forwarded")
21
+ visitor: ClassVar[ForwardedVisitor] = ForwardedVisitor()
22
+
23
+ elements: tuple[ForwardedElement, ...]
24
+
25
+ def __init__(self, *elements: ForwardedElement) -> None:
26
+ # Each ForwardedElement self-validates (Param pairs), so the field-type check
27
+ # fully guarantees a safe serialized value -- no re-parse.
28
+ object.__setattr__(self, "elements", tuple(elements))
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(element) for element in self.elements)
@@ -0,0 +1,42 @@
1
+ """From 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 ParsedStr
12
+
13
+
14
+ class Mailbox(ParsedStr):
15
+ """A From-header mailbox (RFC 9110 / RFC 5322 addr-spec). Self-validating."""
16
+
17
+ parser = rfc9110.Rule("From")
18
+
19
+
20
+ @dataclass(frozen=True)
21
+ class From(Header):
22
+ """From header, as defined by RFC 9110: the mailbox of the user controlling the client.
23
+
24
+ The mailbox is validated against RFC 9110 (RFC 5322 addr-spec) on construction.
25
+ """
26
+
27
+ name: ClassVar[str] = "From"
28
+ rule: ClassVar[Rule] = rfc9110.Rule("From")
29
+
30
+ mailbox: Mailbox
31
+
32
+ def __init__(self, mailbox: str) -> None:
33
+ # The mailbox self-validates as a leaf; build from a string via From(...).
34
+ object.__setattr__(self, "mailbox", Mailbox(mailbox))
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.mailbox
http_fields/header.py ADDED
@@ -0,0 +1,250 @@
1
+ """HTTP header abstract base class and the generic custom-header class."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import dataclasses
6
+ import inspect
7
+ import types
8
+ from abc import ABC, abstractmethod
9
+ from collections.abc import Callable, Iterable
10
+ from dataclasses import dataclass
11
+ from functools import cached_property, wraps
12
+ from typing import Any, ClassVar, Union, get_args, get_origin
13
+
14
+ from abnf import Node, ParseError, Rule
15
+ from abnf.grammars import rfc9110
16
+
17
+ from http_fields.visitors.rfc9110 import FieldName, FieldValue
18
+
19
+
20
+ def _matches_type(value: object, hint: object) -> bool:
21
+ """A small, deterministic runtime type check covering the field-annotation shapes
22
+ headers actually use: a concrete type, ``tuple[X, ...]``/``tuple[X, Y]``, unions
23
+ (``X | Y``, ``Optional``), and ``None``. Every element is checked (no sampling).
24
+ Unrecognised shapes are not blocked."""
25
+ origin = get_origin(hint)
26
+ if origin is tuple:
27
+ args = get_args(hint)
28
+ if not isinstance(value, tuple):
29
+ return False
30
+ if len(args) == 2 and args[1] is Ellipsis: # tuple[X, ...]
31
+ return all(_matches_type(v, args[0]) for v in value)
32
+ return len(value) == len(args) and all(
33
+ _matches_type(v, a) for v, a in zip(value, args, strict=True)
34
+ )
35
+ if origin is Union or origin is types.UnionType:
36
+ return any(_matches_type(value, a) for a in get_args(hint))
37
+ if hint is type(None):
38
+ return value is None
39
+ if isinstance(hint, type):
40
+ return isinstance(value, hint)
41
+ return True # unrecognised annotation shape: don't block
42
+
43
+
44
+ class Header(ABC):
45
+ """Abstract base for HTTP headers.
46
+
47
+ Known headers are frozen dataclasses whose fields are the structured components of the
48
+ header value. Each sets ``name`` (and usually ``rule``/``visitor``) as a ClassVar,
49
+ implements a ``parse(value)`` classmethod, and exposes a derived ``value`` property.
50
+ Arbitrary headers (e.g. ``X-Request-Id``) are represented by :class:`CustomHeader`.
51
+ """
52
+
53
+ encoding: ClassVar[str] = "ISO-8859-1"
54
+ rule: ClassVar[Rule] # abnf rule for the header value; provided by subclasses
55
+ name: ClassVar[str] # field name; a class constant on known headers,
56
+ # overridden by a per-instance property on CustomHeader.
57
+ # Maximum accepted length of a value passed to parse(). Parsing is super-linear
58
+ # in the length for some grammars, so an oversized value is a CPU-exhaustion
59
+ # vector; reject it up front. Override per subclass where larger values are
60
+ # legitimate (e.g. cookie-heavy deployments).
61
+ max_length: ClassVar[int] = 8192
62
+ # Per-class cache of resolved field-annotation hints (populated on first use).
63
+ _field_hints_cache: ClassVar[dict[str, object] | None] = None
64
+
65
+ def __init_subclass__(cls, **kwargs: object) -> None:
66
+ super().__init_subclass__(**kwargs)
67
+ # Headers (and every value-object they hold) are immutable, so the serialized
68
+ # value is stable: cache it per instance with functools.cached_property, so it
69
+ # is computed at most once (str/bytes/asgi_value/__eq__/__hash__ and direct
70
+ # access all share it). We swap the descriptor in here, at class creation,
71
+ # rather than decorating each subclass's `value` -- a cached_property directly
72
+ # overriding the abstract `value` property trips reportIncompatibleMethodOverride,
73
+ # so subclasses keep a plain @property. cached_property writes to the instance
74
+ # __dict__, bypassing the frozen __setattr__. Only the class that *defines*
75
+ # value is wrapped; subclasses inherit the cached descriptor.
76
+ prop = cls.__dict__.get("value")
77
+ if isinstance(prop, property) and prop.fget is not None:
78
+ cached = cached_property(prop.fget)
79
+ cached.__set_name__(cls, "value")
80
+ cls.value = cached # type: ignore[assignment]
81
+
82
+ # Wrap a custom __init__ so field types are checked after construction. The
83
+ # type checker still reads the source __init__ signature for argument
84
+ # checking; this only adds the runtime guard. (A dataclass-generated __init__
85
+ # isn't present yet at __init_subclass__ time and is not wrapped.)
86
+ user_init = cls.__dict__.get("__init__")
87
+ if callable(user_init):
88
+
89
+ @wraps(user_init)
90
+ def checked_init(
91
+ self: Header,
92
+ *args: object,
93
+ _init: Callable[..., object] = user_init,
94
+ **kwargs: object,
95
+ ) -> None:
96
+ _init(self, *args, **kwargs)
97
+ self._check_field_types()
98
+
99
+ cls.__init__ = checked_init # type: ignore[assignment]
100
+
101
+ @property
102
+ @abstractmethod
103
+ def value(self) -> str:
104
+ """The serialized header field value (without the name)."""
105
+
106
+ def __str__(self) -> str:
107
+ """Return the full ``Name: value`` header field."""
108
+ return f"{self.name}: {self.value}"
109
+
110
+ def __bytes__(self) -> bytes:
111
+ return str(self).encode(self.encoding)
112
+
113
+ @property
114
+ def asgi_value(self) -> tuple[bytes, bytes]:
115
+ """Return a ``(name, value)`` pair of latin-1 bytes for an ASGI send dict.
116
+
117
+ ASGI encodes header values as ISO-8859-1 (latin-1); using the class
118
+ encoding keeps this consistent with ``__bytes__`` and accepts obs-text.
119
+ """
120
+ return (self.name.encode(self.encoding), self.value.encode(self.encoding))
121
+
122
+ def __eq__(self, other: object, /) -> bool:
123
+ if isinstance(other, Header) and type(other) is type(self):
124
+ return (self.name, self.value) == (other.name, other.value)
125
+ return NotImplemented
126
+
127
+ def __hash__(self) -> int:
128
+ return hash((self.name, self.value))
129
+
130
+ @classmethod
131
+ def _field_hints(cls) -> dict[str, object]:
132
+ """Resolved ``{field_name: type_hint}`` for this header's dataclass fields,
133
+ cached per class. Annotations are resolved per class across the MRO (so an
134
+ unresolvable annotation on one class doesn't disable the check for others)."""
135
+ cached = cls.__dict__.get("_field_hints_cache")
136
+ if cached is None:
137
+ merged: dict[str, Any] = {}
138
+ for klass in reversed(cls.__mro__): # base -> derived; derived wins
139
+ try:
140
+ merged.update(inspect.get_annotations(klass, eval_str=True))
141
+ except Exception:
142
+ continue
143
+ cached = {
144
+ f.name: merged[f.name]
145
+ for f in dataclasses.fields(cls) # type: ignore[arg-type]
146
+ if f.name in merged
147
+ }
148
+ cls._field_hints_cache = cached
149
+ return cached
150
+
151
+ def __post_init__(self) -> None:
152
+ # Field-type check for headers with a dataclass-generated __init__ (which
153
+ # calls __post_init__). Custom __init__s are wrapped in __init_subclass__
154
+ # instead. A subclass overriding __post_init__ should call super() to keep
155
+ # this guard (or it must validate its fields another way).
156
+ self._check_field_types()
157
+
158
+ def _check_field_types(self) -> None:
159
+ """Runtime enforcement of the construction contract: every field must match
160
+ its declared type. Because each field type validates itself on creation, a
161
+ type check is a complete guarantee -- no grammar re-parse. Raises TypeError
162
+ (pointing to :meth:`parse`) on a mismatch."""
163
+ for name, hint in type(self)._field_hints().items():
164
+ value = getattr(self, name)
165
+ if not _matches_type(value, hint):
166
+ cls_name = type(self).__name__
167
+ shown = repr(value)
168
+ if len(shown) > 60:
169
+ shown = shown[:57] + "..."
170
+ raise TypeError(
171
+ f"{cls_name}.{name} expects {hint!r}; got {shown}. Use "
172
+ f"{cls_name}.parse() to build a {cls_name} from a string."
173
+ )
174
+
175
+ @classmethod
176
+ def _check_length(cls, value: str) -> None:
177
+ """Reject an oversized parse() input before the (super-linear) grammar runs."""
178
+ if len(value) > cls.max_length:
179
+ raise ValueError(
180
+ f"{cls.__name__} value of {len(value)} chars exceeds the maximum "
181
+ f"of {cls.max_length}."
182
+ )
183
+
184
+ @classmethod
185
+ def _node(cls, value: str) -> Node:
186
+ """Parse ``value`` against ``cls.rule``, translating a ParseError to a ValueError."""
187
+ cls._check_length(value)
188
+ try:
189
+ return cls.rule.parse_all(value)
190
+ except ParseError as exc:
191
+ raise ValueError(f'Invalid {cls.__name__} value "{value}".') from exc
192
+
193
+ @classmethod
194
+ def _prefixed_node(cls, value: str) -> Node:
195
+ """Like :meth:`_node`, but for grammar rules that match the whole header line (name
196
+ included); prepend ``"<name>: "`` before parsing."""
197
+ cls._check_length(value)
198
+ try:
199
+ return cls.rule.parse_all(f"{cls.name}: {value}")
200
+ except ParseError as exc:
201
+ raise ValueError(f'Invalid {cls.__name__} value "{value}".') from exc
202
+
203
+ @classmethod
204
+ def parse(cls, value: str) -> Header:
205
+ """Parse a header value into an instance. Every concrete known header overrides this
206
+ (CustomHeader carries an arbitrary name and is built via its constructor instead)."""
207
+ raise NotImplementedError
208
+
209
+ @classmethod
210
+ def create(cls, name: str, value: str) -> Header:
211
+ """Return a header for ``(name, value)``: a matching known subclass if one is
212
+ registered for ``name``, otherwise a :class:`CustomHeader`."""
213
+ lname = name.lower()
214
+ for subcls in cls.subclass_tree():
215
+ cls_name = getattr(subcls, "name", None)
216
+ if isinstance(cls_name, str) and cls_name.lower() == lname:
217
+ return subcls.parse(value)
218
+ return CustomHeader(name, value)
219
+
220
+ @classmethod
221
+ def subclass_tree(cls) -> Iterable[type[Header]]:
222
+ """Yield all Header subclasses, depth-first."""
223
+ for subcls in cls.__subclasses__():
224
+ yield subcls
225
+ yield from subcls.subclass_tree()
226
+
227
+
228
+ @dataclass(frozen=True)
229
+ class CustomHeader(Header):
230
+ """A header with an arbitrary field name, e.g. ``X-Request-Id``.
231
+
232
+ Both name and value are validated against RFC 9110 (``field-name`` / ``field-value``).
233
+ """
234
+
235
+ rule: ClassVar[Rule] = rfc9110.Rule("field-value")
236
+
237
+ field_name: FieldName
238
+ field_value: FieldValue
239
+
240
+ def __init__(self, name: str, value: str) -> None:
241
+ object.__setattr__(self, "field_name", FieldName(name))
242
+ object.__setattr__(self, "field_value", FieldValue(value))
243
+
244
+ @property
245
+ def name(self) -> str: # pyright: ignore[reportIncompatibleVariableOverride]
246
+ return self.field_name
247
+
248
+ @property
249
+ def value(self) -> str:
250
+ return self.field_value