Engine API Reference

This page is generated from docstrings via Sphinx autodoc. It is intended for contributors who want to understand or extend the engine.

Note

LedgerLoom is still in early versions. The engine API is intentionally small and conservative. We prefer adding new helpers over changing existing behavior.

Package exports

LedgerLoom Engine (v0.1).

This package contains reusable accounting primitives extracted from the chapter runners. Chapters remain responsible for writing artifacts.

Public v0.1 API (intentionally small): - ledgerloom.engine.config.LedgerEngineConfig - ledgerloom.engine.ledger.LedgerEngine - ledgerloom.engine.coa.COASchema

class ledgerloom.engine.Account(code: 'str', name: 'str', account_type: 'str', normal_side: 'str', statement: 'str', rollup_code: 'str', is_contra: 'bool', is_active: 'bool', track_department: 'bool', track_project: 'bool', description: 'str')[source]
account_type: str
code: str
description: str
is_active: bool
is_contra: bool
name: str
normal_side: str
rollup_code: str
statement: str
track_department: bool
track_project: bool
class ledgerloom.engine.COASchema(accounts: tuple[Account, ...], segment_dimensions: tuple[dict[str, str], ...], segment_values: tuple[SegmentValue, ...])[source]

A COA schema bundle suitable for joins + validation.

account_master_rows() list[dict[str, str]][source]
accounts: tuple[Account, ...]
canonical_master_hash() str[source]
static default() COASchema[source]
example_income_statement_by_department(seed: int) list[dict[str, str]][source]
schema_dict() dict[str, object][source]
segment_dimensions: tuple[dict[str, str], ...]
segment_value_rows() list[dict[str, str]][source]
segment_values: tuple[SegmentValue, ...]
validate_checks() list[str][source]
class ledgerloom.engine.Dimension(name: str, key: str, default: str = '', required: bool = False)[source]

A configurable segment (dimension) materialized into the postings fact table.

Dimensions are read from Entry.meta and written as separate string columns on the postings table (e.g., department, project, location).

This stays intentionally simple in v0.1: - Entry-level metadata only (no posting-level overrides). - Deterministic: dimension columns appear in the configured order.

default: str = ''
key: str
name: str
required: bool = False
class ledgerloom.engine.LedgerEngine(cfg: LedgerEngineConfig | None = None, *, config: LedgerEngineConfig | None = None)[source]

The reusable ledger compute engine.

The LedgerLoom engine is the contract layer between accounting ideas and software engineering practice:

  • Chapters are free to focus on pedagogy and artifacts.

  • The engine provides a single, tested implementation of conventions (normal balances, posting IDs, deterministic math).

  • Tests and invariants make refactors safe and keep outputs reproducible.

Example

>>> from ledgerloom.engine import LedgerEngine
>>> eng = LedgerEngine()  # or LedgerEngine(cfg=...), LedgerEngine(config=...)
>>> postings = eng.postings_fact_table(entries)

v0.1 minimal API surface (methods): postings_fact_table, balances_by_* , running_balance_by_posting, invariants, gl_schema_description.

balances_by_account(postings: DataFrame) DataFrame[source]
balances_by_account_as_of(postings: DataFrame, as_of: date | str) DataFrame[source]

Balances grouped by account, using postings up to as_of.

balances_by_department(postings: DataFrame) DataFrame[source]
balances_by_dimension(postings: DataFrame, dimension: str) DataFrame[source]
balances_by_period(postings: DataFrame) DataFrame[source]
cfg: LedgerEngineConfig
gl_schema_description() dict[str, Any][source]
invariants(entries: list[Entry], postings: DataFrame) dict[str, Any][source]
postings_as_of(postings: DataFrame, as_of: date | str) DataFrame[source]

Filter postings to rows with date <= as_of.

postings_fact_table(entries: list[Entry]) DataFrame[source]
running_balance_by_posting(postings: DataFrame) DataFrame[source]
validate_entries(entries: list[Entry]) None[source]

Run strict validation checks (does not mutate entries).

class ledgerloom.engine.LedgerEngineConfig(debit_normal_roots: FrozenSet[str] = <factory>, credit_normal_roots: FrozenSet[str] = <factory>, entry_id_key: str = 'entry_id', department_key: str = 'department', dimensions: tuple[~ledgerloom.engine.config.Dimension, ...] | None=None, strict_validation: bool = False, entry_id_policy: Literal['strict', 'generated']='strict')[source]

