loadledger 0.1.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.
- loadledger-0.1.0/.editorconfig +23 -0
- loadledger-0.1.0/.github/workflows/ci.yml +167 -0
- loadledger-0.1.0/.github/workflows/release.yml +57 -0
- loadledger-0.1.0/.gitignore +104 -0
- loadledger-0.1.0/.importlinter +51 -0
- loadledger-0.1.0/.pre-commit-config.yaml +22 -0
- loadledger-0.1.0/CHANGELOG.md +92 -0
- loadledger-0.1.0/CONTRIBUTING.md +50 -0
- loadledger-0.1.0/LICENSE +201 -0
- loadledger-0.1.0/PKG-INFO +212 -0
- loadledger-0.1.0/README.md +179 -0
- loadledger-0.1.0/SECURITY.md +41 -0
- loadledger-0.1.0/docs/README.md +17 -0
- loadledger-0.1.0/docs/mounted-table-upgrades.md +170 -0
- loadledger-0.1.0/docs/packages/loadledger/development-plan.md +107 -0
- loadledger-0.1.0/docs/packages/loadledger/spec.md +330 -0
- loadledger-0.1.0/docs/quickstart.md +176 -0
- loadledger-0.1.0/docs/quickstart.py +135 -0
- loadledger-0.1.0/pyproject.toml +115 -0
- loadledger-0.1.0/requirements/README.md +58 -0
- loadledger-0.1.0/requirements/ci.lock +816 -0
- loadledger-0.1.0/requirements/release.in +6 -0
- loadledger-0.1.0/requirements/release.lock +493 -0
- loadledger-0.1.0/src/loadledger/__about__.py +1 -0
- loadledger-0.1.0/src/loadledger/__init__.py +77 -0
- loadledger-0.1.0/src/loadledger/core.py +668 -0
- loadledger-0.1.0/src/loadledger/errors.py +86 -0
- loadledger-0.1.0/src/loadledger/memory.py +246 -0
- loadledger-0.1.0/src/loadledger/py.typed +0 -0
- loadledger-0.1.0/src/loadledger/sql.py +980 -0
- loadledger-0.1.0/src/loadledger/types.py +447 -0
- loadledger-0.1.0/tests/conftest.py +214 -0
- loadledger-0.1.0/tests/integration/hostapp/__init__.py +10 -0
- loadledger-0.1.0/tests/integration/hostapp/migrations/env.py +48 -0
- loadledger-0.1.0/tests/integration/hostapp/migrations/script.py.mako +28 -0
- loadledger-0.1.0/tests/integration/hostapp/migrations/versions/.gitkeep +0 -0
- loadledger-0.1.0/tests/integration/hostapp/models.py +33 -0
- loadledger-0.1.0/tests/integration/ledger_subprocess.py +190 -0
- loadledger-0.1.0/tests/integration/test_atomicity.py +180 -0
- loadledger-0.1.0/tests/integration/test_concurrency.py +193 -0
- loadledger-0.1.0/tests/integration/test_hostapp.py +293 -0
- loadledger-0.1.0/tests/integration/test_mounting.py +237 -0
- loadledger-0.1.0/tests/integration/test_quickstart.py +67 -0
- loadledger-0.1.0/tests/integration/test_sql_ledger.py +533 -0
- loadledger-0.1.0/tests/performance/test_scaling.py +71 -0
- loadledger-0.1.0/tests/performance/test_sql_scaling.py +164 -0
- loadledger-0.1.0/tests/unit/test_memory_ledger.py +391 -0
- loadledger-0.1.0/tests/unit/test_types.py +257 -0
- loadledger-0.1.0/tests/unit/test_verdicts.py +626 -0
- loadledger-0.1.0/tests/unit/test_windows.py +215 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
root = true
|
|
2
|
+
|
|
3
|
+
[*]
|
|
4
|
+
charset = utf-8
|
|
5
|
+
end_of_line = lf
|
|
6
|
+
insert_final_newline = true
|
|
7
|
+
trim_trailing_whitespace = true
|
|
8
|
+
indent_style = space
|
|
9
|
+
indent_size = 4
|
|
10
|
+
|
|
11
|
+
[*.py]
|
|
12
|
+
indent_size = 4
|
|
13
|
+
max_line_length = 100
|
|
14
|
+
|
|
15
|
+
[*.{toml,yml,yaml,json}]
|
|
16
|
+
indent_size = 2
|
|
17
|
+
|
|
18
|
+
[*.md]
|
|
19
|
+
trim_trailing_whitespace = false
|
|
20
|
+
max_line_length = off
|
|
21
|
+
|
|
22
|
+
[Makefile]
|
|
23
|
+
indent_style = tab
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
format:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/checkout@v4
|
|
13
|
+
- uses: actions/setup-python@v5
|
|
14
|
+
with: { python-version: "3.12" }
|
|
15
|
+
- run: pip install ruff
|
|
16
|
+
- run: ruff format --check .
|
|
17
|
+
|
|
18
|
+
lint:
|
|
19
|
+
runs-on: ubuntu-latest
|
|
20
|
+
steps:
|
|
21
|
+
- uses: actions/checkout@v4
|
|
22
|
+
- uses: actions/setup-python@v5
|
|
23
|
+
with: { python-version: "3.12" }
|
|
24
|
+
- run: pip install ruff
|
|
25
|
+
- run: ruff check .
|
|
26
|
+
|
|
27
|
+
types:
|
|
28
|
+
runs-on: ubuntu-latest
|
|
29
|
+
steps:
|
|
30
|
+
- uses: actions/checkout@v4
|
|
31
|
+
- uses: actions/setup-python@v5
|
|
32
|
+
with: { python-version: "3.12" }
|
|
33
|
+
- run: pip install --require-hashes -r requirements/ci.lock
|
|
34
|
+
- run: pip install . --no-deps
|
|
35
|
+
- run: mypy src tests
|
|
36
|
+
|
|
37
|
+
boundaries:
|
|
38
|
+
runs-on: ubuntu-latest
|
|
39
|
+
steps:
|
|
40
|
+
- uses: actions/checkout@v4
|
|
41
|
+
- uses: actions/setup-python@v5
|
|
42
|
+
with: { python-version: "3.12" }
|
|
43
|
+
- run: pip install --require-hashes -r requirements/ci.lock
|
|
44
|
+
- run: pip install . --no-deps
|
|
45
|
+
- run: lint-imports
|
|
46
|
+
|
|
47
|
+
tests:
|
|
48
|
+
runs-on: ubuntu-latest
|
|
49
|
+
strategy:
|
|
50
|
+
matrix:
|
|
51
|
+
python-version: ["3.12", "3.13"]
|
|
52
|
+
steps:
|
|
53
|
+
- uses: actions/checkout@v4
|
|
54
|
+
- uses: actions/setup-python@v5
|
|
55
|
+
with: { python-version: "${{ matrix.python-version }}" }
|
|
56
|
+
- run: pip install --require-hashes -r requirements/ci.lock
|
|
57
|
+
- run: pip install . --no-deps
|
|
58
|
+
- run: pytest -m "not live and not performance" --cov --cov-report=xml
|
|
59
|
+
|
|
60
|
+
tests-314-early-warning:
|
|
61
|
+
runs-on: ubuntu-latest
|
|
62
|
+
continue-on-error: true
|
|
63
|
+
steps:
|
|
64
|
+
- uses: actions/checkout@v4
|
|
65
|
+
- uses: actions/setup-python@v5
|
|
66
|
+
with: { python-version: "3.14" }
|
|
67
|
+
# Deliberately unpinned: ci.lock is resolved on 3.13, and pinning a version that has no
|
|
68
|
+
# 3.14 wheels would defeat the purpose of an early warning.
|
|
69
|
+
- run: pip install -e ".[dev]"
|
|
70
|
+
- run: pytest -m "not live and not performance"
|
|
71
|
+
|
|
72
|
+
db-matrix:
|
|
73
|
+
name: tests (PostgreSQL)
|
|
74
|
+
runs-on: ubuntu-latest
|
|
75
|
+
services:
|
|
76
|
+
postgres:
|
|
77
|
+
image: postgres:16
|
|
78
|
+
# Credentials and database name match `DEFAULT_POSTGRES_URL` in tests/conftest.py, so the
|
|
79
|
+
# service this job starts is the one the integration tests look for.
|
|
80
|
+
env:
|
|
81
|
+
POSTGRES_USER: loadledger
|
|
82
|
+
POSTGRES_PASSWORD: loadledger
|
|
83
|
+
POSTGRES_DB: loadledger_test
|
|
84
|
+
ports: ["5432:5432"]
|
|
85
|
+
options: >-
|
|
86
|
+
--health-cmd "pg_isready -U loadledger -d loadledger_test" --health-interval 5s --health-timeout 5s --health-retries 10
|
|
87
|
+
steps:
|
|
88
|
+
- uses: actions/checkout@v4
|
|
89
|
+
- uses: actions/setup-python@v5
|
|
90
|
+
with: { python-version: "3.12" }
|
|
91
|
+
- run: pip install --require-hashes -r requirements/ci.lock
|
|
92
|
+
- run: pip install . --no-deps
|
|
93
|
+
- run: pytest -m "not live and not performance" tests/integration
|
|
94
|
+
env:
|
|
95
|
+
# Without this the PostgreSQL legs would *skip* here exactly as they do on a developer's
|
|
96
|
+
# machine, and the job would go green having tested one dialect twice. A skipped dialect
|
|
97
|
+
# is an untested dialect.
|
|
98
|
+
LOADLEDGER_REQUIRE_POSTGRES: "1"
|
|
99
|
+
# The tests read LOADLEDGER_POSTGRES_URL, not DATABASE_URL. Set explicitly rather than
|
|
100
|
+
# relying on the default, so the job states which server it is testing.
|
|
101
|
+
LOADLEDGER_POSTGRES_URL: postgresql+psycopg://loadledger:loadledger@localhost:5432/loadledger_test
|
|
102
|
+
|
|
103
|
+
coverage:
|
|
104
|
+
needs: [tests]
|
|
105
|
+
runs-on: ubuntu-latest
|
|
106
|
+
steps:
|
|
107
|
+
- uses: actions/checkout@v4
|
|
108
|
+
- uses: actions/setup-python@v5
|
|
109
|
+
with: { python-version: "3.12" }
|
|
110
|
+
- run: pip install --require-hashes -r requirements/ci.lock
|
|
111
|
+
- run: pip install . --no-deps
|
|
112
|
+
- run: pytest -m "not live and not performance" --cov --cov-report=term-missing --cov-fail-under=95
|
|
113
|
+
|
|
114
|
+
contracts:
|
|
115
|
+
runs-on: ubuntu-latest
|
|
116
|
+
steps:
|
|
117
|
+
- uses: actions/checkout@v4
|
|
118
|
+
- uses: actions/setup-python@v5
|
|
119
|
+
with: { python-version: "3.12" }
|
|
120
|
+
- run: pip install --require-hashes -r requirements/ci.lock
|
|
121
|
+
- run: pip install . --no-deps
|
|
122
|
+
- run: pytest -m contract
|
|
123
|
+
|
|
124
|
+
security:
|
|
125
|
+
runs-on: ubuntu-latest
|
|
126
|
+
steps:
|
|
127
|
+
- uses: actions/checkout@v4
|
|
128
|
+
# gitleaks scans *history*, and `actions/checkout` fetches a single commit by default.
|
|
129
|
+
# For a push it is handed `<first-pushed>^..<last-pushed>`, so the parent of the first
|
|
130
|
+
# pushed commit has to be in the object store; in a depth-1 clone it is not, and git
|
|
131
|
+
# answers "unknown revision", which the action reports as exit code 1. It fails the same
|
|
132
|
+
# way whether or not a secret exists, so a green run would not have meant anything either.
|
|
133
|
+
with: { fetch-depth: 0 }
|
|
134
|
+
- uses: actions/setup-python@v5
|
|
135
|
+
with: { python-version: "3.12" }
|
|
136
|
+
- run: pip install pip-audit
|
|
137
|
+
# Audit the locked sets, not the job's own environment: a bare `pip-audit` here would
|
|
138
|
+
# inspect an environment containing only pip-audit itself (Security Standards §11).
|
|
139
|
+
- run: pip-audit --require-hashes -r requirements/ci.lock
|
|
140
|
+
- run: pip-audit --require-hashes -r requirements/release.lock
|
|
141
|
+
- uses: gitleaks/gitleaks-action@v2
|
|
142
|
+
env:
|
|
143
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
144
|
+
|
|
145
|
+
build:
|
|
146
|
+
runs-on: ubuntu-latest
|
|
147
|
+
steps:
|
|
148
|
+
- uses: actions/checkout@v4
|
|
149
|
+
- uses: actions/setup-python@v5
|
|
150
|
+
with: { python-version: "3.12" }
|
|
151
|
+
- run: pip install --require-hashes -r requirements/release.lock
|
|
152
|
+
- run: python -m build --no-isolation
|
|
153
|
+
- run: twine check dist/*
|
|
154
|
+
- uses: actions/upload-artifact@v4
|
|
155
|
+
with: { name: dist, path: dist/ }
|
|
156
|
+
|
|
157
|
+
install-check:
|
|
158
|
+
needs: [build]
|
|
159
|
+
runs-on: ubuntu-latest
|
|
160
|
+
steps:
|
|
161
|
+
- uses: actions/checkout@v4
|
|
162
|
+
- uses: actions/setup-python@v5
|
|
163
|
+
with: { python-version: "3.12" }
|
|
164
|
+
- uses: actions/download-artifact@v4
|
|
165
|
+
with: { name: dist, path: dist/ }
|
|
166
|
+
- run: pip install dist/*.whl
|
|
167
|
+
- run: python -c "import loadledger"
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags: ["v*.*.*"]
|
|
6
|
+
workflow_dispatch: # manual TestPyPI dry run; see publish-testpypi below
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
id-token: write # required for PyPI Trusted Publishing
|
|
10
|
+
contents: write # required to create the GitHub release
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
release:
|
|
14
|
+
# Tag pushes only. Without this, clicking "Run workflow" for the TestPyPI dry run below would
|
|
15
|
+
# also fire this job and publish to real PyPI — the exact opposite of a dry run.
|
|
16
|
+
if: github.event_name == 'push'
|
|
17
|
+
runs-on: ubuntu-latest
|
|
18
|
+
environment: pypi # must match the Environment name set on the PyPI trusted publisher
|
|
19
|
+
steps:
|
|
20
|
+
- uses: actions/checkout@v4
|
|
21
|
+
- uses: actions/setup-python@v5
|
|
22
|
+
with: { python-version: "3.12" }
|
|
23
|
+
# Byte-for-byte the dry run's build chain below. A real release that resolved `build` and
|
|
24
|
+
# `hatchling` fresh from PyPI would not be the artifact the dry run proved.
|
|
25
|
+
- run: pip install --require-hashes -r requirements/release.lock
|
|
26
|
+
- run: python -m build --no-isolation
|
|
27
|
+
- run: twine check dist/*
|
|
28
|
+
- run: pip install "dist/$(ls dist | grep .whl)[dev]"
|
|
29
|
+
- run: pytest -m "not live and not performance"
|
|
30
|
+
- name: Publish to PyPI
|
|
31
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
32
|
+
- name: Create GitHub release
|
|
33
|
+
uses: softprops/action-gh-release@v2
|
|
34
|
+
with:
|
|
35
|
+
generate_release_notes: true
|
|
36
|
+
files: dist/*
|
|
37
|
+
|
|
38
|
+
publish-testpypi:
|
|
39
|
+
# Manual only, via Actions -> Release -> Run workflow. Packaging and Release Standards §6
|
|
40
|
+
# requires a successful TestPyPI publish ahead of a package's first real release; 0.1.0 is
|
|
41
|
+
# this package's first published version, so run this once before tagging v0.1.0. Later
|
|
42
|
+
# releases may skip it, or use it again as a dry run.
|
|
43
|
+
if: github.event_name == 'workflow_dispatch'
|
|
44
|
+
runs-on: ubuntu-latest
|
|
45
|
+
steps:
|
|
46
|
+
- uses: actions/checkout@v4
|
|
47
|
+
- uses: actions/setup-python@v5
|
|
48
|
+
with: { python-version: "3.12" }
|
|
49
|
+
- run: pip install --require-hashes -r requirements/release.lock
|
|
50
|
+
- run: python -m build --no-isolation
|
|
51
|
+
- run: twine check dist/*
|
|
52
|
+
- run: pip install "dist/$(ls dist | grep .whl)[dev]"
|
|
53
|
+
- run: pytest -m "not live and not performance"
|
|
54
|
+
- name: Publish to TestPyPI
|
|
55
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
56
|
+
with:
|
|
57
|
+
repository-url: https://test.pypi.org/legacy/
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# ---- Python ----
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.so
|
|
6
|
+
.Python
|
|
7
|
+
build/
|
|
8
|
+
develop-eggs/
|
|
9
|
+
dist/
|
|
10
|
+
downloads/
|
|
11
|
+
eggs/
|
|
12
|
+
.eggs/
|
|
13
|
+
lib/
|
|
14
|
+
lib64/
|
|
15
|
+
parts/
|
|
16
|
+
sdist/
|
|
17
|
+
var/
|
|
18
|
+
wheels/
|
|
19
|
+
*.egg-info/
|
|
20
|
+
.installed.cfg
|
|
21
|
+
*.egg
|
|
22
|
+
MANIFEST
|
|
23
|
+
|
|
24
|
+
# ---- Packaging / build backends ----
|
|
25
|
+
pip-wheel-metadata/
|
|
26
|
+
share/python-wheels/
|
|
27
|
+
|
|
28
|
+
# ---- Testing / coverage ----
|
|
29
|
+
.pytest_cache/
|
|
30
|
+
.cache
|
|
31
|
+
.coverage
|
|
32
|
+
.coverage.*
|
|
33
|
+
coverage.xml
|
|
34
|
+
*.cover
|
|
35
|
+
*.py,cover
|
|
36
|
+
htmlcov/
|
|
37
|
+
nosetests.xml
|
|
38
|
+
.hypothesis/
|
|
39
|
+
|
|
40
|
+
# ---- Type checking / linting ----
|
|
41
|
+
.mypy_cache/
|
|
42
|
+
.dmypy.json
|
|
43
|
+
dmypy.json
|
|
44
|
+
.ruff_cache/
|
|
45
|
+
.pytype/
|
|
46
|
+
|
|
47
|
+
# ---- Virtual environments ----
|
|
48
|
+
.venv/
|
|
49
|
+
venv/
|
|
50
|
+
ENV/
|
|
51
|
+
env/
|
|
52
|
+
env.bak/
|
|
53
|
+
venv.bak/
|
|
54
|
+
.python-version
|
|
55
|
+
|
|
56
|
+
# ---- Distribution / dependency locking ----
|
|
57
|
+
# requirements/*.lock is deliberately NOT ignored. The lock files are committed
|
|
58
|
+
# inputs: CI and release jobs install from them with --require-hashes (Packaging
|
|
59
|
+
# and Release Standards §4, Security Standards §11), so an ignored lock file
|
|
60
|
+
# means a checkout that cannot build.
|
|
61
|
+
|
|
62
|
+
# ---- Editors / OS ----
|
|
63
|
+
.vscode/
|
|
64
|
+
.idea/
|
|
65
|
+
*.swp
|
|
66
|
+
*.swo
|
|
67
|
+
.DS_Store
|
|
68
|
+
Thumbs.db
|
|
69
|
+
|
|
70
|
+
# ---- Suite runtime state (this component's local data) ----
|
|
71
|
+
# The application writes to XDG paths at runtime (~/.config, ~/.local/share,
|
|
72
|
+
# ~/.local/state per Master Architecture §1.2), never inside the repository.
|
|
73
|
+
# These entries only guard against a developer pointing XDG_* at the repo
|
|
74
|
+
# during local testing.
|
|
75
|
+
.local/
|
|
76
|
+
.config/
|
|
77
|
+
*.sqlite3
|
|
78
|
+
*.sqlite3-journal
|
|
79
|
+
*.sqlite3-wal
|
|
80
|
+
*.sqlite3-shm
|
|
81
|
+
/data/
|
|
82
|
+
/logs/
|
|
83
|
+
/backups/
|
|
84
|
+
/artifacts/
|
|
85
|
+
/exports/
|
|
86
|
+
|
|
87
|
+
# ---- Secrets ----
|
|
88
|
+
# Per Security Standards §8: secrets are never committed. Config files may
|
|
89
|
+
# only name where a secret comes from (*_env / *_file), never the value.
|
|
90
|
+
.env
|
|
91
|
+
.env.*
|
|
92
|
+
*.key
|
|
93
|
+
*.pem
|
|
94
|
+
secrets.toml
|
|
95
|
+
|
|
96
|
+
# ---- Node-free JS assets (MirrorWall consumers may still use a local tool) ----
|
|
97
|
+
node_modules/
|
|
98
|
+
|
|
99
|
+
# ---- Build artifacts from docs generation ----
|
|
100
|
+
docs/api/openapi-v1.json.tmp
|
|
101
|
+
|
|
102
|
+
# ---- import-linter cache ----
|
|
103
|
+
# `lint-imports` writes this on every gate run; it is a cache, never an input.
|
|
104
|
+
.import_linter_cache/
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
[importlinter]
|
|
2
|
+
root_package = loadledger
|
|
3
|
+
include_external_packages = True
|
|
4
|
+
|
|
5
|
+
[importlinter:contract:no-application-imports]
|
|
6
|
+
name = LoadLedger must not import applications
|
|
7
|
+
type = forbidden
|
|
8
|
+
source_modules = loadledger
|
|
9
|
+
forbidden_modules =
|
|
10
|
+
freeweight
|
|
11
|
+
loadcoach
|
|
12
|
+
ideapress
|
|
13
|
+
promptcadence
|
|
14
|
+
|
|
15
|
+
# `commissioner` was called `spotcheck` until py/Commissioner's rename (7077cc4). Both spellings
|
|
16
|
+
# are listed: a forbidden module that no longer exists forbids nothing, silently, so dropping the
|
|
17
|
+
# old name would quietly stop guarding against an import written before the rename.
|
|
18
|
+
[importlinter:contract:no-sibling-packages]
|
|
19
|
+
name = LoadLedger must not import sibling capability packages
|
|
20
|
+
type = forbidden
|
|
21
|
+
source_modules = loadledger
|
|
22
|
+
forbidden_modules =
|
|
23
|
+
setspec
|
|
24
|
+
modelrack
|
|
25
|
+
sweatmeter
|
|
26
|
+
weightsdb
|
|
27
|
+
mirrorwall
|
|
28
|
+
cutctx
|
|
29
|
+
toolyard
|
|
30
|
+
commissioner
|
|
31
|
+
spotcheck
|
|
32
|
+
|
|
33
|
+
# Replaces `no-sql-in-phase-1`, which forbade `sqlalchemy` outright while `loadledger.sql` did not
|
|
34
|
+
# exist. Same rule, with the one exemption Phase 2 earned, and it is the mechanical statement of
|
|
35
|
+
# ADR-0050 decision 4: `sqlalchemy` is an optional extra (`loadledger[sql]`) so the pure-value core
|
|
36
|
+
# stays installable with nothing but baseaicore. Weakening this contract — or deleting it, which
|
|
37
|
+
# looks identical in a diff — makes `pip install loadledger` drag an ORM into every consumer that
|
|
38
|
+
# only wanted to add up tokens.
|
|
39
|
+
#
|
|
40
|
+
# `alembic` has no exemption at all: the host owns every migration (ADR-0050 decision 5), so no
|
|
41
|
+
# module in this package imports it, `loadledger.sql` included. It is a test dependency for the
|
|
42
|
+
# miniature host and never a runtime one.
|
|
43
|
+
[importlinter:contract:only-the-sql-module-imports-sqlalchemy]
|
|
44
|
+
name = Only loadledger.sql may import SQLAlchemy, and nothing may import Alembic
|
|
45
|
+
type = forbidden
|
|
46
|
+
source_modules = loadledger
|
|
47
|
+
forbidden_modules =
|
|
48
|
+
sqlalchemy
|
|
49
|
+
alembic
|
|
50
|
+
ignore_imports =
|
|
51
|
+
loadledger.sql -> sqlalchemy
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
repos:
|
|
2
|
+
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
3
|
+
rev: v0.6.9
|
|
4
|
+
hooks:
|
|
5
|
+
- id: ruff
|
|
6
|
+
args: [--fix]
|
|
7
|
+
- id: ruff-format
|
|
8
|
+
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
9
|
+
rev: v4.6.0
|
|
10
|
+
hooks:
|
|
11
|
+
- id: trailing-whitespace
|
|
12
|
+
- id: end-of-file-fixer
|
|
13
|
+
- id: check-toml
|
|
14
|
+
- id: check-json
|
|
15
|
+
- id: check-added-large-files
|
|
16
|
+
- id: check-merge-conflict
|
|
17
|
+
- id: mixed-line-ending
|
|
18
|
+
args: [--fix=lf]
|
|
19
|
+
- repo: https://github.com/gitleaks/gitleaks
|
|
20
|
+
rev: v8.18.4
|
|
21
|
+
hooks:
|
|
22
|
+
- id: gitleaks
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `loadledger` are documented here.
|
|
4
|
+
Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versioning follows
|
|
5
|
+
[Semantic Versioning](https://semver.org/), pre-1.0 per
|
|
6
|
+
packaging and release standards §3.
|
|
7
|
+
|
|
8
|
+
## [0.1.0] — 2026-09-02
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- Repository scaffold: toolchain copied from `py/WeightsDB` (hatchling, ruff, mypy strict,
|
|
12
|
+
import-linter, pytest with `pytest-randomly`, hash-pinned `requirements/` locks, CI and release
|
|
13
|
+
workflows), with the PostgreSQL matrix job dropped — this package touches no database.
|
|
14
|
+
- Phase 1, the pure core: `CeilingScope`, `BudgetCeiling`, `Debit`, `CeilingVerdict`,
|
|
15
|
+
`LedgerEntry`, the `Ledger` protocol, `BalanceBook`, `InMemoryLedger`, `utc_day_start`,
|
|
16
|
+
`utc_day_key`, and the `LedgerError` / `CurrencyMismatch` / `InvalidCeiling` / `UnknownRun`
|
|
17
|
+
hierarchy. No I/O, no SQL, no logging, no environment reads.
|
|
18
|
+
- `CeilingVerdict.unpriced_debit_count` and `CeilingVerdict.unmetered_debit_count`, beyond the
|
|
19
|
+
shape in spec §7: contract 2 requires the unpriced count to ride on the money verdict, and the
|
|
20
|
+
same honesty is owed to the token balance when a provider leaves a token class unreported.
|
|
21
|
+
- `Ledger.declare_run`, beyond the protocol in spec §7: spec §13 defines a run as existing "once
|
|
22
|
+
debited **or declared**", and §7 gave no way to declare one.
|
|
23
|
+
- `as_canonical()` on `BudgetCeiling`, `Debit`, `CeilingVerdict` and `LedgerEntry`, following
|
|
24
|
+
BaseAiCore's own pattern — spec contract 4 requires byte-identical verdict serializations, and
|
|
25
|
+
`baseaicore.canonical_json` needs a mapping form to produce them. `Debit.as_canonical` omits
|
|
26
|
+
the cost deliberately: usage and `pricing_hash` are the stored facts (ADR-0030 rule 1).
|
|
27
|
+
- `PartialPricing` and the keyword-only `BudgetCeiling.partial_pricing` (default `FLOOR`;
|
|
28
|
+
`STRICT` requires a money bound, else `InvalidCeiling`), and `CeilingVerdict.untotalled_debit_count`
|
|
29
|
+
— the subset of the unpriced count that carried an estimate which did not total, which is what
|
|
30
|
+
a strict ceiling fires on. Both appear in the canonical forms, so the goldens changed (ADR-0069).
|
|
31
|
+
|
|
32
|
+
### Changed
|
|
33
|
+
- A debit whose estimate did not total now accumulates the components that *were* priced into the
|
|
34
|
+
money balance as a floor, instead of adding nothing (ADR-0069, reversing spec contract 2 as
|
|
35
|
+
first written). On a floor, `exceeded` is certain when `True` and not when `False`; a `STRICT`
|
|
36
|
+
ceiling treats an untotalled estimate in its window as exceeding, at pre-flight too. A debit
|
|
37
|
+
with no estimate still touches no money balance, and never trips a strict ceiling.
|
|
38
|
+
|
|
39
|
+
- Phase 2, the durable half: `loadledger.sql` under the new `loadledger[sql]` extra (ADR-0050).
|
|
40
|
+
- `mount_ledger_tables(metadata, *, prefix="ledger_") -> LedgerTables` adds four tables —
|
|
41
|
+
`ledger_entries`, `ledger_balances`, `ledger_balance_money`, `ledger_runs` — to an
|
|
42
|
+
application's own `MetaData`, so they appear in the application's own Alembic autogenerate and
|
|
43
|
+
the application owns the rows, the backups and the retention. The package owns no engine, no
|
|
44
|
+
session, no URL, no environment variable, no file and no migration history; `create_all`
|
|
45
|
+
appears nowhere in `src/`, and nothing is created on import.
|
|
46
|
+
- `SqlLedger(session_factory, ceilings, *, clock, table_prefix="ledger_")` implements the whole
|
|
47
|
+
`Ledger` protocol, `declare_run` included, over one injected session factory. It evaluates
|
|
48
|
+
through the same `BalanceBook` as `InMemoryLedger`, so the arithmetic and the honesty rules
|
|
49
|
+
have one implementation and not one per backend.
|
|
50
|
+
- `LedgerTables`, a frozen handle with `prefix`, `entries`, `balances`, `balance_money`, `runs`,
|
|
51
|
+
`metadata` and `all_tables`.
|
|
52
|
+
- `UnsupportedDialect` (`LEDGER_UNSUPPORTED_DIALECT`): ADR-0006 admits SQLite and PostgreSQL, and
|
|
53
|
+
a third dialect is refused at the first statement rather than found as a syntax error inside a
|
|
54
|
+
money transaction. Now in spec §7's error list and §13's table.
|
|
55
|
+
- `loadledger.core` gains the seams a durable ledger needs, all documented: `DebitContribution`,
|
|
56
|
+
`contribution_of`, `BalanceBook.windows_touched` / `window_for` / `seed`, `resolved_debit` and
|
|
57
|
+
`is_unpriced`. The package's top-level `__all__` is unchanged.
|
|
58
|
+
- `docs/quickstart.md` and the standalone `docs/quickstart.py` it publishes the output of (spec §20
|
|
59
|
+
acceptance criterion 2), with a test that runs the script so it cannot rot.
|
|
60
|
+
- `docs/mounted-table-upgrades.md`: the upgrade-note template and migration recipe LoadLedger ships
|
|
61
|
+
when a mounted table changes shape, since the host owns every migration (spec §19, ADR-0050
|
|
62
|
+
decision 5), with one worked example.
|
|
63
|
+
- CI gains a `db-matrix` job running the integration tests against PostgreSQL 16 with
|
|
64
|
+
`LOADLEDGER_REQUIRE_POSTGRES=1`, so a dialect cannot be skipped into a green run.
|
|
65
|
+
|
|
66
|
+
### Changed
|
|
67
|
+
- `.importlinter`'s `no-sql-in-phase-1` is **replaced** by
|
|
68
|
+
`only-the-sql-module-imports-sqlalchemy`: same forbidden modules, one ignored import for
|
|
69
|
+
`loadledger.sql -> sqlalchemy`, and no exemption at all for `alembic`. The `no-sibling-packages`
|
|
70
|
+
contract's `spotcheck` entry gains `commissioner`, the package's name since `7077cc4`; the old
|
|
71
|
+
spelling is kept, because a forbidden module that no longer exists forbids nothing.
|
|
72
|
+
- `pytest` `addopts` gains `-ra`, so a skipped PostgreSQL leg always names itself in the summary.
|
|
73
|
+
|
|
74
|
+
### Specification
|
|
75
|
+
- Spec §7, §10, §11, §13 and §15 were amended to describe what Phase 2 built, and the amendments
|
|
76
|
+
were accepted before release:
|
|
77
|
+
- §7 gains `LedgerTables`'s field list and `UnsupportedDialect`, and states what a durable
|
|
78
|
+
ledger's `entries()` returns.
|
|
79
|
+
- §10 names the four mounted tables and their keys, says why money is a table rather than a
|
|
80
|
+
column, and makes the `BigInteger` width part of the mounted contract.
|
|
81
|
+
- §11 contract 1 states that a durable ledger does not persist the `CostEstimate` — the one
|
|
82
|
+
place a consumer swapping `InMemoryLedger` for `SqlLedger` sees a difference.
|
|
83
|
+
- §13 gains rows for the prefix `ValueError` and for `UnsupportedDialect`.
|
|
84
|
+
- §15's single `entries` budget is split: for `SqlLedger` on SQLite, the query is ≤ 100 ms and
|
|
85
|
+
full materialization ≤ 250 ms. `InMemoryLedger` keeps ≤ 100 ms. The old single figure was set
|
|
86
|
+
before `SqlLedger` existed and was never about constructing ten thousand value objects.
|
|
87
|
+
|
|
88
|
+
### Performance, as measured
|
|
89
|
+
- `debit` with three ceilings ~1.5 ms (budget 5 ms) and flat as history grows — balances are
|
|
90
|
+
maintained, not recomputed. `would_exceed` ~0.4 ms (budget 2 ms). `entries` over a 10 000-entry
|
|
91
|
+
run: ~17 ms for the query (budget 100 ms), ~155 ms fully materialized (budget 250 ms). All
|
|
92
|
+
inside the amended §15.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Contributing to LoadLedger
|
|
2
|
+
|
|
3
|
+
This repository is one component of the Local AI Suite. Before changing anything, read
|
|
4
|
+
`docs/packages/loadledger/spec.md` and the current
|
|
5
|
+
phase in `development-plan.md` — both are in this repository's `docs/` folder, copied from the suite's
|
|
6
|
+
central documentation set so this repository can be worked on independently.
|
|
7
|
+
|
|
8
|
+
## Development setup
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
python -m venv .venv
|
|
12
|
+
source .venv/bin/activate
|
|
13
|
+
pip install -e ".[dev]"
|
|
14
|
+
pre-commit install
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Required reading, in order
|
|
18
|
+
|
|
19
|
+
1. This component's spec — purpose, scope, non-goals, contracts.
|
|
20
|
+
2. `development-plan.md` in the same folder — the phase you are implementing, its acceptance criteria and its tests.
|
|
21
|
+
|
|
22
|
+
## Rules that apply to every change here
|
|
23
|
+
|
|
24
|
+
* Follow the architecture's dependency direction.
|
|
25
|
+
This repository's `.importlinter` enforces it in CI; do not weaken that file to make an import work.
|
|
26
|
+
* No business logic in a route handler or CLI command body — both call one service method and render
|
|
27
|
+
.
|
|
28
|
+
* An unavailable measurement is `Unsupported`, never zero, never `None` used as a substitute
|
|
29
|
+
.
|
|
30
|
+
* Prompts are versioned JSON records, not Python string literals.
|
|
31
|
+
* Every phase's acceptance criteria in `development-plan.md` must be demonstrable, not merely
|
|
32
|
+
test-covered — the plan states what to run and what a person should see.
|
|
33
|
+
|
|
34
|
+
## Before opening a pull request
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
ruff format --check .
|
|
38
|
+
ruff check .
|
|
39
|
+
mypy src tests
|
|
40
|
+
lint-imports
|
|
41
|
+
pytest -m "not live and not performance"
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
All of the above run in CI (`.github/workflows/ci.yml`); a red CI run blocks merge.
|
|
45
|
+
|
|
46
|
+
## Commit style
|
|
47
|
+
|
|
48
|
+
Conventional Commits (`feat:`, `fix:`, `docs:`, `refactor:`, `test:`, `chore:`, `perf:`, `build:`,
|
|
49
|
+
`ci:`), with `!` or a `BREAKING CHANGE:` footer for breaking changes. Update `CHANGELOG.md` under
|
|
50
|
+
`## [Unreleased]` for any user-visible change.
|