"""Canonical serialization and stable SHA-256 helpers.""" from __future__ import annotations import dataclasses import hashlib import json from pathlib import Path from typing import Any, Mapping import numpy as np def to_jsonable(value: Any) -> Any: """Convert supported scientific-Python values to strict JSON values.""" if dataclasses.is_dataclass(value): return to_jsonable(dataclasses.asdict(value)) if isinstance(value, Path): return str(value) if isinstance(value, np.ndarray): return to_jsonable(value.tolist()) if isinstance(value, np.generic): return to_jsonable(value.item()) if isinstance(value, Mapping): return {str(key): to_jsonable(item) for key, item in value.items()} if isinstance(value, (tuple, list)): return [to_jsonable(item) for item in value] if isinstance(value, (str, int, float, bool)) or value is None: return value raise TypeError(f"Unsupported value for canonical JSON: {type(value).__name__}") def canonical_json_bytes(value: Any) -> bytes: """Return deterministic UTF-8 JSON bytes, rejecting NaN and infinity.""" return json.dumps( to_jsonable(value), sort_keys=True, separators=(",", ":"), ensure_ascii=False, allow_nan=False, ).encode("utf-8") def stable_hash(value: Any, *, prefix: str = "") -> str: """Hash a value using canonical JSON and an optional domain prefix.""" digest = hashlib.sha256() if prefix: digest.update(prefix.encode("utf-8")) digest.update(b"\0") digest.update(canonical_json_bytes(value)) return digest.hexdigest() def file_sha256(path: Path) -> str: digest = hashlib.sha256() with path.open("rb") as stream: while True: block = stream.read(1024 * 1024) if not block: break digest.update(block) return digest.hexdigest()