Configuration for LedgerEngine.

Roots are the first segment of an account path, e.g. Assets:Cash -> Assets.

Normal-balance convention: - debit-normal: balances increase with debits (Assets, Expenses) - credit-normal: balances increase with credits (Liabilities, Equity, Revenue)

The engine treats unknown roots as debit-normal for computation, but invariants will report them.

credit_normal_roots: FrozenSet[str]
debit_normal_roots: FrozenSet[str]
department_key: str = 'department'
dimensions: tuple[Dimension, ...] | None = None
property effective_dimensions: tuple[Dimension, ...]

Return configured dimension specs in deterministic order.

If dimensions is None, fall back to a single department dimension using department_key (backward-compatible with earlier chapters/apps).

entry_id_key: str = 'entry_id'
entry_id_policy: Literal['strict', 'generated'] = 'strict'
property recognized_roots: FrozenSet[str]
strict_validation: bool = False
class ledgerloom.engine.SegmentValue(dimension_code: 'str', value_code: 'str', value_name: 'str')[source]
dimension_code: str
value_code: str
value_name: str
ledgerloom.engine.closing_entries_from_adjusted_tb(tb_adj: DataFrame, *, period: str, close_date: date) list[Entry][source]

Generate closing entries from an adjusted trial balance.

Returns a list of Entry objects.

Closing policy (workbook-friendly): - Revenue accounts close directly to Retained Earnings. - Expense accounts close directly to Retained Earnings. - Dividend/Draw accounts (identified by account name) close to Retained Earnings.

The helper is deterministic: - stable entry_ids - stable account ordering inside each entry

Ledger compilation + views

Ledger engine (v0.1).

The engine takes a list of ledgerloom.core.Entry objects and produces canonical ledger tables:

  • postings: one row per posting line (fact table)

  • balance views: by account / period / segment

  • invariants: explicit constraints you can assert in tests

This module intentionally stays “boring”: it copies chapter logic into a reusable core, keeping byte-for-byte identical artifacts when chapters call it.

class ledgerloom.engine.ledger.LedgerEngine(cfg: LedgerEngineConfig | None = None, *, config: LedgerEngineConfig | None = None)[source]

The reusable ledger compute engine.

The LedgerLoom engine is the contract layer between accounting ideas and software engineering practice:

  • Chapters are free to focus on pedagogy and artifacts.

  • The engine provides a single, tested implementation of conventions (normal balances, posting IDs, deterministic math).

  • Tests and invariants make refactors safe and keep outputs reproducible.

Example

>>> from ledgerloom.engine import LedgerEngine
>>> eng = LedgerEngine()  # or LedgerEngine(cfg=...), LedgerEngine(config=...)
>>> postings = eng.postings_fact_table(entries)

v0.1 minimal API surface (methods): postings_fact_table, balances_by_* , running_balance_by_posting, invariants, gl_schema_description.

balances_by_account(postings: DataFrame) DataFrame[source]
balances_by_account_as_of(postings: DataFrame, as_of: date | str) DataFrame[source]

Balances grouped by account, using postings up to as_of.

balances_by_department(postings: DataFrame) DataFrame[source]
balances_by_dimension(postings: DataFrame, dimension: str) DataFrame[source]
balances_by_period(postings: DataFrame) DataFrame[source]
cfg: LedgerEngineConfig
gl_schema_description() dict[str, Any][source]
invariants(entries: list[Entry], postings: DataFrame) dict[str, Any][source]
postings_as_of(postings: DataFrame, as_of: date | str) DataFrame[source]

Filter postings to rows with date <= as_of.

postings_fact_table(entries: list[Entry]) DataFrame[source]
running_balance_by_posting(postings: DataFrame) DataFrame[source]
validate_entries(entries: list[Entry]) None[source]

Run strict validation checks (does not mutate entries).

ledgerloom.engine.ledger.account_root(account: str) str[source]

Return the root segment of a colon-path account.

ledgerloom.engine.ledger.balances_by_account(postings: DataFrame, cfg: LedgerEngineConfig) DataFrame[source]

Materialized view: balances grouped by account.

ledgerloom.engine.ledger.balances_by_department(postings: DataFrame) DataFrame[source]

Materialized view: balances grouped by department and root.

ledgerloom.engine.ledger.balances_by_dimension(postings: DataFrame, dimension: str) DataFrame[source]

