import sqlite3 import json from typing import Any, Dict, List, Optional from datetime import datetime from .models import NarrativeBlock class EventLog: """Simple append-only event log stored in SQLite. Events are JSON with a timestamp.""" def __init__(self, path: str = ":memory:"): self.path = path self._conn = sqlite3.connect(self.path, check_same_thread=False) self._init_db() def _init_db(self): cur = self._conn.cursor() cur.execute(""" CREATE TABLE IF NOT EXISTS events ( id INTEGER PRIMARY KEY AUTOINCREMENT, ts TEXT NOT NULL, type TEXT NOT NULL, payload TEXT NOT NULL ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS blocks ( id TEXT PRIMARY KEY, ts TEXT NOT NULL, block_json TEXT NOT NULL, ledger_anchor TEXT ) """) self._conn.commit() def append_event(self, event_type: str, payload: Dict[str, Any], ts: Optional[datetime] = None): ts = ts or datetime.utcnow() cur = self._conn.cursor() cur.execute( "INSERT INTO events (ts, type, payload) VALUES (?, ?, ?)", (ts.isoformat(), event_type, json.dumps(payload, sort_keys=True)), ) self._conn.commit() def read_events(self) -> List[Dict[str, Any]]: cur = self._conn.cursor() cur.execute("SELECT ts, type, payload FROM events ORDER BY ts, id") rows = cur.fetchall() out = [] for ts, type_, payload in rows: out.append({"ts": ts, "type": type_, "payload": json.loads(payload)}) return out def save_block(self, block: NarrativeBlock, ledger_anchor: Optional[str] = None): cur = self._conn.cursor() # model_dump -> dict, then dump with deterministic keys for ledger block_json = json.dumps(block.model_dump(), sort_keys=True, default=str) cur.execute( "INSERT OR REPLACE INTO blocks (id, ts, block_json, ledger_anchor) VALUES (?, ?, ?, ?)", (block.id, block.timestamp.isoformat(), block_json, ledger_anchor), ) self._conn.commit() def get_block(self, block_id: str) -> Optional[NarrativeBlock]: cur = self._conn.cursor() cur.execute("SELECT block_json FROM blocks WHERE id = ?", (block_id,)) row = cur.fetchone() if not row: return None return NarrativeBlock.parse_raw(row[0])