ledger_io.py python
79 lines 2.5 KB
Raw
sha256:a78e7e5a8740e03315f325d19edeb3aa1b306b3337d04abbaa9a9e0f3bbeb7a1 docs: MuseHub-first before ISR #74 — staging solidify NEXT Human 1 day ago
1 """JSONL ledger IO helpers (§K9.7)."""
2
3 from __future__ import annotations
4
5 import json
6 import os
7 import tempfile
8 from pathlib import Path
9 from typing import Any
10
11
12 class LedgerIOError(Exception):
13 """Raised when ledger file IO fails."""
14
15 def __init__(self, message: str) -> None:
16 super().__init__(message)
17
18
19 def split_jsonl_lines(text: str) -> list[str]:
20 """Split JSONL text and ignore a trailing empty segment after final LF."""
21 if not text:
22 return []
23 lines = text.split("\n")
24 if lines and lines[-1] == "":
25 lines = lines[:-1]
26 return lines
27
28
29 def parse_jsonl_text(text: str) -> list[dict[str, Any]]:
30 """Parse JSONL records; malformed JSON raises ``ValueError``."""
31 records: list[dict[str, Any]] = []
32 for index, line in enumerate(split_jsonl_lines(text)):
33 if not line.strip():
34 continue
35 try:
36 obj = json.loads(line)
37 except json.JSONDecodeError as exc:
38 raise ValueError(f"malformed JSONL at line {index + 1}: {exc}") from exc
39 if not isinstance(obj, dict):
40 raise ValueError(f"ledger line {index + 1} must be a JSON object")
41 records.append(obj)
42 return records
43
44
45 def read_ledger_entries(path: Path) -> list[dict[str, Any]]:
46 """Read ledger entries; missing or empty file returns []."""
47 if not path.is_file():
48 return []
49 text = path.read_text(encoding="utf-8")
50 if not text:
51 return []
52 return parse_jsonl_text(text)
53
54
55 def serialize_entry(entry: dict[str, Any]) -> str:
56 """Serialize one ledger entry as a single JSONL line with trailing LF."""
57 return json.dumps(entry, ensure_ascii=False, separators=(",", ":"), sort_keys=True) + "\n"
58
59
60 def atomic_append_lines(path: Path, lines: list[str]) -> None:
61 """Append lines atomically; on failure leave prior bytes unchanged."""
62 path.parent.mkdir(parents=True, exist_ok=True)
63 if not path.exists():
64 path.write_text("", encoding="utf-8")
65
66 existing = path.read_text(encoding="utf-8")
67 new_content = existing + "".join(lines)
68 fd, temp_name = tempfile.mkstemp(prefix=".ledger-", dir=path.parent)
69 temp_path = Path(temp_name)
70 try:
71 with os.fdopen(fd, "w", encoding="utf-8") as handle:
72 handle.write(new_content)
73 handle.flush()
74 os.fsync(handle.fileno())
75 os.replace(temp_path, path)
76 except OSError as exc:
77 if temp_path.exists():
78 temp_path.unlink(missing_ok=True)
79 raise LedgerIOError(f"ledger write failed: {exc}") from exc
File History 2 commits
sha256:a78e7e5a8740e03315f325d19edeb3aa1b306b3337d04abbaa9a9e0f3bbeb7a1 docs: MuseHub-first before ISR #74 — staging solidify NEXT Human 1 day ago
sha256:4671b7f787ddbe63ced31c895b688c77ab495653b65a730b423329f26b3c1439 feat: K1-P1 complete — agent provenance, build-verification… Sonnet 4.6 patch 52 days ago