Materialized view: balances grouped by a dimension and root.

ledgerloom.engine.ledger.balances_by_period(postings: DataFrame) DataFrame[source]

Materialized view: balances grouped by period (YYYY-MM) and account.

ledgerloom.engine.ledger.entry_department(entry: Entry, cfg: LedgerEngineConfig) str[source]

Backward-compatible helper: the configured “department” dimension value.

ledgerloom.engine.ledger.entry_dimensions(entry: Entry, cfg: LedgerEngineConfig) dict[str, str][source]

Return configured dimension values for this entry.

Dimensions are read from Entry.meta and materialized as columns on the postings fact table. Missing keys fall back to each dimension’s default.

ledgerloom.engine.ledger.entry_id(entry: Entry, cfg: LedgerEngineConfig) str[source]

Return the stable identifier for an entry.

By default, LedgerLoom treats entry.meta[cfg.entry_id_key] as required. This is a pragmatic constraint for real systems: it makes matching, reconciliation, and traceability explicit.

The behavior is controlled by ledgerloom.engine.config.LedgerEngineConfig.entry_id_policy:

  • "strict": raise if missing

  • "generated": synthesize a deterministic id from entry content

ledgerloom.engine.ledger.gl_schema_description(cfg: LedgerEngineConfig | None = None) dict[str, Any][source]

A tiny schema description for the GL tables (for docs/tooling).

ledgerloom.engine.ledger.invariants(entries: list[Entry], postings: DataFrame, cfg: LedgerEngineConfig) dict[str, Any][source]

Compute core invariants for a balanced ledger.

ledgerloom.engine.ledger.postings_as_of(postings: DataFrame, as_of: date | str) DataFrame[source]

Filter postings to rows with date <= as_of.

The postings table stores dates as ISO strings (YYYY-MM-DD), so lexical comparison is safe and deterministic.

ledgerloom.engine.ledger.postings_fact_table(entries: list[Entry], cfg: LedgerEngineConfig) DataFrame[source]

Build the postings fact table (one row per posting line).

ledgerloom.engine.ledger.running_balance_by_posting(postings: DataFrame, cfg: LedgerEngineConfig | None = None) DataFrame[source]

Window-function style running balances per account.

ledgerloom.engine.ledger.signed_cents(cfg: LedgerEngineConfig, root: str, debit_cents: int, credit_cents: int) int[source]

Return balance delta in the account’s normal sign convention.

ledgerloom.engine.ledger.validate_entries(entries: list[Entry], cfg: LedgerEngineConfig) None[source]

Optional strict validation for real-world usage (opt-in).

This function is intentionally separate from invariants: - invariants are reports you can assert in tests - strict validation is an exception-throwing gate you can enable in apps

Enabled by setting LedgerEngineConfig.strict_validation = True or by calling ledgerloom.engine.ledger.LedgerEngine.validate_entries().

Chart of Accounts schema

Chart of Accounts (COA) engine primitives.

Chapter 03 introduces a COA as a schema: - accounts as a dimension table (account master) - rollups (parent/child) for reporting - segments (department/project) as extra dimensions

The engine provides data structures + deterministic computations. Chapters can write these out to CSV/JSON however they like.

class ledgerloom.engine.coa.Account(code: 'str', name: 'str', account_type: 'str', normal_side: 'str', statement: 'str', rollup_code: 'str', is_contra: 'bool', is_active: 'bool', track_department: 'bool', track_project: 'bool', description: 'str')[source]
account_type: str
code: str
description: str
is_active: bool
is_contra: bool
name: str
normal_side: str
rollup_code: str
statement: str
track_department: bool
track_project: bool
class ledgerloom.engine.coa.COASchema(accounts: tuple[Account, ...], segment_dimensions: tuple[dict[str, str], ...], segment_values: tuple[SegmentValue, ...])[source]

A COA schema bundle suitable for joins + validation.

account_master_rows() list[dict[str, str]][source]
accounts: tuple[Account, ...]
canonical_master_hash() str[source]
static default() COASchema[source]
example_income_statement_by_department(seed: int) list[dict[str, str]][source]
schema_dict() dict[str, object][source]
segment_dimensions: tuple[dict[str, str], ...]
segment_value_rows() list[dict[str, str]][source]
segment_values: tuple[SegmentValue, ...]
validate_checks() list[str][source]
class ledgerloom.engine.coa.SegmentValue(dimension_code: 'str', value_code: 'str', value_name: 'str')[source]
dimension_code: str
value_code: str
value_name: str
ledgerloom.engine.coa.build_account_master_rows(accounts: Sequence[Account]) list[dict[str, str]][source]
ledgerloom.engine.coa.canonical_master_hash(master_rows: Sequence[dict[str, str]]) str[source]
ledgerloom.engine.coa.dec_str_2(x: Decimal) str[source]

