lib-ledger-core 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. lib_ledger_core-0.3.0/.gitignore +61 -0
  2. lib_ledger_core-0.3.0/.gitlab-ci.yml +53 -0
  3. lib_ledger_core-0.3.0/CHANGELOG.md +42 -0
  4. lib_ledger_core-0.3.0/PKG-INFO +267 -0
  5. lib_ledger_core-0.3.0/README.md +231 -0
  6. lib_ledger_core-0.3.0/alembic.ini +41 -0
  7. lib_ledger_core-0.3.0/ledger_core/__init__.py +53 -0
  8. lib_ledger_core-0.3.0/ledger_core/adapters/__init__.py +17 -0
  9. lib_ledger_core-0.3.0/ledger_core/adapters/kurrent.py +105 -0
  10. lib_ledger_core-0.3.0/ledger_core/adapters/registry.py +38 -0
  11. lib_ledger_core-0.3.0/ledger_core/adapters/sqlalchemy.py +355 -0
  12. lib_ledger_core-0.3.0/ledger_core/adapters/tigerbeetle.py +380 -0
  13. lib_ledger_core-0.3.0/ledger_core/ddl/__init__.py +15 -0
  14. lib_ledger_core-0.3.0/ledger_core/ddl/generic.py +59 -0
  15. lib_ledger_core-0.3.0/ledger_core/ddl/plugin.py +19 -0
  16. lib_ledger_core-0.3.0/ledger_core/ddl/postgres.py +58 -0
  17. lib_ledger_core-0.3.0/ledger_core/ddl/registry.py +37 -0
  18. lib_ledger_core-0.3.0/ledger_core/exceptions.py +31 -0
  19. lib_ledger_core-0.3.0/ledger_core/interfaces.py +32 -0
  20. lib_ledger_core-0.3.0/ledger_core/migrations/__init__.py +39 -0
  21. lib_ledger_core-0.3.0/ledger_core/migrations/env.py +72 -0
  22. lib_ledger_core-0.3.0/ledger_core/migrations/script.py.mako +26 -0
  23. lib_ledger_core-0.3.0/ledger_core/migrations/versions/001_baseline.py +40 -0
  24. lib_ledger_core-0.3.0/ledger_core/models.py +28 -0
  25. lib_ledger_core-0.3.0/ledger_core/py.typed +0 -0
  26. lib_ledger_core-0.3.0/ledger_core/schema.py +40 -0
  27. lib_ledger_core-0.3.0/pyproject.toml +71 -0
  28. lib_ledger_core-0.3.0/tests/__init__.py +0 -0
  29. lib_ledger_core-0.3.0/tests/integration/__init__.py +0 -0
  30. lib_ledger_core-0.3.0/tests/integration/conftest.py +132 -0
  31. lib_ledger_core-0.3.0/tests/integration/contract.py +208 -0
  32. lib_ledger_core-0.3.0/tests/integration/entrypoint-tigerbeetle.sh +14 -0
  33. lib_ledger_core-0.3.0/tests/integration/test_kurrent.py +216 -0
  34. lib_ledger_core-0.3.0/tests/integration/test_sqlalchemy.py +68 -0
  35. lib_ledger_core-0.3.0/tests/integration/test_sqlite.py +139 -0
  36. lib_ledger_core-0.3.0/tests/integration/test_tigerbeetle.py +185 -0
  37. lib_ledger_core-0.3.0/tests/unit/__init__.py +0 -0
  38. lib_ledger_core-0.3.0/tests/unit/test_adapters_registry.py +45 -0
  39. lib_ledger_core-0.3.0/tests/unit/test_ddl.py +59 -0
  40. lib_ledger_core-0.3.0/tests/unit/test_exceptions.py +47 -0
  41. lib_ledger_core-0.3.0/tests/unit/test_migrations.py +128 -0
  42. lib_ledger_core-0.3.0/tests/unit/test_models.py +70 -0
  43. lib_ledger_core-0.3.0/tests/unit/test_schema.py +45 -0
