# ============================================================================= # Projet : AIR — Accounting Intermediate Representation # Auteur : Simon-Pierre Boucher # Contact : contact@spboucher.ai # Fichier : document_io.py # Description : Load/serialize AIR documents (YAML/JSON) with strict float rejection. # ============================================================================= """AIR document I/O. AIR documents are YAML or JSON. All amounts, quantities, and rates MUST be strings ("19.99") — bare numbers would arrive as binary floats and are rejected by the schema (see core/events.py). This keeps every figure exact. """ from __future__ import annotations import json from pathlib import Path from typing import Any import yaml from core.events import AirDocument def load_air_document(path: str | Path) -> AirDocument: path = Path(path) text = path.read_text(encoding="utf-8") data: Any if path.suffix == ".json": data = json.loads(text) data.pop("_author", None) # header key, not part of the schema else: data = yaml.safe_load(text) if not isinstance(data, dict): raise ValueError(f"{path}: expected a mapping at top level") return AirDocument.model_validate(data) def dump_air_document(document: AirDocument) -> str: """Canonical JSON serialization (amounts as strings).""" payload = {"_author": "Simon-Pierre Boucher "} payload.update(json.loads(document.model_dump_json(exclude_none=True))) return json.dumps(payload, indent=2, default=str)