Skip to main content

Generic ledger and storage primitives for Python projects

Project description

ledgercore

Generic, typed storage and reference primitives for ledger-like Python applications.

ledgercore is a small Python library for projects that store structured records as files. It provides reusable primitives for atomic writes, YAML front matter, deterministic JSON/YAML storage, safe relative paths, config discovery, numeric IDs, and cross-ledger references.

It has no CLI and no dependency on any downstream ledger application.

Why ledgercore exists

Ledger-like tools (task trackers, architecture logs, spec registries) share the same low-level problems: safely writing files, formatting IDs, validating paths, and linking records across namespaces. ledgercore extracts those shared primitives into one typed, zero-surprise package so downstream projects do not reinvent them.

What is included

  • Atomic UTF-8 text writes and create-only writes.
  • YAML front matter read/write helpers.
  • Deterministic JSON, JSONL, and YAML file I/O.
  • Safe relative POSIX path validation.
  • Generic content fingerprints and path-text normalization.
  • Upward config discovery.
  • Prefixed numeric ID formatting.
  • Cross-ledger references such as tl:task-0001.
  • A typed public API and shared exception hierarchy.

What is not included

  • No command-line interface.
  • No database layer.
  • No sync protocol.
  • No task, architecture, or project-specific schema.
  • No dependency on taskledger, archledger, or another product package.

Installation

pip install ledgercore

Requirements:

  • Python 3.10+
  • PyYAML

Quick start

from pathlib import Path

from ledgercore.frontmatter import write_front_matter_document
from ledgercore.ids import LedgerIdFormat
from ledgercore.refs import parse_resource_ref

task_ids = LedgerIdFormat(prefix="task")
task_id = task_ids.next(["task-0001", "task-0002"])

write_front_matter_document(
    Path(f"records/{task_id}.md"),
    {"id": task_id, "status": "open"},
    "# New task\n",
)

ref = parse_resource_ref("tl:task-0003")
assert ref.local_id == "task-0003"
assert ref.global_ref == "tl:task-0003"

Cross-ledger references

Inside a single ledger, keep local IDs short:

task-0001
adr-0002

When linking records across ledgers, use canonical global refs:

<ledger>:<kind>-<number>

Examples:

tl:task-0001
al:adr-0002
sw:spec-0003

A cross-ledger link can then store both endpoints unambiguously:

source: tl:task-0001
target: al:adr-0002
relation: implements

For filenames or systems that cannot use :, use the file-safe alias:

tl-task-0001
al-adr-0002
from ledgercore.refs import parse_resource_ref

ref = parse_resource_ref("tl:task-0001")

assert ref.ledger == "tl"
assert ref.kind == "task"
assert ref.number == 1
assert ref.local_id == "task-0001"
assert ref.global_ref == "tl:task-0001"
assert ref.file_ref == "tl-task-0001"

ID formatting

Use LedgerIdFormat as the primary ID formatter:

from ledgercore.ids import LedgerIdFormat

ids = LedgerIdFormat(prefix="task")

assert ids.format(1) == "task-0001"
assert ids.parse("task-0007") == 7
assert ids.next(["task-0001", "task-0002"]) == "task-0003"

For segmented, legacy-compatible IDs:

from ledgercore.ids import LedgerIdFormat

adr_ids = LedgerIdFormat(prefix="adr", separator="-", segment_separator="-")

assert adr_ids.format(13, segment="content") == "adr-content-0013"

NumericIdFormat remains available as a simpler compatibility wrapper.

Front matter documents

from pathlib import Path
from ledgercore.frontmatter import read_front_matter_document, write_front_matter_document

path = Path("records/task-0001.md")

write_front_matter_document(
    path,
    {"id": "task-0001", "status": "open"},
    "# Implement parser\n",
    body_mode="ensure-single-final-newline",
)

metadata, body = read_front_matter_document(path)

Front matter documents must start with --- followed by a newline and contain a YAML mapping. The body follows the closing --- delimiter.

For in-memory content, use split_front_matter_text, render_front_matter_text, and update_front_matter_text. Permissive parsing, timestamp-as-string loading, template placeholders, key ordering, and body normalization are explicit options.

Use scalar_style="minimal" for deterministic simple front matter:

from ledgercore.frontmatter import render_front_matter_text

text = render_front_matter_text(
    {"title": "Example", "tags": ["one", "two"], "empty": ""},
    scalar_style="minimal",
    sequence_indent="  ",
    empty_string_style="double",
)

The default remains PyYAML-compatible. Render options also pass through update and file-writing helpers. Template parsing supports whole-value placeholders and a conservative "anywhere" mode for simple scalar values.

JSON and YAML stores

from pathlib import Path
from ledgercore.jsonio import dumps_json, load_json_object, write_json
from ledgercore.yamlio import load_yaml_object, write_yaml

