Skip to content

spanforge.io — Synchronous JSONL utilities

Module: spanforge.io
Added in: 2.0.3

spanforge.io provides reliable, synchronous JSONL read/write helpers. They are a simpler, dependency-free alternative to the async JSONLExporter and EventStream.from_file() pattern — ideal for scripts, tests, and offline pipelines.


Quick example

from spanforge.io import write_events, read_events

# Write eval results as event envelopes
write_events(
    [{"case_id": "tc-001", "score": 0.95}, {"case_id": "tc-002", "score": 0.72}],
    "results.jsonl",
    event_type="llm.eval.done",
    source="my-eval-runner@1.0",
)

# Read them back, filtered by event type
payloads = read_events("results.jsonl", event_type="llm.eval.done")
# → [{"case_id": "tc-001", "score": 0.95}, ...]

API

write_jsonl()

def write_jsonl(
    records: Iterable[dict],
    path: str | Path,
    *,
    mode: str = "w",
) -> int: ...

Serialise each record as a JSON line and write to path. Parent directories are created automatically.

ParameterDescription
recordsAny iterable of dict objects (including generators).
pathDestination file path.
mode"w" (overwrite, default) or "a" (append).

Returns: Number of records written.


read_jsonl()

def read_jsonl(
    path: str | Path,
    *,
    event_type: str | None = None,
    skip_errors: bool = True,
) -> list[dict]: ...

Read a JSONL file and return a list of dict records.

ParameterDescription
pathSource file path. Raises FileNotFoundError if absent.
event_typeIf set, only records with "event_type" == event_type are returned.
skip_errorsWhen True (default), malformed lines are silently skipped. Set to False to raise json.JSONDecodeError on the first bad line.

Non-dict JSON values (arrays, scalars) are always skipped.


append_jsonl()

def append_jsonl(record: dict, path: str | Path) -> None: ...

Append a single record to path, creating the file if it does not exist. Equivalent to write_jsonl([record], path, mode="a").


write_events()

def write_events(
    payloads: Iterable[dict],
    path: str | Path,
    *,
    event_type: str,
    source: str = "spanforge",
    mode: str = "w",
) -> int: ...

Wrap each payload in a spanforge event envelope and write to path.

Envelope format:

{"event_type": "<event_type>", "source": "<source>", "payload": { ... }}

read_events()

def read_events(
    path: str | Path,
    *,
    event_type: str,
) -> list[dict]: ...

Read event envelopes from path and return the unwrapped payload objects where "event_type" matches. Lines that do not carry a "payload" field are silently skipped.