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/altsvc.py ADDED
@@ -0,0 +1,70 @@
1
+ """Alt-Svc and Alt-Used header classes."""
2
+
3
+ from dataclasses import dataclass
4
+ from typing import ClassVar
5
+
6
+ from abnf import Rule
7
+ from abnf.grammars import rfc7838
8
+ from typing_extensions import Self
9
+
10
+ from http_fields.header import Header
11
+ from http_fields.parsedobjs import ParsedStr
12
+ from http_fields.visitors.rfc7838 import AltSvcVisitor, AltValue
13
+
14
+
15
+ class AltUsedAuthority(ParsedStr):
16
+ """An Alt-Used authority (the alternative ``host[:port]`` actually used). Self-validating."""
17
+
18
+ parser = rfc7838.Rule("Alt-Used")
19
+
20
+
21
+ @dataclass(frozen=True)
22
+ class AltSvc(Header):
23
+ """Alt-Svc header, as defined by RFC 7838. The special value ``clear`` is represented by
24
+ ``clear=True`` with no ``values``."""
25
+
26
+ name: ClassVar[str] = "Alt-Svc"
27
+ rule: ClassVar[Rule] = rfc7838.Rule("Alt-Svc")
28
+ visitor: ClassVar[AltSvcVisitor] = AltSvcVisitor()
29
+
30
+ values: tuple[AltValue, ...] = ()
31
+ clear: bool = False
32
+
33
+ def __init__(self, *values: AltValue, clear: bool = False) -> None:
34
+ # Each AltValue self-validates (ProtocolId/AltAuthority/Param), so the field-type
35
+ # check fully guarantees a safe serialized value -- no re-parse.
36
+ object.__setattr__(self, "values", tuple(values))
37
+ object.__setattr__(self, "clear", clear)
38
+
39
+ @classmethod
40
+ def parse(cls, value: str) -> Self:
41
+ if value.strip().lower() == "clear":
42
+ cls._node(value) # validate
43
+ return cls(clear=True)
44
+ return cls(*cls.visitor.visit(cls._node(value)))
45
+
46
+ @property
47
+ def value(self) -> str:
48
+ return "clear" if self.clear else ", ".join(str(v) for v in self.values)
49
+
50
+
51
+ @dataclass(frozen=True)
52
+ class AltUsed(Header):
53
+ """Alt-Used header, as defined by RFC 7838: the alternative authority actually used."""
54
+
55
+ name: ClassVar[str] = "Alt-Used"
56
+ rule: ClassVar[Rule] = rfc7838.Rule("Alt-Used")
57
+
58
+ authority: AltUsedAuthority
59
+
60
+ def __init__(self, authority: str) -> None:
61
+ # The authority self-validates as a leaf; build from a string via AltUsed(...).
62
+ object.__setattr__(self, "authority", AltUsedAuthority(authority))
63
+
64
+ @classmethod
65
+ def parse(cls, value: str) -> Self:
66
+ return cls(value)
67
+
68
+ @property
69
+ def value(self) -> str:
70
+ return self.authority
@@ -0,0 +1,79 @@
1
+ """Base classes for the authentication / authorization header families.
2
+
3
+ These pair a request/response header with its proxy counterpart:
4
+ ``ChallengeListHeader`` (WWW-Authenticate / Proxy-Authenticate), ``CredentialsHeader``
5
+ (Authorization / Proxy-Authorization), and ``AuthParamsHeader`` (Authentication-Info /
6
+ Proxy-Authentication-Info). Concrete subclasses supply ``name``/``rule``/``visitor``.
7
+ """
8
+
9
+ from dataclasses import dataclass
10
+ from typing import ClassVar
11
+
12
+ from abnf import NodeVisitor
13
+ from typing_extensions import Self
14
+
15
+ from http_fields.header import Header
16
+ from http_fields.visitors.rfc9110 import (
17
+ AuthParam,
18
+ AuthParamChallenge,
19
+ AuthParamCredentials,
20
+ TokenChallenge,
21
+ TokenCredentials,
22
+ )
23
+
24
+
25
+ @dataclass(frozen=True)
26
+ class ChallengeListHeader(Header):
27
+ """Base for headers that are a list of authentication challenges."""
28
+
29
+ visitor: ClassVar[NodeVisitor]
30
+
31
+ challenges: tuple[TokenChallenge | AuthParamChallenge, ...]
32
+
33
+ def __init__(self, *challenges: TokenChallenge | AuthParamChallenge) -> None:
34
+ object.__setattr__(self, "challenges", tuple(challenges))
35
+
36
+ @classmethod
37
+ def parse(cls, value: str) -> Self:
38
+ return cls(*cls.visitor.visit(cls._node(value)))
39
+
40
+ @property
41
+ def value(self) -> str:
42
+ return ", ".join(str(challenge) for challenge in self.challenges)
43
+
44
+
45
+ @dataclass(frozen=True)
46
+ class CredentialsHeader(Header):
47
+ """Base for headers carrying a single credentials value."""
48
+
49
+ visitor: ClassVar[NodeVisitor]
50
+
51
+ credentials: TokenCredentials | AuthParamCredentials
52
+
53
+ @classmethod
54
+ def parse(cls, value: str) -> Self:
55
+ return cls(cls.visitor.visit(cls._node(value)))
56
+
57
+ @property
58
+ def value(self) -> str:
59
+ return str(self.credentials)
60
+
61
+
62
+ @dataclass(frozen=True)
63
+ class AuthParamsHeader(Header):
64
+ """Base for headers that are a list of auth-params."""
65
+
66
+ visitor: ClassVar[NodeVisitor]
67
+
68
+ auth_params: tuple[AuthParam, ...] = ()
69
+
70
+ def __init__(self, *auth_params: AuthParam) -> None:
71
+ object.__setattr__(self, "auth_params", tuple(auth_params))
72
+
73
+ @classmethod
74
+ def parse(cls, value: str) -> Self:
75
+ return cls(*cls.visitor.visit(cls._node(value)))
76
+
77
+ @property
78
+ def value(self) -> str:
79
+ return ",".join(str(p) for p in self.auth_params)
@@ -0,0 +1,19 @@
1
+ """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 AuthenticationInfoVisitor, AuthParam
10
+
11
+ __all__ = ["AuthenticationInfo", "AuthParam"]
12
+
13
+
14
+ class AuthenticationInfo(AuthParamsHeader):
15
+ """Authentication-Info header, as defined by RFC 9110."""
16
+
17
+ name: ClassVar[str] = "authentication-info"
18
+ rule: ClassVar[Rule] = rfc9110.Rule("Authentication-Info")
19
+ visitor = AuthenticationInfoVisitor()
@@ -0,0 +1,23 @@
1
+ """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 (
10
+ AuthorizationVisitor,
11
+ AuthParamCredentials,
12
+ TokenCredentials,
13
+ )
14
+
15
+ __all__ = ["AuthParamCredentials", "Authorization", "TokenCredentials"]
16
+
17
+
18
+ class Authorization(CredentialsHeader):
19
+ """Authorization header, as defined by RFC 9110."""
20
+
21
+ name: ClassVar[str] = "Authorization"
22
+ rule: ClassVar[Rule] = rfc9110.Rule("Authorization")
23
+ visitor = AuthorizationVisitor()
@@ -0,0 +1,119 @@
1
+ """Cache-Control header class."""
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Any, ClassVar
5
+
6
+ from abnf import Rule
7
+ from abnf.grammars import rfc9111
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.rfc9111 import CacheControlVisitor, CacheDirective
13
+
14
+ # directive-name -> field-name maps, shared by parse() and value.
15
+ _BOOL = {
16
+ "immutable": "immutable",
17
+ "public": "public",
18
+ "no-store": "no_store",
19
+ "no-transform": "no_transform",
20
+ "must-revalidate": "must_revalidate",
21
+ "proxy-revalidate": "proxy_revalidate",
22
+ "only-if-cached": "only_if_cached",
23
+ }
24
+ _INT = {
25
+ "max-age": "max_age",
26
+ "max-stale": "max_stale",
27
+ "min-fresh": "min_fresh",
28
+ "s-maxage": "s_maxage",
29
+ }
30
+ _BOOL_OR_STR = {"no-cache": "no_cache", "private": "private"}
31
+
32
+
33
+ @dataclass(frozen=True, kw_only=True)
34
+ class CacheControl(Header):
35
+ """Cache-Control header, as defined by RFC 9111.
36
+
37
+ The ``no_cache`` and ``private`` directives may be ``True`` or a string of field names.
38
+ Non-standard directives are carried in ``cache_extension``.
39
+ """
40
+
41
+ name: ClassVar[str] = "Cache-Control"
42
+ rule: ClassVar[Rule] = rfc9111.Rule("Cache-Control")
43
+ visitor: ClassVar[CacheControlVisitor] = CacheControlVisitor()
44
+
45
+ immutable: bool = False
46
+ public: bool = False
47
+ private: bool | str = False
48
+ no_cache: bool | str = False
49
+ no_store: bool = False
50
+ no_transform: bool = False
51
+ must_revalidate: bool = False
52
+ proxy_revalidate: bool = False
53
+ max_age: int | None = None
54
+ # max_stale is None when absent, True when valueless ("stale of any age",
55
+ # RFC 9111 section 5.2.1.2), or an int delta-seconds bound.
56
+ max_stale: int | bool | None = None
57
+ min_fresh: int | None = None
58
+ only_if_cached: bool = False
59
+ s_maxage: int | None = None
60
+ cache_extension: tuple[CacheDirective, ...] = ()
61
+
62
+ def __post_init__(self) -> None:
63
+ # fail-fast coercion of the int-valued directives (frozen -> setattr).
64
+ for attr in ("max_age", "min_fresh", "s_maxage"):
65
+ v = getattr(self, attr)
66
+ if v is not None:
67
+ object.__setattr__(self, attr, NonNegativeInt(v))
68
+ # max-stale may be valueless (True); only coerce a delta-seconds bound.
69
+ if self.max_stale is not None and self.max_stale is not True:
70
+ object.__setattr__(self, "max_stale", NonNegativeInt(self.max_stale))
71
+ # Enforce the declared field types (e.g. no-cache/private must be bool | str, not a
72
+ # list that CacheDirective would silently swallow as a valueless directive).
73
+ super().__post_init__()
74
+ # Serializing builds a self-validating CacheDirective for each field (name -> Token,
75
+ # no-cache/private string -> QuotedString, ints -> NonNegativeInt), rejecting invalid
76
+ # content, and caches the result; a valid serialization is grammar-valid by
77
+ # construction, so no re-parse is needed.
78
+ _ = self.value
79
+
80
+ @classmethod
81
+ def parse(cls, value: str) -> Self:
82
+ kw: dict[str, Any] = {}
83
+ extension: list[CacheDirective] = []
84
+ for directive in cls.visitor.visit(cls._node(value)):
85
+ n = str(directive.name)
86
+ if n in _BOOL:
87
+ kw[_BOOL[n]] = True
88
+ elif n in _INT:
89
+ kw[_INT[n]] = directive.value
90
+ elif n in _BOOL_OR_STR:
91
+ kw[_BOOL_OR_STR[n]] = (
92
+ directive.value if isinstance(directive.value, str) else True
93
+ )
94
+ else:
95
+ extension.append(directive)
96
+ return cls(**kw, cache_extension=tuple(extension))
97
+
98
+ @property
99
+ def value(self) -> str:
100
+ out: list[CacheDirective] = []
101
+ for n, attr in _BOOL.items():
102
+ if getattr(self, attr):
103
+ out.append(CacheDirective(n))
104
+ for n, attr in _INT.items():
105
+ v = getattr(self, attr)
106
+ if v is None:
107
+ continue
108
+ if attr == "max_stale" and v is True: # valueless max-stale
109
+ out.append(CacheDirective(n))
110
+ else: # keep 0, unlike a truthiness check
111
+ out.append(CacheDirective(n, v))
112
+ for n, attr in _BOOL_OR_STR.items():
113
+ v = getattr(self, attr)
114
+ if isinstance(v, str):
115
+ out.append(CacheDirective(n, v))
116
+ elif v:
117
+ out.append(CacheDirective(n))
118
+ out.extend(self.cache_extension)
119
+ return ",".join(str(directive) for directive in out)
@@ -0,0 +1,12 @@
1
+ """Cache-Status header class."""
2
+
3
+ from typing import ClassVar
4
+
5
+ from http_fields.structuredheaders import StructuredListHeader
6
+
7
+
8
+ class CacheStatus(StructuredListHeader):
9
+ """Cache-Status header, as defined by RFC 9211. Each ``members`` item identifies a cache
10
+ (its bare value) and carries parameters such as ``hit``, ``fwd``, ``ttl``, and ``detail``."""
11
+
12
+ name: ClassVar[str] = "cache-status"
@@ -0,0 +1,35 @@
1
+ """Connection 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 ConnectionVisitor, Token
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class Connection(Header):
16
+ """Connection header, as defined by RFC 9110."""
17
+
18
+ name: ClassVar[str] = "connection"
19
+ rule: ClassVar[Rule] = rfc9110.Rule("Connection")
20
+ visitor: ClassVar[ConnectionVisitor] = ConnectionVisitor()
21
+
22
+ directives: tuple[Token, ...]
23
+
24
+ def __init__(self, *directives: Token) -> None:
25
+ object.__setattr__(self, "directives", tuple(directives))
26
+
27
+ @classmethod
28
+ def parse(cls, value: str) -> Self:
29
+ return cls(
30
+ *(Token(d, parse=False) for d in cls.visitor.visit(cls._node(value)))
31
+ )
32
+
33
+ @property
34
+ def value(self) -> str:
35
+ return ",".join(self.directives)
@@ -0,0 +1,19 @@
1
+ """Content-Digest and Repr-Digest header classes."""
2
+
3
+ from typing import ClassVar
4
+
5
+ from http_fields.structuredheaders import DigestHeader
6
+
7
+
8
+ class ContentDigest(DigestHeader):
9
+ """Content-Digest header, as defined by RFC 9530: a Dictionary of algorithm name to the
10
+ byte-sequence digest of the message content."""
11
+
12
+ name: ClassVar[str] = "content-digest"
13
+
14
+
15
+ class ReprDigest(DigestHeader):
16
+ """Repr-Digest header, as defined by RFC 9530: like Content-Digest, but over the selected
17
+ representation."""
18
+
19
+ name: ClassVar[str] = "repr-digest"
@@ -0,0 +1,66 @@
1
+ """Content-Disposition 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 rfc6266
8
+ from typing_extensions import Self
9
+
10
+ from http_fields.header import Header
11
+ from http_fields.visitors.rfc6266 import (
12
+ ContentDispositionNodeVisitor,
13
+ DispExtParm,
14
+ DispositionType,
15
+ ExtValue,
16
+ FilenameParm,
17
+ )
18
+
19
+
20
+ @dataclass(frozen=True)
21
+ class ContentDisposition(Header):
22
+ """Content-Disposition header, as defined by RFC 6266."""
23
+
24
+ name: ClassVar[str] = "Content-Disposition"
25
+ rule: ClassVar[Rule] = rfc6266.Rule("content-disposition")
26
+ visitor: ClassVar[ContentDispositionNodeVisitor] = ContentDispositionNodeVisitor()
27
+
28
+ disposition_type: DispositionType
29
+ disposition_parms: tuple[FilenameParm | DispExtParm, ...] = ()
30
+
31
+ @classmethod
32
+ def build(
33
+ cls,
34
+ disposition_type: str,
35
+ disposition_parms: dict[str, str | ExtValue] | None = None,
36
+ ) -> Self:
37
+ """Build a Content-Disposition from a disposition type and a parameter mapping.
38
+
39
+ A parameter named ``filename`` or ``filename*`` (case-insensitive) becomes a
40
+ FilenameParm; anything else becomes a DispExtParm.
41
+ """
42
+ parms = tuple(
43
+ FilenameParm(pname, pvalue)
44
+ if pname.lower() in {"filename", "filename*"}
45
+ else DispExtParm(pname, pvalue)
46
+ for pname, pvalue in (disposition_parms or {}).items()
47
+ )
48
+ return cls(DispositionType(disposition_type), parms)
49
+
50
+ @classmethod
51
+ def parse(cls, value: str) -> Self:
52
+ # unlike most header rules, 'content-disposition' matches the whole header line, so
53
+ # we prepend the field name before parsing.
54
+ cls._check_length(value)
55
+ try:
56
+ node = cls.rule.parse_all(f"content-disposition: {value}")
57
+ except ParseError as exc:
58
+ raise ValueError(f'Invalid {cls.__name__} value "{value}".') from exc
59
+ disposition_type, parms = cls.visitor.visit(node)
60
+ return cls(disposition_type, tuple(parms))
61
+
62
+ @property
63
+ def value(self) -> str:
64
+ return str(self.disposition_type) + "".join(
65
+ f";{parm}" for parm in self.disposition_parms
66
+ )
@@ -0,0 +1,45 @@
1
+ """ContentEncoding 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 abnf.parser import Node, NodeVisitor
9
+ from typing_extensions import Self
10
+
11
+ from http_fields.header import Header
12
+ from http_fields.visitors.rfc9110 import Token
13
+
14
+
15
+ class ContentEncodingVisitor(NodeVisitor):
16
+ def visit_content_encoding(self, node: Node) -> list[str]:
17
+ return list(filter(None, map(self.visit, node.children)))
18
+
19
+ @staticmethod
20
+ def visit_content_coding(node: Node) -> str:
21
+ return node.value
22
+
23
+
24
+ @dataclass(frozen=True)
25
+ class ContentEncoding(Header):
26
+ """Content-Encoding header, as defined by RFC 9110."""
27
+
28
+ name: ClassVar[str] = "content-encoding"
29
+ rule: ClassVar[Rule] = rfc9110.Rule("Content-Encoding")
30
+ visitor: ClassVar[ContentEncodingVisitor] = ContentEncodingVisitor()
31
+
32
+ codings: tuple[Token, ...]
33
+
34
+ def __init__(self, *codings: Token) -> None:
35
+ object.__setattr__(self, "codings", tuple(codings))
36
+
37
+ @classmethod
38
+ def parse(cls, value: str) -> Self:
39
+ return cls(
40
+ *(Token(c, parse=False) for c in cls.visitor.visit(cls._node(value)))
41
+ )
42
+
43
+ @property
44
+ def value(self) -> str:
45
+ return ", ".join(self.codings)
@@ -0,0 +1,38 @@
1
+ """Content-Language 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.contentlanguage import (
12
+ ContentLanguageVisitor,
13
+ LanguageTag,
14
+ )
15
+
16
+
17
+ @dataclass(frozen=True)
18
+ class ContentLanguage(Header):
19
+ """Content-Language header, as defined by RFC 9110."""
20
+
21
+ name: ClassVar[str] = "content-language"
22
+ rule: ClassVar[Rule] = rfc9110.Rule("Content-Language")
23
+ visitor: ClassVar[ContentLanguageVisitor] = ContentLanguageVisitor()
24
+
25
+ languages: tuple[LanguageTag, ...]
26
+
27
+ def __init__(self, *languages: LanguageTag) -> None:
28
+ object.__setattr__(self, "languages", tuple(languages))
29
+
30
+ @classmethod
31
+ def parse(cls, value: str) -> Self:
32
+ return cls(
33
+ *(LanguageTag(x, parse=False) for x in cls.visitor.visit(cls._node(value)))
34
+ )
35
+
36
+ @property
37
+ def value(self) -> str:
38
+ return ", ".join(self.languages)
@@ -0,0 +1,34 @@
1
+ """ContentLength 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 ContentLengthVisitor
13
+
14
+
15
+ @dataclass(frozen=True)
16
+ class ContentLength(Header):
17
+ """Content-Length header."""
18
+
19
+ name: ClassVar[str] = "content-length"
20
+ rule: ClassVar[Rule] = rfc9110.Rule("Content-Length")
21
+ visitor: ClassVar[ContentLengthVisitor] = ContentLengthVisitor()
22
+
23
+ length: NonNegativeInt
24
+
25
+ def __init__(self, length: int) -> None:
26
+ object.__setattr__(self, "length", NonNegativeInt(length))
27
+
28
+ @classmethod
29
+ def parse(cls, value: str) -> Self:
30
+ return cls(cls.visitor.visit(cls._node(value)))
31
+
32
+ @property
33
+ def value(self) -> str:
34
+ return str(self.length)
@@ -0,0 +1,24 @@
1
+ """Content-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 ContentLocationUri(ParsedStr):
13
+ """A Content-Location URI reference (RFC 9110). Self-validating."""
14
+
15
+ parser = rfc9110.Rule("Content-Location")
16
+
17
+
18
+ class ContentLocation(UriHeader):
19
+ """Content-Location header, as defined by RFC 9110."""
20
+
21
+ name: ClassVar[str] = "content-location"
22
+ rule: ClassVar[Rule] = rfc9110.Rule("Content-Location")
23
+ uri_type: ClassVar[type[ParsedStr]] = ContentLocationUri
24
+ uri: ContentLocationUri
@@ -0,0 +1,74 @@
1
+ """Content-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.parsedobjs import NonNegativeInt
12
+ from http_fields.visitors.rfc9110 import ContentRangeVisitor, RangeUnit
13
+ from http_fields.visitors.rfc9110.contentrange import RangeResp
14
+
15
+
16
+ @dataclass(frozen=True)
17
+ class ContentRange(Header):
18
+ """Content-Range header, as defined by RFC 9110."""
19
+
20
+ name: ClassVar[str] = "content-range"
21
+ rule: ClassVar[Rule] = rfc9110.Rule("Content-Range")
22
+ visitor: ClassVar[ContentRangeVisitor] = ContentRangeVisitor()
23
+
24
+ range_unit: RangeUnit
25
+ first_pos: NonNegativeInt | None
26
+ last_pos: NonNegativeInt | None
27
+ complete_length: NonNegativeInt | None
28
+
29
+ def __init__(
30
+ self,
31
+ range_unit: str,
32
+ *,
33
+ first_pos: int | None = None,
34
+ last_pos: int | None = None,
35
+ complete_length: int | None = None,
36
+ ) -> None:
37
+ if (first_pos is None) != (last_pos is None):
38
+ raise ValueError(
39
+ "Both first_pos and last_pos must be given, or both omitted."
40
+ )
41
+ if first_pos is None and complete_length is None:
42
+ raise ValueError(
43
+ "complete_length is required when first_pos and last_pos are omitted."
44
+ )
45
+ object.__setattr__(self, "range_unit", RangeUnit(range_unit))
46
+ object.__setattr__(
47
+ self, "first_pos", None if first_pos is None else NonNegativeInt(first_pos)
48
+ )
49
+ object.__setattr__(
50
+ self, "last_pos", None if last_pos is None else NonNegativeInt(last_pos)
51
+ )
52
+ object.__setattr__(
53
+ self,
54
+ "complete_length",
55
+ None if complete_length is None else NonNegativeInt(complete_length),
56
+ )
57
+
58
+ @classmethod
59
+ def parse(cls, value: str) -> Self:
60
+ range_unit, rng = cls.visitor.visit(cls._node(value))
61
+ satisfied = isinstance(rng, RangeResp)
62
+ return cls(
63
+ range_unit,
64
+ first_pos=rng.first_pos if satisfied else None,
65
+ last_pos=rng.last_pos if satisfied else None,
66
+ complete_length=rng.complete_length,
67
+ )
68
+
69
+ @property
70
+ def value(self) -> str:
71
+ if self.first_pos is not None and self.last_pos is not None:
72
+ length = self.complete_length if self.complete_length is not None else "*"
73
+ return f"{self.range_unit} {self.first_pos}-{self.last_pos}/{length}"
74
+ return f"{self.range_unit} */{self.complete_length}"