TOMK / src /python /tomagi /canonical.py
c0mbatduckzz's picture
Publish WQK 0.6 formal specification, use cases and verified release (part 2)
767a051 verified
Raw History Blame Contribute Delete
1.7 kB
"""Canonical JSON and content-addressed definition records."""
from __future__ import annotations
import hashlib
import json
import math
from typing import Any
def _validate_json(value: Any, *, path: str = "$") -> None:
"""Reject Python values that cannot originate from a JSON document."""
if value is None or isinstance(value, (bool, str, int)):
return
if isinstance(value, float):
if not math.isfinite(value):
raise ValueError(f"{path}: non-finite numbers are not valid JSON")
return
if isinstance(value, list):
for index, child in enumerate(value):
_validate_json(child, path=f"{path}[{index}]")
return
if isinstance(value, dict):
for key, child in value.items():
if not isinstance(key, str):
raise TypeError(f"{path}: JSON object keys must be strings")
_validate_json(child, path=f"{path}.{key}")
return
raise TypeError(f"{path}: value of type {type(value).__name__} is not JSON-native")
def canonical_bytes(value: Any) -> bytes:
_validate_json(value)
return json.dumps(value, sort_keys=True, ensure_ascii=False,
separators=(",", ":"), allow_nan=False).encode("utf-8")
def content_hash(record: dict[str, Any]) -> str:
body = {k: v for k, v in record.items() if k != "content_hash"}
return "sha256:" + hashlib.sha256(canonical_bytes(body)).hexdigest()
def attach_hash(record: dict[str, Any]) -> dict[str, Any]:
out = dict(record)
out["content_hash"] = content_hash(out)
return out
def verify_hash(record: dict[str, Any]) -> bool:
return record.get("content_hash") == content_hash(record)