Stable 2-decimal formatting; normalize -0.00 -> 0.00.

ledgerloom.engine.coa.default_accounts() list[Account][source]

A tiny but realistic default COA used in Chapter 03.

ledgerloom.engine.coa.default_segments() tuple[list[dict[str, str]], list[SegmentValue]][source]
ledgerloom.engine.coa.example_income_statement_by_department(seed: int) list[dict[str, str]][source]

Tiny worked example: revenue + expenses by department.

ledgerloom.engine.coa.schema_dict() dict[str, object][source]
ledgerloom.engine.coa.sha256_bytes(b: bytes) str[source]
ledgerloom.engine.coa.validate_accounts(accounts: Sequence[Account]) list[str][source]

Configuration

LedgerLoom Engine configuration.

The “engine” is the reusable core that chapters can build on.

v0.1 design constraints - Small surface area (a handful of types/functions). - Explicit accounting conventions (normal balances by root). - Deterministic math (integer cents / stable string formatting). - Engine is pure-compute; chapters own file I/O.

class ledgerloom.engine.config.Dimension(name: str, key: str, default: str = '', required: bool = False)[source]

A configurable segment (dimension) materialized into the postings fact table.

Dimensions are read from Entry.meta and written as separate string columns on the postings table (e.g., department, project, location).

This stays intentionally simple in v0.1: - Entry-level metadata only (no posting-level overrides). - Deterministic: dimension columns appear in the configured order.

default: str = ''
key: str
name: str
required: bool = False
class ledgerloom.engine.config.LedgerEngineConfig(debit_normal_roots: FrozenSet[str] = <factory>, credit_normal_roots: FrozenSet[str] = <factory>, entry_id_key: str = 'entry_id', department_key: str = 'department', dimensions: tuple[~ledgerloom.engine.config.Dimension, ...] | None=None, strict_validation: bool = False, entry_id_policy: Literal['strict', 'generated']='strict')[source]

Configuration for LedgerEngine.

Roots are the first segment of an account path, e.g. Assets:Cash -> Assets.

Normal-balance convention: - debit-normal: balances increase with debits (Assets, Expenses) - credit-normal: balances increase with credits (Liabilities, Equity, Revenue)

The engine treats unknown roots as debit-normal for computation, but invariants will report them.

credit_normal_roots: FrozenSet[str]
debit_normal_roots: FrozenSet[str]
department_key: str = 'department'
dimensions: tuple[Dimension, ...] | None = None
property effective_dimensions: tuple[Dimension, ...]

Return configured dimension specs in deterministic order.

If dimensions is None, fall back to a single department dimension using department_key (backward-compatible with earlier chapters/apps).

entry_id_key: str = 'entry_id'
entry_id_policy: Literal['strict', 'generated'] = 'strict'
property recognized_roots: FrozenSet[str]
strict_validation: bool = False

Money helpers

Deterministic money helpers.

Chapters intentionally write monetary amounts as strings with 2 decimals. Internally, the engine computes in integer cents to avoid floating-point drift.

Why integer cents? - Avoids floating-point rounding drift. - Makes invariants testable (sums are exact integers). - Keeps output deterministic across platforms.

Why explicit rounding? Decimal.quantize can consult the ambient Decimal context if no rounding mode is specified. LedgerLoom makes rounding explicit so results are stable and the convention is documented.

For v0.1, LedgerLoom uses ROUND_HALF_UP (“5 rounds up”).

ledgerloom.engine.money.cents_to_str(cents: int) str[source]

Format cents as a fixed 2-decimal string.

Examples

0 -> “0.00” 12 -> “0.12” -305 -> “-3.05”

ledgerloom.engine.money.str_to_cents(s: str) int[source]

Parse a decimal string and return integer cents.

This is primarily used for internal view computation.

Note: input is rounded to cents using the engine’s rounding policy.

ledgerloom.engine.money.to_cents(x: Decimal) int[source]

Quantize to cents and return an integer number of cents.