Source code for cadaques.core.ledger

"""The campaign ledger: every transaction, declared and settled.

The ledger is the accounting record of a discovery campaign and,
almost for free, its provenance and replay substrate: exporting it
to JSONL yields a complete, reproducible trace of what was asked,
what it was expected to cost, and what it actually cost.
"""

from __future__ import annotations

import json
import time
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Iterator, Literal, Mapping

from .cost import Cost

TransactionKind = Literal["oracle", "driver"]


[docs] @dataclass(frozen=True) class Transaction: kind: TransactionKind label: str declared: Cost settled: Cost timestamp: float = field(default_factory=time.time) meta: Mapping[str, Any] = field(default_factory=dict) @property def overrun(self) -> Cost: """Settled minus declared: the price of optimism.""" return self.settled - self.declared
[docs] @dataclass class Ledger: """Append-only record of all campaign transactions.""" transactions: list[Transaction] = field(default_factory=list) def record( self, kind: TransactionKind, label: str, declared: Cost, settled: Cost, **meta: Any, ) -> Transaction: tx = Transaction(kind=kind, label=label, declared=declared, settled=settled, meta=meta) self.transactions.append(tx) return tx # -- aggregation ------------------------------------------------ def total(self, kind: TransactionKind | None = None) -> Cost: total = Cost() for tx in self.transactions: if kind is None or tx.kind == kind: total = total + tx.settled return total
[docs] def cumulative(self, kind: TransactionKind | None = None) -> Iterator[tuple[Transaction, Cost]]: """Yield (transaction, running settled total) pairs.""" running = Cost() for tx in self.transactions: if kind is None or tx.kind == kind: running = running + tx.settled yield tx, running
def __len__(self) -> int: return len(self.transactions) # -- persistence ------------------------------------------------ def to_jsonl(self, path: str | Path) -> Path: path = Path(path) with path.open("w", encoding="utf-8") as fh: for tx in self.transactions: fh.write( json.dumps( { "kind": tx.kind, "label": tx.label, "declared": tx.declared.as_dict(), "settled": tx.settled.as_dict(), "timestamp": tx.timestamp, "meta": dict(tx.meta), } ) + "\n" ) return path