@@ -0,0 +1,61 @@
1
+ # System files
2
+ .DS_Store
3
+ Thumbs.db
4
+ .history
5
+
6
+ # IDEs and Editors
7
+ .vscode/
8
+ .idea/
9
+ *.swp
10
+ *.swo
11
+ .antigravitycli/
12
+
13
+ # Tigerbeetle test data
14
+ .tigerbeetle
15
+
16
+ # --- uv / Virtual Environments ---
17
+ .venv/
18
+ .uv/
19
+ # uv.lock <-- Keep this UNLESS you have a specific reason to ignore it
20
+
21
+ # --- Python ---
22
+ __pycache__/
23
+ *.py[cod]
24
+ *$py.class
25
+ dist/
26
+ build/
27
+ *.egg-info/
28
+ # Allow committing .gitkeep in frontend dist folders to satisfy Hatchling forced includes
29
+ !**/frontend/dist/.gitkeep
30
+ *.spec
31
+
32
+ # --- Tooling ---
33
+ .pytest_cache/
34
+ .ruff_cache/
35
+ .mypy_cache/
36
+ .coverage
37
+ node_modules/
38
+ **/.m/
39
+ .pnpm-store/
40
+
41
+ # --- Secrets ---
42
+ .env
43
+ .env.*
44
+ !.env.example
45
+ dev.db
46
+
47
+ # --- Rust & Tauri ---
48
+ **/src-tauri/target/
49
+ **/src-tauri/bin/*
50
+ !**/src-tauri/bin/.gitkeep
51
+ **/src-tauri/lib/
52
+ **/src-tauri/share/
53
+ temp_sidecars/
54
+
55
+ # --- Local Development Symlinks ---
56
+ apps/business-m/src/business_m/static
57
+
58
+ # --- Generated Documentation (CI/CD Artifacts) ---
59
+ website/public/docs/machine/
60
+ website/src/content/docs/developer/generated/
61
+ .aider*
@@ -0,0 +1,53 @@
1
+ ledger-core:lint:
2
+ stage: lint
3
+ image: python:3.12
4
+ script:
5
+ - pip install uv
6
+ - cd libs/ledger-core
7
+ - uv sync --all-packages --all-extras --all-groups
8
+ - uv tool run ruff check .
9
+ - uv tool run ruff format --check .
10
+ - uv run mypy ledger_core --strict
11
+ rules:
12
+ - if: $CI_COMMIT_MESSAGE =~ /^chore\(release\):/
13
+ when: never
14
+ - if: $CI_COMMIT_BRANCH =~ /^release\/.*/
15
+ when: never
16
+ - if: $CI_MERGE_REQUEST_SOURCE_BRANCH_NAME =~ /^release\/.*/
17
+ when: never
18
+ - if: $CI_COMMIT_TAG
19
+ when: never
20
+ - changes:
21
+ - libs/ledger-core/ledger_core/**/*
22
+ - libs/ledger-core/tests/**/*
23
+ - libs/ledger-core/pyproject.toml
24
+
25
+ ledger-core:test:
26
+ stage: test
27
+ image: python:3.12
28
+ needs: []
29
+ services:
30
+ - name: docker:dind
31
+ command: ["--tls=false"]
32
+ variables:
33
+ DOCKER_HOST: tcp://docker:2375
34
+ DOCKER_TLS_CERTDIR: ""
35
+ DOCKER_DRIVER: overlay2
36
+ script:
37
+ - pip install uv
38
+ - cd libs/ledger-core
39
+ - uv sync --all-packages --all-extras --all-groups
40
+ - uv run pytest --cov=ledger_core tests
41
+ rules:
42
+ - if: $CI_COMMIT_MESSAGE =~ /^chore\(release\):/
43
+ when: never
44
+ - if: $CI_COMMIT_BRANCH =~ /^release\/.*/
45
+ when: never
46
+ - if: $CI_MERGE_REQUEST_SOURCE_BRANCH_NAME =~ /^release\/.*/
47
+ when: never
48
+ - if: $CI_COMMIT_TAG
49
+ when: never
50
+ - changes:
51
+ - libs/ledger-core/ledger_core/**/*
52
+ - libs/ledger-core/tests/**/*
53
+ - libs/ledger-core/pyproject.toml
@@ -0,0 +1,42 @@
1
+ ## ledger-core v0.3.0
2
+
3
+ ### Features
4
+
5
+ - publish lib with documentation (df7e378)
6
+ - rename published package from ledger-core to lib-ledger-core (00ce765)
7
+
8
+ ## ledger-core v0.2.3
9
+
10
+ ### Bug Fixes
11
+
12
+ - release lib-ledger-core (1b1fa2c)
13
+
14
+ ## ledger-core v0.2.2
15
+
16
+ ### Bug Fixes
17
+
18
+ - serialize json parameters correctly across db dialects (9978aa9)
19
+
20
+ ## ledger-core v0.2.1
21
+
22
+ ### Bug Fixes
23
+
24
+ - alembic.ini packing location (ca78963)
25
+
26
+ ## ledger-core v0.2.0
27
+
28
+ ### Features
29
+
30
+ - introduce generic ledger and event-store primitives (327760f)
31
+
32
+ ### Bug Fixes
33
+
34
+ - missing framework app entry point (b7cf7b2)
35
+ - remove ledger-level insufficient balance constraints (9b7092f)
36
+ - update core adapter interfaces for TigerBeetle and SQLAlchemy (0817e49)
37
+ - use kurrentdbclient and aiomysql for mysql/mariadb extras (47a4658)
38
+ - final unified CD DAG release pipeline (5beb5ea)
39
+ - decouple builds from helm and secure OCI credentials (d0c9e2f)
40
+ - resolve package registry (ad30d6c)
41
+ - resolve package registry URL dynamically, optimize builds, and skip redundant prepare jobs on tags (b6c9a50)
42
+
@@ -0,0 +1,267 @@
1
+ Metadata-Version: 2.5
2
+ Name: lib-ledger-core
3
+ Version: 0.3.0
4
+ Summary: Generic ledger primitives and event-store ports for Business-M keepers
5
+ Author: Business M Contributors
6
+ Requires-Python: >=3.12
7
+ Requires-Dist: alembic>=1.19.1
8
+ Requires-Dist: msgspec>=0.21.1
9
+ Requires-Dist: sqlalchemy[asyncio]>=2.0.51
10
+ Provides-Extra: kurrent
11
+ Requires-Dist: kurrentdbclient>=1.3.3; extra == 'kurrent'
12
+ Provides-Extra: mariadb
13
+ Requires-Dist: aiomysql>=0.3.2; extra == 'mariadb'
14
+ Provides-Extra: mssql
15
+ Requires-Dist: aioodbc>=0.5.0; extra == 'mssql'
16
+ Provides-Extra: mysql
17
+ Requires-Dist: aiomysql>=0.3.2; extra == 'mysql'
18
+ Provides-Extra: oracle
19
+ Requires-Dist: oracledb>=4.0.2; extra == 'oracle'
20
+ Provides-Extra: postgres
21
+ Requires-Dist: asyncpg>=0.31.0; extra == 'postgres'
22
+ Provides-Extra: sqlite
23
+ Requires-Dist: aiosqlite>=0.22.1; extra == 'sqlite'
24
+ Provides-Extra: test
25
+ Requires-Dist: aiosqlite>=0.22.1; extra == 'test'
26
+ Requires-Dist: anyio>=4.14.2; extra == 'test'
27
+ Requires-Dist: asyncpg>=0.31.0; extra == 'test'
28
+ Requires-Dist: psycopg2-binary>=2.9.12; extra == 'test'
29
+ Requires-Dist: pytest-asyncio>=1.4.0; extra == 'test'
30
+ Requires-Dist: pytest-cov>=7.1.0; extra == 'test'
31
+ Requires-Dist: pytest>=9.1.1; extra == 'test'
32
+ Requires-Dist: testcontainers[postgres]>=4.15.0; extra == 'test'
33
+ Provides-Extra: tigerbeetle
34
+ Requires-Dist: tigerbeetle>=0.17.3; extra == 'tigerbeetle'
35
+ Description-Content-Type: text/markdown
36
+
37
+ # `lib-ledger-core`
38
+
39
+ `lib-ledger-core` is an extensible, asynchronous Python library that provides standard domain models, ports, and storage adapters for immutable double-entry accounting and event-sourced state tracking.
40
+
41
+ Built around the **Ports and Adapters (Hexagonal Architecture)** design pattern, `lib-ledger-core` decouples high-level business rules from storage drivers—allowing you to easily swap underlying databases, log engines, or external financial backends without altering domain logic.
42
+
43
+ ---
44
+
45
+ ## System Philosophy: CQRS & Event Sourcing
46
+
47
+ At its core, `lib-ledger-core` enforces a clean **Command-Query Responsibility Segregation (CQRS)** pattern coupled with **Event Sourcing**:
48
+
49
+ ```mermaid
50
+ flowchart TD
51
+ A["Incoming Instruction"] --> B
52
+
53
+ subgraph Command ["1. COMMAND / VALIDATION SIDE (Ledger Backend)"]
54
+ direction TB
55
+ B["Accepts TransferCommand Instructions"] --> C["Validates Domain Invariants<br><i>(Account Verification, Double-Entry Rules)</i>"]
56
+ C --> D["Commits State Transition & Returns Result"]
57
+ end
58
+
59
+ D -->|"Valid Transfer Result"| E
60
+
61
+ subgraph Query ["2. QUERY / READ SIDE (Event Store Backend)"]
62
+ direction TB
63
+ E["Receives Validated Ledger Execution State"] --> F["Appends Immutable Events to Streams<br><i>(Enforces Optimistic Concurrency Control)</i>"]
64
+ F --> G["Acts as Single Source of Truth<br><i>(Powers Projections & Aggregates)</i>"]
65
+ end
66
+
67
+ style Command fill:#1e1e2e,stroke:#89b4fa,stroke-width:2px,color:#cdd6f4
68
+ style Query fill:#1e1e2e,stroke:#a6e3a1,stroke-width:2px,color:#cdd6f4
69
+ style A fill:#313244,stroke:#f5e0dc,stroke-width:1px,color:#cdd6f4
70
+ ```
71
+
72
+ ### Why Event Sourced Aggregates?
73
+ 1. **Schema-less Aggregate Evolution**: Instead of modifying SQL relational table schemas whenever business rules change, domain aggregates (such as account balances, tenant summaries, or risk profiles) are projected directly by reading and replaying historical event streams. Adding new aggregate views requires no database migrations.
74
+ 2. **Complete Auditability**: Traditional database updates overwrite previous state. The event store retains every historical event sequentially, providing a permanent, tamper-evident audit trail required for compliance and financial reconciliation.
75
+ 3. **Optimistic Concurrency Control (OCC)**: Event appending guarantees that concurrent attempts to modify the same stream are safely rejected if the sequence version changes unexpectedly.
76
+
77
+ ---
78
+
79
+ ## Universal Balance Tracking: Use Cases
80
+
81
+ While designed to handle double-entry financial bookkeeping and warehouse inventory movements for ERP environments, the transfer command abstraction fits any system that moves units between two balance states:
82
+
83
+ * **Financial & Double-Entry Bookkeeping**: Manages credits and debits across general ledger accounts, revenue tracking, and accounts payable.
84
+ * **Stock Keeping & Warehouse Movements**: Validates and records inventory transfers between physical warehouses, storage bins, or supply chain nodes.
85
+ * **Crypto & Coin Wallets**: Manages balances, gas fees, and token movements between user wallets, cold storage, and hot pools.
86
+ * **Carbon Credits & Offsets**: Tracks issuance, transfer, and retirement of verified metric tons of carbon emissions ($tCO_2e$) between reserves and corporate accounts.
87
+ * **Loyalty Points & Rewards**: Handles issuance, transfers, holds, and redemptions of promotional rewards points.
88
+ * **Compute & API Quotas**: Controls consumption, allocation, and rate-limiting credits for multi-tenant microservices.
89
+
90
+ ---
91
+
92
+ ## Core Architectural Components
93
+
94
+ ### 1. Abstract Ports (Interfaces)
95
+ * **`LedgerPort` (Validation & Command Side)**: Defines operations for evaluating transfer instructions, checking current balance snapshots, processing pending holds, and executing multi-leg compound movements.
96
+ * **`EventStorePort` (Audit & Read Side)**: Defines append-only operations for persisting versioned event streams and querying stream history.
97
+
98
+ ### 2. Data Models
99
+ * **`TransferCommand`**: Immutable instruction specifying debit/credit target accounts, transaction reference, metadata, pending status, and multi-leg transfer definitions.
100
+ * **`Entry`**: Immutable transaction record representing an individual debit or credit line item.
101
+
102
+ ### 3. Backend Adapters
103
+ * **`SqlAlchemyLedger` & `SqlAlchemyEventStore`**: Relational backends utilizing async SQL engines to process transfers and maintain versioned JSON event streams.
104
+ * **`TigerBeetleLedger`**: High-throughput ledger integration utilizing TigerBeetle for low-latency balance tracking and account flags.
105
+ * **`KurrentEventStore`**: Event-sourcing integration built on top of KurrentDB (EventStoreDB) utilizing `msgspec` for fast serialization.
106
+
107
+ ---
108
+
109
+ ## Dynamic Adapter Registry
110
+
111
+ Custom adapters can be created by implementing either the `LedgerPort` or `EventStorePort` interface. Third-party modules can register their custom drivers using Python entry-point groups (`ledger_core.ledger` and `ledger_core.event_store`). Once registered, the core library can dynamically discover and load adapters at runtime.
112
+
113
+ ---
114
+
115
+ ## Exception & Error Hierarchy
116
+
117
+ * **`LedgerError`**: Base exception class for all errors generated by the library.
118
+ * **`InsufficientBalanceError`**: Raised when a transfer command violates non-negative balance constraints.
119
+ * **`OccError`**: Raised when a stream version mismatch occurs during an append operation to the event store.
120
+
121
+ # Using `lib-ledger-core`
122
+
123
+ The `lib-ledger-core` library provides generic ledger primitives, event sourcing ports, dynamic adapter registration, and programmatic database migrations.
124
+
125
+ ## 1. Database Migrations Programmatically
126
+
127
+ `lib-ledger-core` encapsulates its Alembic migration scripts internally. Higher-level applications like `book-keeper` use the `run_migrations` helper to initialize or upgrade the schema without maintaining duplicate SQL migration files:
128
+
129
+ ```python
130
+ import asyncio
131
+ from ledger_core.migrations import run_downgrade, run_migrations
132
+ from sqlalchemy.ext.asyncio import create_async_engine
133
+
134
+ DATABASE_URL = "postgresql+asyncpg://postgres:postgres@localhost:5432/book_keeper"
135
+
136
+
137
+ async def setup_database():
138
+ engine = create_async_engine(DATABASE_URL, future=True)
139
+
140
+ # Run all pending ledger-core Alembic migrations
141
+ await run_migrations(engine)
142
+
143
+ # ... application setup ...
144
+
145
+ await engine.dispose()
146
+
147
+
148
+ if __name__ == "__main__":
149
+ asyncio.run(setup_database())
150
+ ```
151
+
152
+ ---
153
+
154
+ ## 2. Basic Ledger & Event Store Usage
155
+
156
+ `lib-ledger-core` exposes `LedgerPort` and `EventStorePort` implementations (such as `SqlAlchemyLedger`, `TigerBeetleLedger`, `SqlAlchemyEventStore`, and `KurrentEventStore`).
157
+
158
+ ```python
159
+ import asyncio
160
+ from decimal import Decimal
161
+ from ledger_core.adapters.sqlalchemy import SqlAlchemyEventStore, SqlAlchemyLedger
162
+ from ledger_core.models import TransferCommand
163
+ from sqlalchemy.ext.asyncio import create_async_engine
164
+
165
+
166
+ async def main():
167
+ engine = create_async_engine("sqlite+aiosqlite:///:memory:", future=True)
168
+
169
+ # Instantiate Adapters
170
+ ledger = SqlAlchemyLedger(engine)
171
+ event_store = SqlAlchemyEventStore(engine)
172
+
173
+ # Seed an account with initial funds
174
+ await ledger.seed_account(
175
+ tenant_id="tenant_1", account="CASH", amount=Decimal("1000.00")
176
+ )
177
+
178
+ # Execute a Transfer
179
+ cmd = TransferCommand(
180
+ tenant_id="tenant_1",
181
+ debit_account="EQUIPMENT",
182
+ credit_account="CASH",
183
+ amount=Decimal("250.00"),
184
+ reference="INV-2026-001",
185
+ description="Purchased office equipment",
186
+ )
187
+ transfer_id = await ledger.transfer(cmd)
188
+ print(f"Executed Transfer ID: {transfer_id}")
189
+
190
+ # Record Domain Event
191
+ await event_store.append(
192
+ tenant_id="tenant_1",
193
+ stream_id="equipment-purchases",
194
+ events=[
195
+ {
196
+ "type": "EquipmentPurchased",
197
+ "transfer_id": transfer_id,
198
+ "amount": "250.00",
199
+ }
200
+ ],
201
+ expected_version=0,
202
+ )
203
+
204
+ # Check Balances
205
+ cash_bal = await ledger.get_balance("tenant_1", "CASH")
206
+ equipment_bal = await ledger.get_balance("tenant_1", "EQUIPMENT")
207
+ print(f"CASH Balance: {cash_bal}") # Outputs: 750.00
208
+ print(f"EQUIPMENT Balance: {equipment_bal}") # Outputs: 250.00
209
+
210
+ await ledger.close()
211
+ await event_store.close()
212
+
213
+
214
+ if __name__ == "__main__":
215
+ asyncio.run(main())
216
+ ```
217
+
218
+ ---
219
+
220
+ ## 3. Loading Adapters via Registry
221
+
222
+ Adapters can also be loaded dynamically using the entry-point registry:
223
+
224
+ ```python
225
+ from ledger_core import load_event_store_adapter, load_ledger_adapter
226
+ from sqlalchemy.ext.asyncio import create_async_engine
227
+
228
+ # Dynamically resolve factories using entry-point identifiers
229
+ ledger_factory = load_ledger_adapter("sqlalchemy")
230
+ event_store_factory = load_event_store_adapter("kurrent")
231
+
232
+ engine = create_async_engine("postgresql+asyncpg://...")
233
+ ledger = ledger_factory(engine)
234
+ event_store = event_store_factory("esdb://localhost:2113?tls=false")
235
+ ```
236
+
237
+ ---
238
+
239
+ ## 4. Application Integration (`book-keeper` example)
240
+
241
+ Inside `book-keeper`, `lib-ledger-core` adapters are conditionally selected during application startup based on settings:
242
+
243
+ ```python
244
+ from ledger_core.adapters.kurrent import KurrentEventStore
245
+ from ledger_core.adapters.sqlalchemy import SqlAlchemyEventStore, SqlAlchemyLedger
246
+ from ledger_core.adapters.tigerbeetle import TigerBeetleLedger
247
+ from ledger_core.interfaces import EventStorePort, LedgerPort
248
+
249
+
250
+ def create_ledger(engine, settings) -> LedgerPort:
251
+ if settings.ledger_type == "postgres":
252
+ return SqlAlchemyLedger(engine)
253
+ return TigerBeetleLedger(
254
+ addresses=settings.tigerbeetle_addresses,
255
+ account_namespace=settings.account_namespace,
256
+ )
257
+
258
+
259
+ def create_event_store(engine, settings) -> EventStorePort:
260
+ if settings.event_store_type == "postgres":
261
+ return SqlAlchemyEventStore(engine)
262
+ return KurrentEventStore(connection_string=settings.kurrent_connection_string)
263
+ ```
264
+
265
+ # License
266
+
267
+ Apache 2.0
@@ -0,0 +1,231 @@
1
+ # `lib-ledger-core`
2
+
3
+ `lib-ledger-core` is an extensible, asynchronous Python library that provides standard domain models, ports, and storage adapters for immutable double-entry accounting and event-sourced state tracking.
4
+
5
+ Built around the **Ports and Adapters (Hexagonal Architecture)** design pattern, `lib-ledger-core` decouples high-level business rules from storage drivers—allowing you to easily swap underlying databases, log engines, or external financial backends without altering domain logic.
6
+
7
+ ---
8
+
9
+ ## System Philosophy: CQRS & Event Sourcing
10
+
11
+ At its core, `lib-ledger-core` enforces a clean **Command-Query Responsibility Segregation (CQRS)** pattern coupled with **Event Sourcing**:
12
+
13
+ ```mermaid
14
+ flowchart TD
15
+ A["Incoming Instruction"] --> B
16
+
17
+ subgraph Command ["1. COMMAND / VALIDATION SIDE (Ledger Backend)"]
18
+ direction TB
19
+ B["Accepts TransferCommand Instructions"] --> C["Validates Domain Invariants<br><i>(Account Verification, Double-Entry Rules)</i>"]
20
+ C --> D["Commits State Transition & Returns Result"]
21
+ end
22
+
23
+ D -->|"Valid Transfer Result"| E
24
+
25
+ subgraph Query ["2. QUERY / READ SIDE (Event Store Backend)"]
26
+ direction TB
27
+ E["Receives Validated Ledger Execution State"] --> F["Appends Immutable Events to Streams<br><i>(Enforces Optimistic Concurrency Control)</i>"]
28
+ F --> G["Acts as Single Source of Truth<br><i>(Powers Projections & Aggregates)</i>"]
29
+ end
30
+
31
+ style Command fill:#1e1e2e,stroke:#89b4fa,stroke-width:2px,color:#cdd6f4
32
+ style Query fill:#1e1e2e,stroke:#a6e3a1,stroke-width:2px,color:#cdd6f4
33
+ style A fill:#313244,stroke:#f5e0dc,stroke-width:1px,color:#cdd6f4
34
+ ```
35
+
36
+ ### Why Event Sourced Aggregates?
37
+ 1. **Schema-less Aggregate Evolution**: Instead of modifying SQL relational table schemas whenever business rules change, domain aggregates (such as account balances, tenant summaries, or risk profiles) are projected directly by reading and replaying historical event streams. Adding new aggregate views requires no database migrations.
38
+ 2. **Complete Auditability**: Traditional database updates overwrite previous state. The event store retains every historical event sequentially, providing a permanent, tamper-evident audit trail required for compliance and financial reconciliation.
39
+ 3. **Optimistic Concurrency Control (OCC)**: Event appending guarantees that concurrent attempts to modify the same stream are safely rejected if the sequence version changes unexpectedly.
40
+
41
+ ---
42
+
43
+ ## Universal Balance Tracking: Use Cases
44
+
45
+ While designed to handle double-entry financial bookkeeping and warehouse inventory movements for ERP environments, the transfer command abstraction fits any system that moves units between two balance states:
46
+
47
+ * **Financial & Double-Entry Bookkeeping**: Manages credits and debits across general ledger accounts, revenue tracking, and accounts payable.
48
+ * **Stock Keeping & Warehouse Movements**: Validates and records inventory transfers between physical warehouses, storage bins, or supply chain nodes.
49
+ * **Crypto & Coin Wallets**: Manages balances, gas fees, and token movements between user wallets, cold storage, and hot pools.
50
+ * **Carbon Credits & Offsets**: Tracks issuance, transfer, and retirement of verified metric tons of carbon emissions ($tCO_2e$) between reserves and corporate accounts.
51
+ * **Loyalty Points & Rewards**: Handles issuance, transfers, holds, and redemptions of promotional rewards points.
52
+ * **Compute & API Quotas**: Controls consumption, allocation, and rate-limiting credits for multi-tenant microservices.
53
+
54
+ ---
55
+
56
+ ## Core Architectural Components
57
+
58
+ ### 1. Abstract Ports (Interfaces)
59
+ * **`LedgerPort` (Validation & Command Side)**: Defines operations for evaluating transfer instructions, checking current balance snapshots, processing pending holds, and executing multi-leg compound movements.
60
+ * **`EventStorePort` (Audit & Read Side)**: Defines append-only operations for persisting versioned event streams and querying stream history.
61
+
62
+ ### 2. Data Models
63
+ * **`TransferCommand`**: Immutable instruction specifying debit/credit target accounts, transaction reference, metadata, pending status, and multi-leg transfer definitions.
64
+ * **`Entry`**: Immutable transaction record representing an individual debit or credit line item.
65
+
66
+ ### 3. Backend Adapters
67
+ * **`SqlAlchemyLedger` & `SqlAlchemyEventStore`**: Relational backends utilizing async SQL engines to process transfers and maintain versioned JSON event streams.
68
+ * **`TigerBeetleLedger`**: High-throughput ledger integration utilizing TigerBeetle for low-latency balance tracking and account flags.
69
+ * **`KurrentEventStore`**: Event-sourcing integration built on top of KurrentDB (EventStoreDB) utilizing `msgspec` for fast serialization.
70
+
71
+ ---
72
+
73
+ ## Dynamic Adapter Registry
74
+
75
+ Custom adapters can be created by implementing either the `LedgerPort` or `EventStorePort` interface. Third-party modules can register their custom drivers using Python entry-point groups (`ledger_core.ledger` and `ledger_core.event_store`). Once registered, the core library can dynamically discover and load adapters at runtime.
76
+
77
+ ---
78
+
79
+ ## Exception & Error Hierarchy
80
+
81
+ * **`LedgerError`**: Base exception class for all errors generated by the library.
82
+ * **`InsufficientBalanceError`**: Raised when a transfer command violates non-negative balance constraints.
83
+ * **`OccError`**: Raised when a stream version mismatch occurs during an append operation to the event store.
84
+
85
+ # Using `lib-ledger-core`
86
+
87
+ The `lib-ledger-core` library provides generic ledger primitives, event sourcing ports, dynamic adapter registration, and programmatic database migrations.
88
+
89
+ ## 1. Database Migrations Programmatically
90
+
91
+ `lib-ledger-core` encapsulates its Alembic migration scripts internally. Higher-level applications like `book-keeper` use the `run_migrations` helper to initialize or upgrade the schema without maintaining duplicate SQL migration files:
92
+
93
+ ```python
94
+ import asyncio
95
+ from ledger_core.migrations import run_downgrade, run_migrations
96
+ from sqlalchemy.ext.asyncio import create_async_engine
97
+
98
+ DATABASE_URL = "postgresql+asyncpg://postgres:postgres@localhost:5432/book_keeper"
99
+
100
+
101
+ async def setup_database():
102
+ engine = create_async_engine(DATABASE_URL, future=True)
103
+
104
+ # Run all pending ledger-core Alembic migrations
105
+ await run_migrations(engine)
106
+
107
+ # ... application setup ...
108
+
109
+ await engine.dispose()
110
+
111
+
112
+ if __name__ == "__main__":
113
+ asyncio.run(setup_database())
114
+ ```
115
+
116
+ ---
117
+
118
+ ## 2. Basic Ledger & Event Store Usage
119
+
120
+ `lib-ledger-core` exposes `LedgerPort` and `EventStorePort` implementations (such as `SqlAlchemyLedger`, `TigerBeetleLedger`, `SqlAlchemyEventStore`, and `KurrentEventStore`).
121
+
122
+ ```python
123
+ import asyncio
124
+ from decimal import Decimal
125
+ from ledger_core.adapters.sqlalchemy import SqlAlchemyEventStore, SqlAlchemyLedger
126
+ from ledger_core.models import TransferCommand
127
+ from sqlalchemy.ext.asyncio import create_async_engine
128
+
129
+
130
+ async def main():
131
+ engine = create_async_engine("sqlite+aiosqlite:///:memory:", future=True)
132
+
133
+ # Instantiate Adapters
134
+ ledger = SqlAlchemyLedger(engine)
135
+ event_store = SqlAlchemyEventStore(engine)
136
+
137
+ # Seed an account with initial funds
138
+ await ledger.seed_account(
139
+ tenant_id="tenant_1", account="CASH", amount=Decimal("1000.00")
140
+ )
141
+
142
+ # Execute a Transfer
143
+ cmd = TransferCommand(
144
+ tenant_id="tenant_1",
145
+ debit_account="EQUIPMENT",
146
+ credit_account="CASH",
147
+ amount=Decimal("250.00"),
148
+ reference="INV-2026-001",
149
+ description="Purchased office equipment",
150
+ )
151
+ transfer_id = await ledger.transfer(cmd)
152
+ print(f"Executed Transfer ID: {transfer_id}")
153
+
154
+ # Record Domain Event
155
+ await event_store.append(
156
+ tenant_id="tenant_1",
157
+ stream_id="equipment-purchases",
158
+ events=[
159
+ {
160
+ "type": "EquipmentPurchased",
161
+ "transfer_id": transfer_id,
162
+ "amount": "250.00",
163
+ }
164
+ ],
165
+ expected_version=0,
166
+ )
167
+
168
+ # Check Balances
169
+ cash_bal = await ledger.get_balance("tenant_1", "CASH")
170
+ equipment_bal = await ledger.get_balance("tenant_1", "EQUIPMENT")
171
+ print(f"CASH Balance: {cash_bal}") # Outputs: 750.00
172
+ print(f"EQUIPMENT Balance: {equipment_bal}") # Outputs: 250.00
173
+
174
+ await ledger.close()
175
+ await event_store.close()
176
+
177
+
178
+ if __name__ == "__main__":
179
+ asyncio.run(main())
180
+ ```
181
+
182
+ ---
183
+
184
+ ## 3. Loading Adapters via Registry
185
+
186
+ Adapters can also be loaded dynamically using the entry-point registry:
187
+
188
+ ```python
189
+ from ledger_core import load_event_store_adapter, load_ledger_adapter
190
+ from sqlalchemy.ext.asyncio import create_async_engine
191
+
192
+ # Dynamically resolve factories using entry-point identifiers
193
+ ledger_factory = load_ledger_adapter("sqlalchemy")
194
+ event_store_factory = load_event_store_adapter("kurrent")
195
+
196
+ engine = create_async_engine("postgresql+asyncpg://...")
197
+ ledger = ledger_factory(engine)
198
+ event_store = event_store_factory("esdb://localhost:2113?tls=false")
199
+ ```
200
+
201
+ ---
202
+
203
+ ## 4. Application Integration (`book-keeper` example)
204
+
205
+ Inside `book-keeper`, `lib-ledger-core` adapters are conditionally selected during application startup based on settings:
206
+
207
+ ```python
208
+ from ledger_core.adapters.kurrent import KurrentEventStore
209
+ from ledger_core.adapters.sqlalchemy import SqlAlchemyEventStore, SqlAlchemyLedger
210
+ from ledger_core.adapters.tigerbeetle import TigerBeetleLedger
211
+ from ledger_core.interfaces import EventStorePort, LedgerPort
212
+
213
+
214
+ def create_ledger(engine, settings) -> LedgerPort:
215
+ if settings.ledger_type == "postgres":
216
+ return SqlAlchemyLedger(engine)
217
+ return TigerBeetleLedger(
218
+ addresses=settings.tigerbeetle_addresses,
219
+ account_namespace=settings.account_namespace,
220
+ )
221
+
222
+
223
+ def create_event_store(engine, settings) -> EventStorePort:
224
+ if settings.event_store_type == "postgres":
225
+ return SqlAlchemyEventStore(engine)
226
+ return KurrentEventStore(connection_string=settings.kurrent_connection_string)
227
+ ```
228
+
229
+ # License
230
+
231
+ Apache 2.0
@@ -0,0 +1,41 @@
1
+ [alembic]
2
+ script_location = ledger_core/migrations
3
+ prepend_sys_path = .
4
+ path_separator = os
5
+ version_path_separator = os
6
+
7
+ [post_write_hooks]
8
+
9
+ [loggers]
10
+ keys = root,sqlalchemy,alembic
11
+
12
+ [handlers]
13
+ keys = console
14
+
15
+ [formatters]
16
+ keys = generic
17
+
18
+ [logger_root]
19
+ level = WARN
20
+ handlers = console
21
+ qualname =
22
+
23
+ [logger_sqlalchemy]
24
+ level = WARN
25
+ handlers =
26
+ qualname = sqlalchemy.engine
27
+
28
+ [logger_alembic]
29
+ level = INFO
30
+ handlers =
31
+ qualname = alembic
32
+
33
+ [handler_console]
34
+ class = StreamHandler
35
+ args = (sys.stderr,)
36
+ level = NOTSET
37
+ formatter = generic
38
+
39
+ [formatter_generic]
40
+ format = %(levelname)-5.5s [%(name)s] %(message)s
41
+ datefmt = %H:%M:%S