from __future__ import annotations import hashlib import json from collections.abc import Mapping, Sequence from datetime import datetime from typing import Any from fastapi import Request, Response from fastapi.encoders import jsonable_encoder def _strip_keys(value: Any, excluded_keys: set[str]) -> Any: if isinstance(value, Mapping): return { key: _strip_keys(item, excluded_keys) for key, item in value.items() if key not in excluded_keys } if isinstance(value, Sequence) and not isinstance(value, (str, bytes, bytearray)): return [_strip_keys(item, excluded_keys) for item in value] return value def build_etag(payload: Any, *, exclude_keys: set[str] | None = None) -> str: encoded = jsonable_encoder(payload) if exclude_keys: encoded = _strip_keys(encoded, exclude_keys) serialized = json.dumps(encoded, sort_keys=True, separators=(",", ":"), ensure_ascii=False) digest = hashlib.sha1(serialized.encode("utf-8")).hexdigest() return f'"{digest}"' def is_matching_etag(request: Request, etag: str) -> bool: header = request.headers.get("if-none-match") if not header: return False for candidate in header.split(","): if candidate.strip() in {etag, "*"}: return True return False def apply_cache_headers( response: Response, *, cache_control: str, etag: str | None = None, last_modified: datetime | None = None, ) -> None: response.headers["Cache-Control"] = cache_control response.headers["Vary"] = "Cookie, Authorization" if etag is not None: response.headers["ETag"] = etag if last_modified is not None: response.headers["Last-Modified"] = last_modified.strftime("%a, %d %b %Y %H:%M:%S GMT") def set_no_store(response: Response) -> None: apply_cache_headers(response, cache_control="no-store") def set_private_revalidate(response: Response, *, etag: str | None = None, last_modified: datetime | None = None) -> None: apply_cache_headers( response, cache_control="private, max-age=0, must-revalidate", etag=etag, last_modified=last_modified, ) def set_public_immutable(response: Response, *, etag: str | None = None, last_modified: datetime | None = None) -> None: apply_cache_headers( response, cache_control="public, max-age=31536000, immutable", etag=etag, last_modified=last_modified, )