yggdrasil.http_.headers¶
headers ¶
HTTP header container — :class:HTTPHeaders.
A case-insensitive (RFC 7230 §3.2) string→string mapping that preserves each field's original casing for faithful serialization, bumps a version counter on mutation for cache fingerprinting, and carries the canonical-name normalization + anonymization vocabulary. This is the single header type the request / response / session pipeline speaks.
PromotedHeaders
dataclass
¶
PromotedHeaders(
host: str | None = None,
user_agent: str | None = None,
accept: str | None = None,
accept_encoding: str | None = None,
accept_language: str | None = None,
content_type: str | None = None,
content_length: int = 0,
content_encoding: str | None = None,
transfer_encoding: str | None = None,
remaining: dict[str, str] = dict(),
)
Common HTTP headers extracted into dedicated typed fields.
remaining contains all non-promoted headers after normalization.
extract
classmethod
¶
extract(
headers: Mapping[HeaderValue, HeaderValue],
*,
normalize: bool = True,
host: str | None = None
) -> "PromotedHeaders"
Extract common headers into typed attributes.
Matching is case-insensitive. When normalize=True, recognized header
names are canonicalized in remaining.
HTTPHeaders ¶
Bases: MutableMapping[str, str]
Mutable string-string mapping with versioned change tracking.
Acts as a drop-in replacement for the plain dict[str, str]
that :class:Session, :class:PreparedRequest, and
:class:Response were using. Two reasons to specialize it:
- Version counter. Every mutation that actually changes the
contents bumps :attr:
version.(id(headers), version)is a tight cache fingerprint —idcovers wholesale dict swaps,versioncovers in-place mutations theidcan't see (the common case:headers["Authorization"] = new_tokenon the same dict object). - Cached digests. :attr:
byte_lengthand :attr:xxh3_64memoize against the version, so a request that hashes the same headers ten times pays the walk once. Same-value writes (headers["Accept"] = "*/*"when it already is) short-circuit without bumping the version, so re-applying defaults is free.
Keys and values are coerced to str on the way in. The store keeps each
name's original case (for faithful serialization), but lookups are
case-insensitive per RFC 7230 §3.2 — headers.get("Location") finds a
wire location (HTTP/2 lowercases every field name) and a redirect /
Retry-After read works regardless of how the origin cased it. Canonical
Title-Case rewriting still lives in :func:normalize_headers, separate from
this container.
version
property
¶
Monotonically increasing counter — bumped on every mutation
that actually changes :meth:__eq__. Callers can stash
(id(headers), version) and re-check it later for an O(1)
"did anything change?" test.
byte_length
property
¶
Total byte length of all keys + values. Memoized — pays
the walk once per :attr:version and serves an int the rest
of the time.
canonical_bytes
property
¶
Sorted key=value\x00key=value\x00… byte sequence —
the canonical wire form used by digest mixing. Order is
deterministic (sorted by key) so HTTPHeaders({A:1, B:2}) and
HTTPHeaders({B:2, A:1}) produce the same payload. Memoized
against :attr:version so each request pays the encode walk
once across :attr:xxh3_64, :attr:PreparedRequest.hash,
and :attr:Response.hash.
xxh3_64
property
¶
xxh3_64 digest over :attr:canonical_bytes — order-
independent (same pairs → same digest). Returns a signed
int64 to match the rest of the codebase's :func:xxhash
use. Memoized against :attr:version.
from_
classmethod
¶
from_(
arg: "HTTPHeaders | Mapping[Any, Any] | Iterable[tuple[Any, Any]] | None" = None,
) -> "HTTPHeaders"
Coerce arg to :class:HTTPHeaders — passing an existing
instance through unchanged so callers can HTTPHeaders.from_(x)
regardless of what x is.
update ¶
Bulk update — same shape as :meth:dict.update.
We override the mixin so the version bumps once per actual
change rather than once per touched key, and so the no-op
early return matches :meth:__setitem__.
copy ¶
Shallow copy (version reset).
Bypasses __init__'s per-item str() coercion — we already
know the source's keys / values are strings and the rest of the
cache slots start fresh.
to_dict ¶
Snapshot as a plain dict — handy for code that needs
to mutate independently of the live container or hand the
contents to an API that only speaks dict.
canonical_name
staticmethod
¶
Canonical casing for name (e.g. content-type →
Content-Type). Falls back to the caller's stripped form
when the header isn't in the registry.
anonymized ¶
Return a copy with sensitive values dropped/redacted.
Authorization scheme (Bearer, Basic, …) is preserved
in redact mode; long token-shaped values are caught via
:func:_looks_like_token so unrecognized credential headers
still get sanitized. Names are canonicalized on the way out
so repeated normalize calls are idempotent.
Memoized against :attr:version — the canonical anonymized
form is hot on every request's public_hash /
public_url_hash computation, and per response's
public_hash. A version bump on the source headers
invalidates the cache transparently.
normalized ¶
normalized(
*,
is_request: bool,
add_missing: bool = True,
mode: Literal["remove", "redact"] = "remove",
anonymize: bool = False,
body: "Optional[Holder]" = None
) -> "HTTPHeaders"
Return a fresh :class:HTTPHeaders with names canonicalized,
sensitive values optionally sanitized, and (when
add_missing) request defaults / body-derived Content-*
slots filled in.
This is the canonical normalize surface — :func:normalize_headers
is a thin wrapper that calls into this method so every
canonicalization site goes through one piece of code.
normalize_headers ¶
normalize_headers(
headers: "HTTPHeaders | Mapping[HeaderValue, HeaderValue] | None",
*,
is_request: bool,
add_missing: bool = True,
mode: Literal["remove", "redact"] = "remove",
anonymize: bool = False,
body: Optional[IO] = None
) -> "HTTPHeaders"
Backwards-compatible thin wrapper around :meth:HTTPHeaders.normalized.
The whole normalization vocabulary (canonical names, sensitive
detection, body-derived Content-* backfill, request defaults)
lives on :class:HTTPHeaders so a single audit covers the whole
behaviour. This free function stays so existing callers
(normalize_headers(some_dict, is_request=True)) keep working
without rewriting every site.