from __future__ import annotations import hashlib import json import math from typing import Any from pydantic import BaseModel def _jcs(value: Any) -> str: """Deterministic JSON close to RFC 8785 for this integer/string DTO domain.""" if value is None: return "null" if value is True: return "true" if value is False: return "false" if isinstance(value, str): return json.dumps(value, ensure_ascii=False, separators=(",", ":")) if isinstance(value, int): return str(value) if isinstance(value, float): if not math.isfinite(value): raise ValueError("non-finite numbers are not JSON canonicalizable") raise TypeError("floating point values are forbidden in safety fingerprints") if isinstance(value, list): return "[" + ",".join(_jcs(item) for item in value) + "]" if isinstance(value, dict): keys = sorted(value, key=lambda key: key.encode("utf-16be")) return "{" + ",".join(f"{_jcs(key)}:{_jcs(value[key])}" for key in keys) + "}" raise TypeError(f"unsupported fingerprint type: {type(value).__name__}") def canonical_json(model: BaseModel | dict[str, Any]) -> bytes: value = model.model_dump(mode="json") if isinstance(model, BaseModel) else model return _jcs(value).encode("utf-8") def fingerprint(model: BaseModel | dict[str, Any]) -> bytes: return hashlib.sha256(canonical_json(model)).digest()