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.
- http_fields/__init__.py +193 -0
- http_fields/accept.py +37 -0
- http_fields/acceptcharset.py +48 -0
- http_fields/acceptencoding.py +42 -0
- http_fields/acceptlanguage.py +47 -0
- http_fields/acceptranges.py +35 -0
- http_fields/accesscontrol.py +197 -0
- http_fields/age.py +34 -0
- http_fields/allow.py +46 -0
- http_fields/altsvc.py +70 -0
- http_fields/authbases.py +79 -0
- http_fields/authenticationinfo.py +19 -0
- http_fields/authorization.py +23 -0
- http_fields/cachecontrol.py +119 -0
- http_fields/cachestatus.py +12 -0
- http_fields/connection.py +35 -0
- http_fields/contentdigest.py +19 -0
- http_fields/contentdisposition.py +66 -0
- http_fields/contentencoding.py +45 -0
- http_fields/contentlanguage.py +38 -0
- http_fields/contentlength.py +34 -0
- http_fields/contentlocation.py +24 -0
- http_fields/contentrange.py +74 -0
- http_fields/contenttype.py +104 -0
- http_fields/cookie.py +38 -0
- http_fields/date.py +17 -0
- http_fields/dateheader.py +32 -0
- http_fields/entitytaglist.py +39 -0
- http_fields/etag.py +46 -0
- http_fields/expect.py +52 -0
- http_fields/expires.py +17 -0
- http_fields/forwarded.py +36 -0
- http_fields/fromheader.py +42 -0
- http_fields/header.py +250 -0
- http_fields/host.py +44 -0
- http_fields/ifmatch.py +24 -0
- http_fields/ifmodifiedsince.py +17 -0
- http_fields/ifnonematch.py +24 -0
- http_fields/ifrange.py +49 -0
- http_fields/ifunmodifiedsince.py +17 -0
- http_fields/lastmodified.py +17 -0
- http_fields/link.py +35 -0
- http_fields/location.py +24 -0
- http_fields/maxforwards.py +32 -0
- http_fields/origin.py +42 -0
- http_fields/parsedobjs.py +62 -0
- http_fields/prefer.py +64 -0
- http_fields/priority.py +55 -0
- http_fields/productlistheader.py +33 -0
- http_fields/proxyauthenticate.py +17 -0
- http_fields/proxyauthenticationinfo.py +17 -0
- http_fields/proxyauthorization.py +17 -0
- http_fields/proxystatus.py +12 -0
- http_fields/py.typed +0 -0
- http_fields/range.py +37 -0
- http_fields/referer.py +24 -0
- http_fields/retryafter.py +45 -0
- http_fields/server.py +18 -0
- http_fields/setcookie.py +419 -0
- http_fields/stricttransportsecurity.py +96 -0
- http_fields/structuredfields.py +356 -0
- http_fields/structuredheaders.py +90 -0
- http_fields/te.py +36 -0
- http_fields/trailer.py +44 -0
- http_fields/upgrade.py +35 -0
- http_fields/uriheader.py +36 -0
- http_fields/useragent.py +18 -0
- http_fields/vary.py +40 -0
- http_fields/via.py +35 -0
- http_fields/visitors/__init__.py +1 -0
- http_fields/visitors/rfc3986.py +195 -0
- http_fields/visitors/rfc6265.py +62 -0
- http_fields/visitors/rfc6266.py +234 -0
- http_fields/visitors/rfc7230.py +80 -0
- http_fields/visitors/rfc7239.py +43 -0
- http_fields/visitors/rfc7240.py +77 -0
- http_fields/visitors/rfc7838.py +77 -0
- http_fields/visitors/rfc8288.py +56 -0
- http_fields/visitors/rfc9110/__init__.py +117 -0
- http_fields/visitors/rfc9110/_base.py +71 -0
- http_fields/visitors/rfc9110/accept.py +135 -0
- http_fields/visitors/rfc9110/acceptcharset.py +26 -0
- http_fields/visitors/rfc9110/acceptencoding.py +76 -0
- http_fields/visitors/rfc9110/acceptlanguage.py +55 -0
- http_fields/visitors/rfc9110/acceptranges.py +13 -0
- http_fields/visitors/rfc9110/allow.py +13 -0
- http_fields/visitors/rfc9110/authenticationinfo.py +19 -0
- http_fields/visitors/rfc9110/authorization.py +77 -0
- http_fields/visitors/rfc9110/authparam.py +40 -0
- http_fields/visitors/rfc9110/authscheme.py +17 -0
- http_fields/visitors/rfc9110/challenge.py +57 -0
- http_fields/visitors/rfc9110/comment.py +99 -0
- http_fields/visitors/rfc9110/connection.py +13 -0
- http_fields/visitors/rfc9110/contentlanguage.py +21 -0
- http_fields/visitors/rfc9110/contentlength.py +9 -0
- http_fields/visitors/rfc9110/contentrange.py +70 -0
- http_fields/visitors/rfc9110/contenttype.py +52 -0
- http_fields/visitors/rfc9110/date.py +12 -0
- http_fields/visitors/rfc9110/entitytag.py +66 -0
- http_fields/visitors/rfc9110/etag.py +12 -0
- http_fields/visitors/rfc9110/field.py +15 -0
- http_fields/visitors/rfc9110/host.py +32 -0
- http_fields/visitors/rfc9110/httpdate.py +167 -0
- http_fields/visitors/rfc9110/ifmatch.py +24 -0
- http_fields/visitors/rfc9110/ifmodifiedsince.py +12 -0
- http_fields/visitors/rfc9110/ifnonematch.py +26 -0
- http_fields/visitors/rfc9110/ifunmodifiedsince.py +12 -0
- http_fields/visitors/rfc9110/lastmodified.py +14 -0
- http_fields/visitors/rfc9110/location.py +9 -0
- http_fields/visitors/rfc9110/parameters.py +124 -0
- http_fields/visitors/rfc9110/product.py +39 -0
- http_fields/visitors/rfc9110/quotedstring.py +66 -0
- http_fields/visitors/rfc9110/range.py +100 -0
- http_fields/visitors/rfc9110/rangeunit.py +17 -0
- http_fields/visitors/rfc9110/retryafter.py +20 -0
- http_fields/visitors/rfc9110/server.py +16 -0
- http_fields/visitors/rfc9110/te.py +83 -0
- http_fields/visitors/rfc9110/token.py +18 -0
- http_fields/visitors/rfc9110/token68.py +18 -0
- http_fields/visitors/rfc9110/type.py +17 -0
- http_fields/visitors/rfc9110/upgrade.py +57 -0
- http_fields/visitors/rfc9110/useragent.py +16 -0
- http_fields/visitors/rfc9110/vary.py +10 -0
- http_fields/visitors/rfc9110/via.py +86 -0
- http_fields/visitors/rfc9110/weight.py +40 -0
- http_fields/visitors/rfc9110/wwwauthenticate.py +21 -0
- http_fields/visitors/rfc9111.py +92 -0
- http_fields/wwwauthenticate.py +24 -0
- http_fields-0.1.1.dist-info/METADATA +88 -0
- http_fields-0.1.1.dist-info/RECORD +132 -0
- http_fields-0.1.1.dist-info/WHEEL +4 -0
- 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)
|
http_fields/location.py
ADDED
|
@@ -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)
|
http_fields/priority.py
ADDED
|
@@ -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)}"
|