aevum-store-postgres 0.2.0__tar.gz → 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.
- {aevum_store_postgres-0.2.0 → aevum_store_postgres-0.3.0}/.gitignore +31 -31
- {aevum_store_postgres-0.2.0 → aevum_store_postgres-0.3.0}/PKG-INFO +1 -1
- {aevum_store_postgres-0.2.0 → aevum_store_postgres-0.3.0}/README.md +21 -21
- {aevum_store_postgres-0.2.0 → aevum_store_postgres-0.3.0}/pyproject.toml +60 -60
- {aevum_store_postgres-0.2.0 → aevum_store_postgres-0.3.0}/src/aevum/store/postgres/__init__.py +30 -30
- {aevum_store_postgres-0.2.0 → aevum_store_postgres-0.3.0}/src/aevum/store/postgres/consent.py +110 -110
- {aevum_store_postgres-0.2.0 → aevum_store_postgres-0.3.0}/src/aevum/store/postgres/ledger.py +198 -198
- {aevum_store_postgres-0.2.0 → aevum_store_postgres-0.3.0}/src/aevum/store/postgres/migrate.py +134 -134
- {aevum_store_postgres-0.2.0 → aevum_store_postgres-0.3.0}/src/aevum/store/postgres/schema.py +33 -33
- {aevum_store_postgres-0.2.0 → aevum_store_postgres-0.3.0}/src/aevum/store/postgres/store.py +110 -110
- {aevum_store_postgres-0.2.0 → aevum_store_postgres-0.3.0}/tests/conftest.py +164 -164
- {aevum_store_postgres-0.2.0 → aevum_store_postgres-0.3.0}/tests/test_ledger.py +160 -160
- {aevum_store_postgres-0.2.0 → aevum_store_postgres-0.3.0}/tests/test_pg_consent.py +121 -121
- {aevum_store_postgres-0.2.0 → aevum_store_postgres-0.3.0}/tests/test_pg_migrate.py +96 -96
- {aevum_store_postgres-0.2.0 → aevum_store_postgres-0.3.0}/tests/test_pg_store.py +117 -117
- {aevum_store_postgres-0.2.0 → aevum_store_postgres-0.3.0}/src/aevum/store/postgres/py.typed +0 -0
|
@@ -1,31 +1,31 @@
|
|
|
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
|
|
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
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aevum-store-postgres
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.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
|
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
# aevum-store-postgres
|
|
2
|
-
|
|
3
|
-
PostgreSQL-backed graph store for Aevum. Suitable for team deployments with shared state, concurrent writers, and durable persistence.
|
|
4
|
-
|
|
5
|
-
```bash
|
|
6
|
-
pip install aevum-store-postgres
|
|
7
|
-
```
|
|
8
|
-
|
|
9
|
-
```python
|
|
10
|
-
import psycopg
|
|
11
|
-
from aevum.core import Engine
|
|
12
|
-
from aevum.store.postgres import PostgresStore
|
|
13
|
-
from aevum.store.postgres.store import initialize_schema
|
|
14
|
-
|
|
15
|
-
conn = psycopg.connect("postgresql://user:pass@localhost/aevum")
|
|
16
|
-
initialize_schema(conn)
|
|
17
|
-
engine = Engine(graph_store=PostgresStore(conn))
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
For single-node deployments without PostgreSQL, use `aevum-store-oxigraph` instead.
|
|
21
|
-
See the [main repository README](https://github.com/aevum-labs/aevum) for backend selection guidance.
|
|
1
|
+
# aevum-store-postgres
|
|
2
|
+
|
|
3
|
+
PostgreSQL-backed graph store for Aevum. Suitable for team deployments with shared state, concurrent writers, and durable persistence.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
pip install aevum-store-postgres
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```python
|
|
10
|
+
import psycopg
|
|
11
|
+
from aevum.core import Engine
|
|
12
|
+
from aevum.store.postgres import PostgresStore
|
|
13
|
+
from aevum.store.postgres.store import initialize_schema
|
|
14
|
+
|
|
15
|
+
conn = psycopg.connect("postgresql://user:pass@localhost/aevum")
|
|
16
|
+
initialize_schema(conn)
|
|
17
|
+
engine = Engine(graph_store=PostgresStore(conn))
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
For single-node deployments without PostgreSQL, use `aevum-store-oxigraph` instead.
|
|
21
|
+
See the [main repository README](https://github.com/aevum-labs/aevum) for backend selection guidance.
|
|
@@ -1,60 +1,60 @@
|
|
|
1
|
-
[project]
|
|
2
|
-
name = "aevum-store-postgres"
|
|
3
|
-
version = "0.
|
|
4
|
-
description = "Aevum — PostgreSQL GraphStore + ConsentLedger backend (team deployments)."
|
|
5
|
-
readme = "README.md"
|
|
6
|
-
requires-python = ">=3.11"
|
|
7
|
-
license = { text = "Apache-2.0" }
|
|
8
|
-
classifiers = [
|
|
9
|
-
"Development Status :: 3 - Alpha",
|
|
10
|
-
"Intended Audience :: Developers",
|
|
11
|
-
"License :: OSI Approved :: Apache Software License",
|
|
12
|
-
"Programming Language :: Python :: 3.11",
|
|
13
|
-
"Programming Language :: Python :: 3.12",
|
|
14
|
-
"Programming Language :: Python :: 3.13",
|
|
15
|
-
"Typing :: Typed",
|
|
16
|
-
]
|
|
17
|
-
dependencies = [
|
|
18
|
-
"aevum-core",
|
|
19
|
-
"psycopg[binary]>=3.1,<4.0",
|
|
20
|
-
]
|
|
21
|
-
|
|
22
|
-
[project.scripts]
|
|
23
|
-
aevum-store-migrate = "aevum.store.postgres.migrate:main"
|
|
24
|
-
|
|
25
|
-
[project.urls]
|
|
26
|
-
Homepage = "https://aevum.build"
|
|
27
|
-
Repository = "https://github.com/aevum-labs/aevum"
|
|
28
|
-
|
|
29
|
-
[build-system]
|
|
30
|
-
requires = ["hatchling"]
|
|
31
|
-
build-backend = "hatchling.build"
|
|
32
|
-
|
|
33
|
-
[tool.hatch.build.targets.wheel]
|
|
34
|
-
packages = ["src/aevum"]
|
|
35
|
-
|
|
36
|
-
[tool.uv.sources]
|
|
37
|
-
aevum-core = { workspace = true }
|
|
38
|
-
|
|
39
|
-
[tool.pytest.ini_options]
|
|
40
|
-
testpaths = ["tests"]
|
|
41
|
-
asyncio_mode = "auto"
|
|
42
|
-
addopts = "--tb=short"
|
|
43
|
-
pythonpath = ["src", "tests"]
|
|
44
|
-
|
|
45
|
-
[tool.mypy]
|
|
46
|
-
strict = true
|
|
47
|
-
python_version = "3.11"
|
|
48
|
-
mypy_path = "src"
|
|
49
|
-
explicit_package_bases = true
|
|
50
|
-
ignore_missing_imports = true
|
|
51
|
-
|
|
52
|
-
[tool.ruff]
|
|
53
|
-
line-length = 130
|
|
54
|
-
|
|
55
|
-
[tool.ruff.lint]
|
|
56
|
-
select = ["E", "F", "UP", "B", "SIM", "I", "ANN"]
|
|
57
|
-
ignore = ["ANN401"]
|
|
58
|
-
|
|
59
|
-
[tool.ruff.lint.per-file-ignores]
|
|
60
|
-
"tests/**" = ["ANN"]
|
|
1
|
+
[project]
|
|
2
|
+
name = "aevum-store-postgres"
|
|
3
|
+
version = "0.3.0"
|
|
4
|
+
description = "Aevum — PostgreSQL GraphStore + ConsentLedger backend (team deployments)."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.11"
|
|
7
|
+
license = { text = "Apache-2.0" }
|
|
8
|
+
classifiers = [
|
|
9
|
+
"Development Status :: 3 - Alpha",
|
|
10
|
+
"Intended Audience :: Developers",
|
|
11
|
+
"License :: OSI Approved :: Apache Software License",
|
|
12
|
+
"Programming Language :: Python :: 3.11",
|
|
13
|
+
"Programming Language :: Python :: 3.12",
|
|
14
|
+
"Programming Language :: Python :: 3.13",
|
|
15
|
+
"Typing :: Typed",
|
|
16
|
+
]
|
|
17
|
+
dependencies = [
|
|
18
|
+
"aevum-core",
|
|
19
|
+
"psycopg[binary]>=3.1,<4.0",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
[project.scripts]
|
|
23
|
+
aevum-store-migrate = "aevum.store.postgres.migrate:main"
|
|
24
|
+
|
|
25
|
+
[project.urls]
|
|
26
|
+
Homepage = "https://aevum.build"
|
|
27
|
+
Repository = "https://github.com/aevum-labs/aevum"
|
|
28
|
+
|
|
29
|
+
[build-system]
|
|
30
|
+
requires = ["hatchling"]
|
|
31
|
+
build-backend = "hatchling.build"
|
|
32
|
+
|
|
33
|
+
[tool.hatch.build.targets.wheel]
|
|
34
|
+
packages = ["src/aevum"]
|
|
35
|
+
|
|
36
|
+
[tool.uv.sources]
|
|
37
|
+
aevum-core = { workspace = true }
|
|
38
|
+
|
|
39
|
+
[tool.pytest.ini_options]
|
|
40
|
+
testpaths = ["tests"]
|
|
41
|
+
asyncio_mode = "auto"
|
|
42
|
+
addopts = "--tb=short"
|
|
43
|
+
pythonpath = ["src", "tests"]
|
|
44
|
+
|
|
45
|
+
[tool.mypy]
|
|
46
|
+
strict = true
|
|
47
|
+
python_version = "3.11"
|
|
48
|
+
mypy_path = "src"
|
|
49
|
+
explicit_package_bases = true
|
|
50
|
+
ignore_missing_imports = true
|
|
51
|
+
|
|
52
|
+
[tool.ruff]
|
|
53
|
+
line-length = 130
|
|
54
|
+
|
|
55
|
+
[tool.ruff.lint]
|
|
56
|
+
select = ["E", "F", "UP", "B", "SIM", "I", "ANN"]
|
|
57
|
+
ignore = ["ANN401"]
|
|
58
|
+
|
|
59
|
+
[tool.ruff.lint.per-file-ignores]
|
|
60
|
+
"tests/**" = ["ANN"]
|
{aevum_store_postgres-0.2.0 → aevum_store_postgres-0.3.0}/src/aevum/store/postgres/__init__.py
RENAMED
|
@@ -1,30 +1,30 @@
|
|
|
1
|
-
"""
|
|
2
|
-
aevum.store.postgres -- PostgreSQL backends for graph, consent, and ledger.
|
|
3
|
-
|
|
4
|
-
Usage:
|
|
5
|
-
from aevum.store.postgres import PostgresStore, PostgresConsentLedger, PostgresLedger
|
|
6
|
-
from aevum.store.postgres.ledger import initialize_ledger_schema
|
|
7
|
-
import psycopg
|
|
8
|
-
|
|
9
|
-
conn = psycopg.connect("postgresql://user:pass@host/dbname")
|
|
10
|
-
initialize_schema(conn) # graph + consent DDL
|
|
11
|
-
initialize_ledger_schema(conn) # ledger DDL
|
|
12
|
-
|
|
13
|
-
store = PostgresStore(conn)
|
|
14
|
-
consent = PostgresConsentLedger(conn)
|
|
15
|
-
ledger = PostgresLedger(conn, sigchain)
|
|
16
|
-
|
|
17
|
-
engine = Engine(
|
|
18
|
-
graph_store=store,
|
|
19
|
-
consent_ledger=consent,
|
|
20
|
-
ledger=ledger,
|
|
21
|
-
)
|
|
22
|
-
"""
|
|
23
|
-
|
|
24
|
-
from aevum.store.postgres.consent import PostgresConsentLedger
|
|
25
|
-
from aevum.store.postgres.ledger import PostgresLedger
|
|
26
|
-
from aevum.store.postgres.store import PostgresStore
|
|
27
|
-
|
|
28
|
-
__version__ = "0.1.0"
|
|
29
|
-
|
|
30
|
-
__all__ = ["PostgresStore", "PostgresConsentLedger", "PostgresLedger"]
|
|
1
|
+
"""
|
|
2
|
+
aevum.store.postgres -- PostgreSQL backends for graph, consent, and ledger.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
from aevum.store.postgres import PostgresStore, PostgresConsentLedger, PostgresLedger
|
|
6
|
+
from aevum.store.postgres.ledger import initialize_ledger_schema
|
|
7
|
+
import psycopg
|
|
8
|
+
|
|
9
|
+
conn = psycopg.connect("postgresql://user:pass@host/dbname")
|
|
10
|
+
initialize_schema(conn) # graph + consent DDL
|
|
11
|
+
initialize_ledger_schema(conn) # ledger DDL
|
|
12
|
+
|
|
13
|
+
store = PostgresStore(conn)
|
|
14
|
+
consent = PostgresConsentLedger(conn)
|
|
15
|
+
ledger = PostgresLedger(conn, sigchain)
|
|
16
|
+
|
|
17
|
+
engine = Engine(
|
|
18
|
+
graph_store=store,
|
|
19
|
+
consent_ledger=consent,
|
|
20
|
+
ledger=ledger,
|
|
21
|
+
)
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from aevum.store.postgres.consent import PostgresConsentLedger
|
|
25
|
+
from aevum.store.postgres.ledger import PostgresLedger
|
|
26
|
+
from aevum.store.postgres.store import PostgresStore
|
|
27
|
+
|
|
28
|
+
__version__ = "0.1.0"
|
|
29
|
+
|
|
30
|
+
__all__ = ["PostgresStore", "PostgresConsentLedger", "PostgresLedger"]
|
{aevum_store_postgres-0.2.0 → aevum_store_postgres-0.3.0}/src/aevum/store/postgres/consent.py
RENAMED
|
@@ -1,110 +1,110 @@
|
|
|
1
|
-
"""
|
|
2
|
-
PostgresConsentLedger — ConsentLedgerProtocol backed by PostgreSQL.
|
|
3
|
-
|
|
4
|
-
Grants stored as JSONB in aevum_consent_grants.
|
|
5
|
-
OR-Set semantics: revocation wins over concurrent grants.
|
|
6
|
-
Expiration checked in Python (matching InMemoryConsentLedger behaviour).
|
|
7
|
-
"""
|
|
8
|
-
|
|
9
|
-
from __future__ import annotations
|
|
10
|
-
|
|
11
|
-
import contextlib
|
|
12
|
-
import json
|
|
13
|
-
import threading
|
|
14
|
-
from datetime import UTC, datetime
|
|
15
|
-
from typing import Any
|
|
16
|
-
|
|
17
|
-
from aevum.core.consent.models import ConsentGrant
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
class PostgresConsentLedger:
|
|
21
|
-
"""
|
|
22
|
-
ConsentLedgerProtocol implementation backed by PostgreSQL.
|
|
23
|
-
|
|
24
|
-
Args:
|
|
25
|
-
conn: An open psycopg.Connection (autocommit=True recommended).
|
|
26
|
-
lock: Optional shared Lock. Pass the same lock used by PostgresStore
|
|
27
|
-
when both share one connection.
|
|
28
|
-
"""
|
|
29
|
-
|
|
30
|
-
def __init__(self, conn: Any, lock: threading.Lock | None = None) -> None:
|
|
31
|
-
self._conn = conn
|
|
32
|
-
self._lock = lock or threading.Lock()
|
|
33
|
-
|
|
34
|
-
# ── ConsentLedgerProtocol ──────────────────────────────────────────────────
|
|
35
|
-
|
|
36
|
-
def add_grant(self, grant: ConsentGrant) -> None:
|
|
37
|
-
"""Upsert a consent grant. Replaces any prior record with the same grant_id."""
|
|
38
|
-
sql = """
|
|
39
|
-
INSERT INTO aevum_consent_grants (grant_id, grant_data)
|
|
40
|
-
VALUES (%s, %s::jsonb)
|
|
41
|
-
ON CONFLICT (grant_id) DO UPDATE SET grant_data = EXCLUDED.grant_data
|
|
42
|
-
"""
|
|
43
|
-
with self._lock, self._conn.cursor() as cur:
|
|
44
|
-
cur.execute(sql, (grant.grant_id, json.dumps(grant.model_dump())))
|
|
45
|
-
|
|
46
|
-
def revoke_grant(self, grant_id: str) -> None:
|
|
47
|
-
"""Mark a grant as revoked (immutable update via JSONB merge)."""
|
|
48
|
-
sql = """
|
|
49
|
-
UPDATE aevum_consent_grants
|
|
50
|
-
SET grant_data = jsonb_set(grant_data, '{revocation_status}', '"revoked"')
|
|
51
|
-
WHERE grant_id = %s
|
|
52
|
-
"""
|
|
53
|
-
with self._lock, self._conn.cursor() as cur:
|
|
54
|
-
cur.execute(sql, (grant_id,))
|
|
55
|
-
|
|
56
|
-
def has_consent(
|
|
57
|
-
self,
|
|
58
|
-
*,
|
|
59
|
-
subject_id: str,
|
|
60
|
-
operation: str,
|
|
61
|
-
grantee_id: str,
|
|
62
|
-
purpose: str | None = None,
|
|
63
|
-
) -> bool:
|
|
64
|
-
"""
|
|
65
|
-
Return True if an active, unexpired grant covers this operation.
|
|
66
|
-
|
|
67
|
-
Mirrors InMemoryConsentLedger: loads candidates from DB, checks
|
|
68
|
-
expiration and operation in Python to stay consistent with the spec.
|
|
69
|
-
"""
|
|
70
|
-
sql = """
|
|
71
|
-
SELECT grant_data
|
|
72
|
-
FROM aevum_consent_grants
|
|
73
|
-
WHERE grant_data->>'subject_id' = %s
|
|
74
|
-
AND grant_data->>'grantee_id' = %s
|
|
75
|
-
AND grant_data->>'revocation_status' = 'active'
|
|
76
|
-
"""
|
|
77
|
-
with self._lock, self._conn.cursor() as cur:
|
|
78
|
-
cur.execute(sql, (subject_id, grantee_id))
|
|
79
|
-
rows = cur.fetchall()
|
|
80
|
-
|
|
81
|
-
now = datetime.now(UTC)
|
|
82
|
-
for row in rows:
|
|
83
|
-
raw = row[0]
|
|
84
|
-
d: dict[str, Any] = json.loads(raw) if isinstance(raw, str) else raw
|
|
85
|
-
if operation not in d.get("operations", []):
|
|
86
|
-
continue
|
|
87
|
-
try:
|
|
88
|
-
expires = datetime.fromisoformat(
|
|
89
|
-
d["expires_at"].replace("Z", "+00:00")
|
|
90
|
-
)
|
|
91
|
-
if now > expires:
|
|
92
|
-
continue
|
|
93
|
-
except (KeyError, ValueError):
|
|
94
|
-
continue
|
|
95
|
-
return True
|
|
96
|
-
return False
|
|
97
|
-
|
|
98
|
-
def all_grants(self) -> list[ConsentGrant]:
|
|
99
|
-
"""Return all stored grants (active, revoked, and expired)."""
|
|
100
|
-
sql = "SELECT grant_data FROM aevum_consent_grants"
|
|
101
|
-
with self._lock, self._conn.cursor() as cur:
|
|
102
|
-
cur.execute(sql)
|
|
103
|
-
rows = cur.fetchall()
|
|
104
|
-
grants: list[ConsentGrant] = []
|
|
105
|
-
for row in rows:
|
|
106
|
-
raw = row[0]
|
|
107
|
-
d: dict[str, Any] = json.loads(raw) if isinstance(raw, str) else raw
|
|
108
|
-
with contextlib.suppress(Exception):
|
|
109
|
-
grants.append(ConsentGrant(**d))
|
|
110
|
-
return grants
|
|
1
|
+
"""
|
|
2
|
+
PostgresConsentLedger — ConsentLedgerProtocol backed by PostgreSQL.
|
|
3
|
+
|
|
4
|
+
Grants stored as JSONB in aevum_consent_grants.
|
|
5
|
+
OR-Set semantics: revocation wins over concurrent grants.
|
|
6
|
+
Expiration checked in Python (matching InMemoryConsentLedger behaviour).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import contextlib
|
|
12
|
+
import json
|
|
13
|
+
import threading
|
|
14
|
+
from datetime import UTC, datetime
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
from aevum.core.consent.models import ConsentGrant
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class PostgresConsentLedger:
|
|
21
|
+
"""
|
|
22
|
+
ConsentLedgerProtocol implementation backed by PostgreSQL.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
conn: An open psycopg.Connection (autocommit=True recommended).
|
|
26
|
+
lock: Optional shared Lock. Pass the same lock used by PostgresStore
|
|
27
|
+
when both share one connection.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(self, conn: Any, lock: threading.Lock | None = None) -> None:
|
|
31
|
+
self._conn = conn
|
|
32
|
+
self._lock = lock or threading.Lock()
|
|
33
|
+
|
|
34
|
+
# ── ConsentLedgerProtocol ──────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
def add_grant(self, grant: ConsentGrant) -> None:
|
|
37
|
+
"""Upsert a consent grant. Replaces any prior record with the same grant_id."""
|
|
38
|
+
sql = """
|
|
39
|
+
INSERT INTO aevum_consent_grants (grant_id, grant_data)
|
|
40
|
+
VALUES (%s, %s::jsonb)
|
|
41
|
+
ON CONFLICT (grant_id) DO UPDATE SET grant_data = EXCLUDED.grant_data
|
|
42
|
+
"""
|
|
43
|
+
with self._lock, self._conn.cursor() as cur:
|
|
44
|
+
cur.execute(sql, (grant.grant_id, json.dumps(grant.model_dump())))
|
|
45
|
+
|
|
46
|
+
def revoke_grant(self, grant_id: str) -> None:
|
|
47
|
+
"""Mark a grant as revoked (immutable update via JSONB merge)."""
|
|
48
|
+
sql = """
|
|
49
|
+
UPDATE aevum_consent_grants
|
|
50
|
+
SET grant_data = jsonb_set(grant_data, '{revocation_status}', '"revoked"')
|
|
51
|
+
WHERE grant_id = %s
|
|
52
|
+
"""
|
|
53
|
+
with self._lock, self._conn.cursor() as cur:
|
|
54
|
+
cur.execute(sql, (grant_id,))
|
|
55
|
+
|
|
56
|
+
def has_consent(
|
|
57
|
+
self,
|
|
58
|
+
*,
|
|
59
|
+
subject_id: str,
|
|
60
|
+
operation: str,
|
|
61
|
+
grantee_id: str,
|
|
62
|
+
purpose: str | None = None,
|
|
63
|
+
) -> bool:
|
|
64
|
+
"""
|
|
65
|
+
Return True if an active, unexpired grant covers this operation.
|
|
66
|
+
|
|
67
|
+
Mirrors InMemoryConsentLedger: loads candidates from DB, checks
|
|
68
|
+
expiration and operation in Python to stay consistent with the spec.
|
|
69
|
+
"""
|
|
70
|
+
sql = """
|
|
71
|
+
SELECT grant_data
|
|
72
|
+
FROM aevum_consent_grants
|
|
73
|
+
WHERE grant_data->>'subject_id' = %s
|
|
74
|
+
AND grant_data->>'grantee_id' = %s
|
|
75
|
+
AND grant_data->>'revocation_status' = 'active'
|
|
76
|
+
"""
|
|
77
|
+
with self._lock, self._conn.cursor() as cur:
|
|
78
|
+
cur.execute(sql, (subject_id, grantee_id))
|
|
79
|
+
rows = cur.fetchall()
|
|
80
|
+
|
|
81
|
+
now = datetime.now(UTC)
|
|
82
|
+
for row in rows:
|
|
83
|
+
raw = row[0]
|
|
84
|
+
d: dict[str, Any] = json.loads(raw) if isinstance(raw, str) else raw
|
|
85
|
+
if operation not in d.get("operations", []):
|
|
86
|
+
continue
|
|
87
|
+
try:
|
|
88
|
+
expires = datetime.fromisoformat(
|
|
89
|
+
d["expires_at"].replace("Z", "+00:00")
|
|
90
|
+
)
|
|
91
|
+
if now > expires:
|
|
92
|
+
continue
|
|
93
|
+
except (KeyError, ValueError):
|
|
94
|
+
continue
|
|
95
|
+
return True
|
|
96
|
+
return False
|
|
97
|
+
|
|
98
|
+
def all_grants(self) -> list[ConsentGrant]:
|
|
99
|
+
"""Return all stored grants (active, revoked, and expired)."""
|
|
100
|
+
sql = "SELECT grant_data FROM aevum_consent_grants"
|
|
101
|
+
with self._lock, self._conn.cursor() as cur:
|
|
102
|
+
cur.execute(sql)
|
|
103
|
+
rows = cur.fetchall()
|
|
104
|
+
grants: list[ConsentGrant] = []
|
|
105
|
+
for row in rows:
|
|
106
|
+
raw = row[0]
|
|
107
|
+
d: dict[str, Any] = json.loads(raw) if isinstance(raw, str) else raw
|
|
108
|
+
with contextlib.suppress(Exception):
|
|
109
|
+
grants.append(ConsentGrant(**d))
|
|
110
|
+
return grants
|