aevum-store-postgres 0.3.0__tar.gz → 0.5.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.
- aevum_store_postgres-0.5.0/.gitignore +52 -0
- {aevum_store_postgres-0.3.0 → aevum_store_postgres-0.5.0}/PKG-INFO +1 -1
- {aevum_store_postgres-0.3.0 → aevum_store_postgres-0.5.0}/pyproject.toml +1 -1
- {aevum_store_postgres-0.3.0 → aevum_store_postgres-0.5.0}/src/aevum/store/postgres/__init__.py +1 -1
- {aevum_store_postgres-0.3.0 → aevum_store_postgres-0.5.0}/src/aevum/store/postgres/ledger.py +113 -20
- {aevum_store_postgres-0.3.0 → aevum_store_postgres-0.5.0}/tests/test_ledger.py +79 -0
- aevum_store_postgres-0.3.0/.gitignore +0 -31
- {aevum_store_postgres-0.3.0 → aevum_store_postgres-0.5.0}/README.md +0 -0
- {aevum_store_postgres-0.3.0 → aevum_store_postgres-0.5.0}/src/aevum/store/postgres/consent.py +0 -0
- {aevum_store_postgres-0.3.0 → aevum_store_postgres-0.5.0}/src/aevum/store/postgres/migrate.py +0 -0
- {aevum_store_postgres-0.3.0 → aevum_store_postgres-0.5.0}/src/aevum/store/postgres/py.typed +0 -0
- {aevum_store_postgres-0.3.0 → aevum_store_postgres-0.5.0}/src/aevum/store/postgres/schema.py +0 -0
- {aevum_store_postgres-0.3.0 → aevum_store_postgres-0.5.0}/src/aevum/store/postgres/store.py +0 -0
- {aevum_store_postgres-0.3.0 → aevum_store_postgres-0.5.0}/tests/conftest.py +0 -0
- {aevum_store_postgres-0.3.0 → aevum_store_postgres-0.5.0}/tests/test_pg_consent.py +0 -0
- {aevum_store_postgres-0.3.0 → aevum_store_postgres-0.5.0}/tests/test_pg_migrate.py +0 -0
- {aevum_store_postgres-0.3.0 → aevum_store_postgres-0.5.0}/tests/test_pg_store.py +0 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.pyc
|
|
4
|
+
*.pyo
|
|
5
|
+
*.pyd
|
|
6
|
+
.venv/
|
|
7
|
+
*.egg-info/
|
|
8
|
+
|
|
9
|
+
# Build
|
|
10
|
+
dist/
|
|
11
|
+
build/
|
|
12
|
+
site/
|
|
13
|
+
|
|
14
|
+
# Tools
|
|
15
|
+
.mypy_cache/
|
|
16
|
+
.ruff_cache/
|
|
17
|
+
.pytest_cache/
|
|
18
|
+
.hypothesis/
|
|
19
|
+
.cache/
|
|
20
|
+
|
|
21
|
+
# IDE
|
|
22
|
+
.vscode/
|
|
23
|
+
.idea/
|
|
24
|
+
*.swp
|
|
25
|
+
*.swo
|
|
26
|
+
|
|
27
|
+
# OS
|
|
28
|
+
.DS_Store
|
|
29
|
+
Thumbs.db
|
|
30
|
+
|
|
31
|
+
# Verify scripts (run locally, never commit)
|
|
32
|
+
verify_*.py
|
|
33
|
+
scripts/verify_*.py
|
|
34
|
+
|
|
35
|
+
# Aevum development — never commit (Phase 0+)
|
|
36
|
+
aevum_principles.key
|
|
37
|
+
signed_principles_draft.yaml
|
|
38
|
+
tools/sign_principles.py
|
|
39
|
+
|
|
40
|
+
# Private keys — never commit
|
|
41
|
+
*.key
|
|
42
|
+
*.pem
|
|
43
|
+
|
|
44
|
+
# OpenSSF Scorecard output (Phase 0+)
|
|
45
|
+
results.sarif
|
|
46
|
+
verify_phase3.py
|
|
47
|
+
verify_phase7.py
|
|
48
|
+
verify_phase8.py
|
|
49
|
+
verify_phase*.py
|
|
50
|
+
|
|
51
|
+
# Maintenance generated files — local only, never commit
|
|
52
|
+
maintenance/generated/
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aevum-store-postgres
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: Aevum — PostgreSQL GraphStore + ConsentLedger backend (team deployments).
|
|
5
5
|
Project-URL: Homepage, https://aevum.build
|
|
6
6
|
Project-URL: Repository, https://github.com/aevum-labs/aevum
|
{aevum_store_postgres-0.3.0 → aevum_store_postgres-0.5.0}/src/aevum/store/postgres/__init__.py
RENAMED
|
@@ -25,6 +25,6 @@ from aevum.store.postgres.consent import PostgresConsentLedger
|
|
|
25
25
|
from aevum.store.postgres.ledger import PostgresLedger
|
|
26
26
|
from aevum.store.postgres.store import PostgresStore
|
|
27
27
|
|
|
28
|
-
__version__ = "0.
|
|
28
|
+
__version__ = "0.4.0"
|
|
29
29
|
|
|
30
30
|
__all__ = ["PostgresStore", "PostgresConsentLedger", "PostgresLedger"]
|
{aevum_store_postgres-0.3.0 → aevum_store_postgres-0.5.0}/src/aevum/store/postgres/ledger.py
RENAMED
|
@@ -11,6 +11,7 @@ The database table is INSERT-only -- no UPDATE or DELETE ever issued.
|
|
|
11
11
|
from __future__ import annotations
|
|
12
12
|
|
|
13
13
|
import json
|
|
14
|
+
import logging
|
|
14
15
|
import threading
|
|
15
16
|
from typing import Any
|
|
16
17
|
|
|
@@ -118,6 +119,59 @@ class PostgresLedger:
|
|
|
118
119
|
self._conn = conn
|
|
119
120
|
self._sigchain = sigchain
|
|
120
121
|
self._lock = lock or threading.Lock()
|
|
122
|
+
self._resume_chain_from_db()
|
|
123
|
+
|
|
124
|
+
def _resume_chain_from_db(self) -> None:
|
|
125
|
+
"""
|
|
126
|
+
Seed the in-memory sigchain state from the last persisted event.
|
|
127
|
+
|
|
128
|
+
Without this, every Engine restart begins a new chain at
|
|
129
|
+
sequence=1 / prior_hash=GENESIS_HASH, silently forking the chain.
|
|
130
|
+
After this fix, the sigchain continues from where the last process left off.
|
|
131
|
+
|
|
132
|
+
Idempotent — safe to call on a fresh database (no-op if table empty).
|
|
133
|
+
"""
|
|
134
|
+
from psycopg.rows import dict_row
|
|
135
|
+
|
|
136
|
+
log = logging.getLogger(__name__)
|
|
137
|
+
try:
|
|
138
|
+
with self._lock, self._conn.cursor(row_factory=dict_row) as cur:
|
|
139
|
+
cur.execute(
|
|
140
|
+
"""
|
|
141
|
+
SELECT sequence, event_id, audit_id,
|
|
142
|
+
event_type, actor, system_time,
|
|
143
|
+
episode_id, causation_id, correlation_id,
|
|
144
|
+
prior_hash, payload_hash, signature,
|
|
145
|
+
signer_key_id, schema_version,
|
|
146
|
+
valid_from, valid_to,
|
|
147
|
+
trace_id, span_id, payload
|
|
148
|
+
FROM aevum_ledger
|
|
149
|
+
ORDER BY sequence DESC
|
|
150
|
+
LIMIT 1
|
|
151
|
+
"""
|
|
152
|
+
)
|
|
153
|
+
row = cur.fetchone()
|
|
154
|
+
except Exception as exc:
|
|
155
|
+
log.debug(
|
|
156
|
+
"aevum-store-postgres: could not resume chain from DB (%s) "
|
|
157
|
+
"— starting from genesis. Normal on a fresh database.",
|
|
158
|
+
exc,
|
|
159
|
+
)
|
|
160
|
+
return
|
|
161
|
+
|
|
162
|
+
if row is None:
|
|
163
|
+
return # Fresh database — genesis is correct
|
|
164
|
+
|
|
165
|
+
last_event = _row_to_event(row)
|
|
166
|
+
continuation_hash = AuditEvent.hash_event_for_chain(last_event)
|
|
167
|
+
self._sigchain.restore((last_event.sequence, continuation_hash))
|
|
168
|
+
|
|
169
|
+
log.debug(
|
|
170
|
+
"aevum-store-postgres: resumed chain from DB "
|
|
171
|
+
"— sequence=%d prior_hash=%s…",
|
|
172
|
+
last_event.sequence,
|
|
173
|
+
continuation_hash[:12],
|
|
174
|
+
)
|
|
121
175
|
|
|
122
176
|
def append(
|
|
123
177
|
self,
|
|
@@ -130,6 +184,10 @@ class PostgresLedger:
|
|
|
130
184
|
correlation_id: str | None = None,
|
|
131
185
|
) -> AuditEvent:
|
|
132
186
|
with self._lock:
|
|
187
|
+
# Save sigchain state before advancing it.
|
|
188
|
+
# If the INSERT fails, restore prevents the chain from
|
|
189
|
+
# chaining from a ghost event that was never persisted.
|
|
190
|
+
checkpoint = self._sigchain.checkpoint()
|
|
133
191
|
event = self._sigchain.new_event(
|
|
134
192
|
event_type=event_type,
|
|
135
193
|
payload=payload,
|
|
@@ -139,29 +197,44 @@ class PostgresLedger:
|
|
|
139
197
|
correlation_id=correlation_id,
|
|
140
198
|
)
|
|
141
199
|
row = _event_to_row(event)
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
200
|
+
try:
|
|
201
|
+
with self._conn.cursor() as cur:
|
|
202
|
+
cur.execute(
|
|
203
|
+
"""
|
|
204
|
+
INSERT INTO aevum_ledger (
|
|
205
|
+
event_id, audit_id, event_type, actor, system_time,
|
|
206
|
+
episode_id, causation_id, correlation_id,
|
|
207
|
+
prior_hash, payload_hash, signature, signer_key_id,
|
|
208
|
+
schema_version, valid_from, valid_to,
|
|
209
|
+
trace_id, span_id, payload
|
|
210
|
+
) VALUES (
|
|
211
|
+
%(event_id)s, %(audit_id)s, %(event_type)s, %(actor)s,
|
|
212
|
+
%(system_time)s, %(episode_id)s, %(causation_id)s,
|
|
213
|
+
%(correlation_id)s, %(prior_hash)s, %(payload_hash)s,
|
|
214
|
+
%(signature)s, %(signer_key_id)s, %(schema_version)s,
|
|
215
|
+
%(valid_from)s, %(valid_to)s, %(trace_id)s,
|
|
216
|
+
%(span_id)s, %(payload)s::jsonb
|
|
217
|
+
)
|
|
218
|
+
""",
|
|
219
|
+
row,
|
|
158
220
|
)
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
)
|
|
162
|
-
|
|
221
|
+
self._conn.commit()
|
|
222
|
+
except Exception:
|
|
223
|
+
self._sigchain.restore(checkpoint)
|
|
224
|
+
raise
|
|
163
225
|
return event
|
|
164
226
|
|
|
227
|
+
def last_audit_id(self) -> str | None:
|
|
228
|
+
"""Return the audit_id of the most recently appended event, or None."""
|
|
229
|
+
from psycopg.rows import dict_row
|
|
230
|
+
|
|
231
|
+
with self._lock, self._conn.cursor(row_factory=dict_row) as cur:
|
|
232
|
+
cur.execute(
|
|
233
|
+
"SELECT audit_id FROM aevum_ledger ORDER BY sequence DESC LIMIT 1"
|
|
234
|
+
)
|
|
235
|
+
row = cur.fetchone()
|
|
236
|
+
return row["audit_id"] if row else None
|
|
237
|
+
|
|
165
238
|
def get(self, audit_id: str) -> AuditEvent:
|
|
166
239
|
from psycopg.rows import dict_row
|
|
167
240
|
with self._lock, self._conn.cursor(row_factory=dict_row) as cur:
|
|
@@ -187,6 +260,26 @@ class PostgresLedger:
|
|
|
187
260
|
result = cur.fetchone()
|
|
188
261
|
return result[0] if result else 0
|
|
189
262
|
|
|
263
|
+
def max_sequence_for_subjects(self, subject_ids: list[str]) -> int:
|
|
264
|
+
"""
|
|
265
|
+
Return the highest sequence number among all ingest.accepted events
|
|
266
|
+
whose payload subject_id is in subject_ids. Returns 0 if none found.
|
|
267
|
+
"""
|
|
268
|
+
if not subject_ids:
|
|
269
|
+
return 0
|
|
270
|
+
with self._lock, self._conn.cursor() as cur:
|
|
271
|
+
cur.execute(
|
|
272
|
+
"""
|
|
273
|
+
SELECT COALESCE(MAX(sequence), 0)
|
|
274
|
+
FROM aevum_ledger
|
|
275
|
+
WHERE event_type = 'ingest.accepted'
|
|
276
|
+
AND payload->>'subject_id' = ANY(%s)
|
|
277
|
+
""",
|
|
278
|
+
(subject_ids,),
|
|
279
|
+
)
|
|
280
|
+
result = cur.fetchone()
|
|
281
|
+
return int(result[0]) if result else 0
|
|
282
|
+
|
|
190
283
|
def __delitem__(self, key: object) -> None:
|
|
191
284
|
raise BarrierViolationError(
|
|
192
285
|
"Attempted to delete a ledger entry -- Barrier 4 (Audit Immutability) violated."
|
|
@@ -56,6 +56,18 @@ class FakeCursor:
|
|
|
56
56
|
matches = [r for r in self._conn._rows if r.get("audit_id") == audit_id]
|
|
57
57
|
self._last_result = [list(r.values()) for r in matches]
|
|
58
58
|
self.description = [[k] for k in (matches[0].keys() if matches else [])]
|
|
59
|
+
elif "order by sequence desc" in sql_lower and "limit 1" in sql_lower:
|
|
60
|
+
if self._conn._rows:
|
|
61
|
+
last_row = max(self._conn._rows, key=lambda r: r.get("sequence", 0))
|
|
62
|
+
if "select audit_id" in sql_lower:
|
|
63
|
+
self._last_result = [[last_row.get("audit_id", "")]]
|
|
64
|
+
self.description = [["audit_id"]]
|
|
65
|
+
else:
|
|
66
|
+
self._last_result = [list(last_row.values())]
|
|
67
|
+
self.description = [[k] for k in last_row]
|
|
68
|
+
else:
|
|
69
|
+
self._last_result = []
|
|
70
|
+
self.description = []
|
|
59
71
|
elif "order by sequence" in sql_lower:
|
|
60
72
|
sorted_rows = sorted(self._conn._rows, key=lambda r: r.get("sequence", 0))
|
|
61
73
|
self._last_result = [list(r.values()) for r in sorted_rows]
|
|
@@ -120,6 +132,73 @@ class TestPostgresLedger:
|
|
|
120
132
|
assert "payload" in row
|
|
121
133
|
|
|
122
134
|
|
|
135
|
+
def test_rollback_on_write_failure() -> None:
|
|
136
|
+
"""Failed INSERT must not corrupt in-memory sigchain state."""
|
|
137
|
+
from aevum.core.audit.sigchain import GENESIS_HASH
|
|
138
|
+
|
|
139
|
+
class ExplodingCursor:
|
|
140
|
+
description: list = []
|
|
141
|
+
def __enter__(self): return self
|
|
142
|
+
def __exit__(self, *a): pass
|
|
143
|
+
def execute(self, sql: str, params=None) -> None:
|
|
144
|
+
if "INSERT" in sql.upper():
|
|
145
|
+
raise RuntimeError("simulated disk full")
|
|
146
|
+
def fetchone(self): return None
|
|
147
|
+
def fetchall(self): return []
|
|
148
|
+
|
|
149
|
+
class ExplodingConn:
|
|
150
|
+
_rows: list = []
|
|
151
|
+
_sequence: int = 0
|
|
152
|
+
def cursor(self, row_factory=None): return ExplodingCursor()
|
|
153
|
+
def commit(self): pass
|
|
154
|
+
|
|
155
|
+
sc = Sigchain()
|
|
156
|
+
ledger = PostgresLedger(ExplodingConn(), sc)
|
|
157
|
+
|
|
158
|
+
with pytest.raises(RuntimeError):
|
|
159
|
+
ledger.append(event_type="will.fail", payload={}, actor="a")
|
|
160
|
+
|
|
161
|
+
assert sc._sequence == 0, f"rollback failed: _sequence={sc._sequence}"
|
|
162
|
+
assert sc._prior_hash == GENESIS_HASH, "rollback failed: prior_hash changed"
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def test_chain_continuity_across_restart() -> None:
|
|
166
|
+
"""New PostgresLedger on existing data must continue the chain."""
|
|
167
|
+
conn = FakeConn()
|
|
168
|
+
sc1 = Sigchain()
|
|
169
|
+
ledger1 = PostgresLedger(conn, sc1)
|
|
170
|
+
for i in range(3):
|
|
171
|
+
ledger1.append(event_type=f"s1.{i}", payload={"i": i}, actor="a")
|
|
172
|
+
assert sc1._sequence == 3
|
|
173
|
+
|
|
174
|
+
# Simulate restart: new sigchain + new ledger on same conn
|
|
175
|
+
sc2 = Sigchain()
|
|
176
|
+
assert sc2._sequence == 0 # starts fresh
|
|
177
|
+
|
|
178
|
+
ledger2 = PostgresLedger(conn, sc2) # _resume_chain_from_db fires
|
|
179
|
+
|
|
180
|
+
assert sc2._sequence == 3, (
|
|
181
|
+
f"Expected sequence=3 after resume, got {sc2._sequence}. "
|
|
182
|
+
"Chain fork: each restart should CONTINUE, not start over."
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
e = ledger2.append(event_type="s2.first", payload={}, actor="b")
|
|
186
|
+
assert e.sequence == 4, f"Expected sequence=4, got {e.sequence}"
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def test_last_audit_id_empty() -> None:
|
|
190
|
+
conn = FakeConn()
|
|
191
|
+
ledger = PostgresLedger(conn, Sigchain())
|
|
192
|
+
assert ledger.last_audit_id() is None
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def test_last_audit_id_after_append() -> None:
|
|
196
|
+
conn = FakeConn()
|
|
197
|
+
ledger = PostgresLedger(conn, Sigchain())
|
|
198
|
+
e = ledger.append(event_type="test", payload={}, actor="a")
|
|
199
|
+
assert ledger.last_audit_id() == e.audit_id()
|
|
200
|
+
|
|
201
|
+
|
|
123
202
|
# Integration tests -- require a real Postgres database
|
|
124
203
|
_POSTGRES_DSN = os.environ.get("AEVUM_TEST_POSTGRES_DSN")
|
|
125
204
|
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
# Python
|
|
2
|
-
__pycache__/
|
|
3
|
-
*.pyc
|
|
4
|
-
*.pyo
|
|
5
|
-
*.pyd
|
|
6
|
-
.venv/
|
|
7
|
-
*.egg-info/
|
|
8
|
-
|
|
9
|
-
# Build
|
|
10
|
-
dist/
|
|
11
|
-
build/
|
|
12
|
-
|
|
13
|
-
# Tools
|
|
14
|
-
.mypy_cache/
|
|
15
|
-
.ruff_cache/
|
|
16
|
-
.pytest_cache/
|
|
17
|
-
.hypothesis/
|
|
18
|
-
|
|
19
|
-
# IDE
|
|
20
|
-
.vscode/
|
|
21
|
-
.idea/
|
|
22
|
-
*.swp
|
|
23
|
-
*.swo
|
|
24
|
-
|
|
25
|
-
# OS
|
|
26
|
-
.DS_Store
|
|
27
|
-
Thumbs.db
|
|
28
|
-
|
|
29
|
-
# Verify scripts (run locally, never commit)
|
|
30
|
-
verify_phase*.py
|
|
31
|
-
scripts/verify_phase*.py
|
|
File without changes
|
{aevum_store_postgres-0.3.0 → aevum_store_postgres-0.5.0}/src/aevum/store/postgres/consent.py
RENAMED
|
File without changes
|
{aevum_store_postgres-0.3.0 → aevum_store_postgres-0.5.0}/src/aevum/store/postgres/migrate.py
RENAMED
|
File without changes
|
|
File without changes
|
{aevum_store_postgres-0.3.0 → aevum_store_postgres-0.5.0}/src/aevum/store/postgres/schema.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|