state_path = Path("state.json")
write_json(state_path, {"next": 4})
state = load_json_object(state_path, missing="empty")
compact = dumps_json(state, compact=True)

JSON output uses indent 2, sorted keys, and a final newline. YAML uses block style and can sort keys when requested.

canonical_json produces compact deterministic JSON for hashing. load_jsonl_object_rows retains source lines. load_jsonl_object_map builds a keyed manifest while reporting missing, invalid, and duplicate keys. write_jsonl_objects writes one compact object per line atomically.

Timestamp output supports precision and suffix control:

from ledgercore.time import utc_now_iso

timestamp = utc_now_iso(timespec="microseconds", timezone_style="offset")

Safe paths and config discovery

from pathlib import Path
from ledgercore.paths import (
    ConfigLocator, locate_config, resolve_config_relative_path,
)

locator = locate_config(Path.cwd(), ("ledger.toml", ".ledger.toml"))
if locator is not None:
    records_dir = resolve_config_relative_path(
        locator.config_path,
        "records",
        field_name="records_dir",
    )

locate_config returns a ConfigLocator with workspace_root, config_path, and source fields. Path helpers reject absolute paths, .., . segments, backslashes, and paths escaping the base directory.

Use ensure_inside_base, relative_to_base, and resolve_under_base when converting between resolved paths and safe base-relative paths. The separate normalize_path_text helper is for matching human-authored path text; it does not authorize filesystem access. It supports "basic", "wide", and "none" punctuation profiles plus custom translations.

Atomic writes

from pathlib import Path
from ledgercore.atomic import atomic_create_text, atomic_write_text

atomic_create_text(Path("records/task-0001.md"), "---\nid: task-0001\n---\n")
atomic_write_text(Path("index.json"), "{}\n")
  • atomic_create_text: create only; fails if target exists.
  • atomic_write_text: replace target atomically via temp file and os.replace.

Error model

All package-specific errors inherit from LedgerCoreError.

from ledgercore.errors import (
    LedgerCoreError, StorageError, AtomicWriteError,
    FrontMatterError, JsonStoreError, YamlStoreError,
    PathValidationError, IdFormatError,
)

try:
    ...
except LedgerCoreError as exc:
    print(exc.code, str(exc))

Each exception carries a stable code attribute for programmatic handling.

Using ledgercore from a CLI application

ledgercore does not depend on a CLI framework. Adapt its errors at the application boundary:

from ledgercore.errors import LedgerCoreError

def to_usage_error(exc: LedgerCoreError) -> UsageError:
    return UsageError(str(exc))

try:
    load_application_state()
except LedgerCoreError as exc:
    raise to_usage_error(exc) from exc

This keeps exit codes, terminal formatting, and framework-specific exception types in the downstream application.

Type checking

ledgercore ships a py.typed marker. It is fully typed and passes strict mypy with strict = true.

Development

python -m pip install -e ".[dev]"
python -m pytest -q
python -m ruff check .
python -m mypy ledgercore

Release checklist

  1. Update version in pyproject.toml.
  2. Run python -m pytest -q.
  3. Run python -m ruff check ..
  4. Run python -m mypy ledgercore.
  5. Run python -m build.
  6. Run python -m twine check dist/*.
  7. Smoke-test the built wheel in a clean virtualenv.

Stability

ledgercore is pre-1.0. Public APIs are intended to be stable within the 0.2.x series, but breaking changes may still happen before 1.0.0 when needed to keep the core API small and consistent.

  • No CLI is included.
  • No global configuration format is imposed.
  • No ledger schema is imposed.
  • No product-specific IDs are baked in.
  • All paths and refs are strings/paths chosen by downstream packages.

License

Apache-2.0.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

ledgercore-0.2.0.tar.gz (73.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

ledgercore-0.2.0-py3-none-any.whl (30.7 kB view details)

Uploaded Python 3

File details

Details for the file ledgercore-0.2.0.tar.gz.

File metadata

  • Download URL: ledgercore-0.2.0.tar.gz
  • Upload date:
  • Size: 73.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for ledgercore-0.2.0.tar.gz
Algorithm Hash digest
SHA256 6cde519ae10ddd96c8924fc4db6a76260e9dfd6b9b55038cb08181c4081caee4
MD5 553cf680b6292fb4c116ecfdc4c22977
BLAKE2b-256 e71bbc79dea29dd00c70c90437865fffbb514dc77f0d808db45af4afe026f43b

See more details on using hashes here.

File details

Details for the file ledgercore-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: ledgercore-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 30.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for ledgercore-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 d4388b3cc82df35d707509f936fe97aa3bfce7b34fd5d46b9ec7c646871e56e6
MD5 8f9c218725ef2c6822ee59ccaff67bfa
BLAKE2b-256 5f10a4c4b8cb55f19626ab63680ec8515de96c378f5309de756036d2a2445c3b

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page