nl2data-semantic-catalog-postgres 0.1.0__py3-none-any.whl
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.
- nl2data_semantic_catalog_postgres/__init__.py +18 -0
- nl2data_semantic_catalog_postgres/client.py +139 -0
- nl2data_semantic_catalog_postgres/config.py +136 -0
- nl2data_semantic_catalog_postgres/envelope.py +327 -0
- nl2data_semantic_catalog_postgres/errors.py +188 -0
- nl2data_semantic_catalog_postgres/fake_postgres/__init__.py +53 -0
- nl2data_semantic_catalog_postgres/fake_postgres/driver.py +209 -0
- nl2data_semantic_catalog_postgres/fake_postgres/handlers_audit.py +243 -0
- nl2data_semantic_catalog_postgres/fake_postgres/handlers_drafts.py +84 -0
- nl2data_semantic_catalog_postgres/fake_postgres/handlers_maintenance.py +263 -0
- nl2data_semantic_catalog_postgres/fake_postgres/handlers_publications.py +249 -0
- nl2data_semantic_catalog_postgres/fake_postgres/handlers_schema.py +23 -0
- nl2data_semantic_catalog_postgres/fake_postgres/handlers_snapshots.py +197 -0
- nl2data_semantic_catalog_postgres/fake_postgres/handlers_versions.py +330 -0
- nl2data_semantic_catalog_postgres/fake_postgres/keys.py +99 -0
- nl2data_semantic_catalog_postgres/fake_postgres/pool.py +152 -0
- nl2data_semantic_catalog_postgres/fake_postgres/registry.py +144 -0
- nl2data_semantic_catalog_postgres/maintenance.py +235 -0
- nl2data_semantic_catalog_postgres/py.typed +0 -0
- nl2data_semantic_catalog_postgres/repositories/__init__.py +26 -0
- nl2data_semantic_catalog_postgres/repositories/activation.py +849 -0
- nl2data_semantic_catalog_postgres/repositories/audit_evidence.py +221 -0
- nl2data_semantic_catalog_postgres/repositories/drafts.py +156 -0
- nl2data_semantic_catalog_postgres/repositories/evidence.py +415 -0
- nl2data_semantic_catalog_postgres/repositories/publications.py +486 -0
- nl2data_semantic_catalog_postgres/repositories/snapshots.py +372 -0
- nl2data_semantic_catalog_postgres/schema.py +337 -0
- nl2data_semantic_catalog_postgres/sql.py +520 -0
- nl2data_semantic_catalog_postgres/store.py +716 -0
- nl2data_semantic_catalog_postgres/unit_of_work.py +589 -0
- nl2data_semantic_catalog_postgres-0.1.0.dist-info/METADATA +76 -0
- nl2data_semantic_catalog_postgres-0.1.0.dist-info/RECORD +34 -0
- nl2data_semantic_catalog_postgres-0.1.0.dist-info/WHEEL +5 -0
- nl2data_semantic_catalog_postgres-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""Thread-safe in-memory pool executing the catalog's SQL statement set.
|
|
2
|
+
|
|
3
|
+
Expose ``snapshots``/``snapshot_pointers``/``proposal_sets``/``publications``/
|
|
4
|
+
``bundle_pointers``/``bundle_history``/``events``/``clock`` for direct
|
|
5
|
+
assertions in tests. Statement dispatch goes through the domain handler
|
|
6
|
+
registry in :mod:`.registry`; unrecognized statements fail loudly with
|
|
7
|
+
``AssertionError``.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import contextlib
|
|
13
|
+
import threading
|
|
14
|
+
from collections.abc import Iterator
|
|
15
|
+
from datetime import datetime
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
from ..sql import SQL_TEMPLATES
|
|
19
|
+
from .driver import (
|
|
20
|
+
FakeClock,
|
|
21
|
+
OperationalError,
|
|
22
|
+
PoolClosed,
|
|
23
|
+
_FakeConnection,
|
|
24
|
+
_FakeCursor,
|
|
25
|
+
_monotonic,
|
|
26
|
+
_normalize,
|
|
27
|
+
_PassFailure,
|
|
28
|
+
)
|
|
29
|
+
from .registry import HANDLERS
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class FakePostgresPool:
|
|
33
|
+
"""Thread-safe in-memory pool executing the catalog's SQL statement set."""
|
|
34
|
+
|
|
35
|
+
def __init__(self, *, start: datetime | None = None) -> None:
|
|
36
|
+
self.clock = FakeClock(start)
|
|
37
|
+
self.snapshots: dict[tuple[str, str], dict[str, Any]] = {}
|
|
38
|
+
self.snapshot_pointers: dict[tuple[str, str], dict[str, Any]] = {}
|
|
39
|
+
self.proposal_sets: dict[tuple[str, str], dict[str, Any]] = {}
|
|
40
|
+
self.assembly_drafts: dict[tuple[str, str], dict[str, Any]] = {}
|
|
41
|
+
self.publications: dict[tuple[str, str, str], dict[str, Any]] = {}
|
|
42
|
+
self.accepted_manifests: dict[tuple[str, str, str], dict[str, Any]] = {}
|
|
43
|
+
self.verification_evidence: dict[tuple[str, str, str], dict[str, Any]] = {}
|
|
44
|
+
self.publish_audits: dict[tuple[str, str, str], dict[str, Any]] = {}
|
|
45
|
+
self.published_versions: dict[tuple[str, str, str], dict[str, Any]] = {}
|
|
46
|
+
self.supersession_edges: dict[tuple[str, str, str], dict[str, Any]] = {}
|
|
47
|
+
self.bundle_pointers: dict[tuple[str, str], dict[str, Any]] = {}
|
|
48
|
+
self.bundle_history: dict[tuple[str, str], dict[int, dict[str, Any]]] = {}
|
|
49
|
+
self.events: dict[tuple[str, str], dict[str, Any]] = {}
|
|
50
|
+
self.publication_audit_evidence: dict[
|
|
51
|
+
tuple[str, str, str], dict[str, Any]
|
|
52
|
+
] = {}
|
|
53
|
+
self.audit_entries: dict[tuple[str, str], dict[str, Any]] = {}
|
|
54
|
+
self.schema_metadata: dict[str, str] = {}
|
|
55
|
+
self._closed = False
|
|
56
|
+
self._struct_lock = threading.RLock()
|
|
57
|
+
self._lock_owner: dict[tuple[Any, ...], str] = {}
|
|
58
|
+
self._lock_cond = threading.Condition(self._struct_lock)
|
|
59
|
+
self._pending_failures: list[BaseException | _PassFailure] = []
|
|
60
|
+
self.statement_journal: list[str] = []
|
|
61
|
+
self._statement_names = {
|
|
62
|
+
_normalize(template.format(schema='"catalog"')): name
|
|
63
|
+
for name, template in SQL_TEMPLATES.items()
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
# -- lifecycle --------------------------------------------------------
|
|
67
|
+
|
|
68
|
+
@contextlib.contextmanager
|
|
69
|
+
def connection(self) -> Iterator[_FakeConnection]:
|
|
70
|
+
"""One transaction-backed connection; ``commit``/``rollback`` explicit."""
|
|
71
|
+
with self._struct_lock:
|
|
72
|
+
if self._closed:
|
|
73
|
+
raise PoolClosed("fake pool is closed")
|
|
74
|
+
connection = _FakeConnection(self)
|
|
75
|
+
try:
|
|
76
|
+
yield connection
|
|
77
|
+
finally:
|
|
78
|
+
connection._finish()
|
|
79
|
+
|
|
80
|
+
def close(self) -> None:
|
|
81
|
+
"""Close the pool; later checkouts fail with ``PoolClosed``."""
|
|
82
|
+
with self._struct_lock:
|
|
83
|
+
self._closed = True
|
|
84
|
+
|
|
85
|
+
def set_schema_version(self, version: int) -> None:
|
|
86
|
+
"""Seed ``schema_metadata`` so catalog initialization sees a version."""
|
|
87
|
+
with self._struct_lock:
|
|
88
|
+
self.schema_metadata["schema_version"] = str(version)
|
|
89
|
+
|
|
90
|
+
def fail_next(
|
|
91
|
+
self, error: BaseException, *, count: int = 1, after: int = 0
|
|
92
|
+
) -> None:
|
|
93
|
+
"""Fail the next ``count`` statements, after ``after`` pass-throughs.
|
|
94
|
+
|
|
95
|
+
``after`` lets a multi-statement operation commit earlier writes and
|
|
96
|
+
then fail mid-transaction, so store rollback of partial work can be
|
|
97
|
+
asserted deterministically.
|
|
98
|
+
"""
|
|
99
|
+
with self._struct_lock:
|
|
100
|
+
self._pending_failures.extend([_PassFailure()] * after)
|
|
101
|
+
self._pending_failures.extend([error] * count)
|
|
102
|
+
|
|
103
|
+
# -- row locking ------------------------------------------------------
|
|
104
|
+
|
|
105
|
+
def _lock(
|
|
106
|
+
self, connection: _FakeConnection, key: tuple[Any, ...], timeout: float
|
|
107
|
+
) -> None:
|
|
108
|
+
"""Acquire a row lock, blocking while another connection holds it."""
|
|
109
|
+
deadline = _monotonic() + timeout
|
|
110
|
+
with self._lock_cond:
|
|
111
|
+
while self._lock_owner.get(key) not in (None, connection.id):
|
|
112
|
+
remaining = deadline - _monotonic()
|
|
113
|
+
if remaining <= 0:
|
|
114
|
+
raise OperationalError("lock wait timeout exceeded")
|
|
115
|
+
self._lock_cond.wait(remaining)
|
|
116
|
+
self._lock_owner[key] = connection.id
|
|
117
|
+
connection._owned_keys.add(key)
|
|
118
|
+
|
|
119
|
+
def _release(self, connection: _FakeConnection) -> None:
|
|
120
|
+
"""Release every row lock owned by the connection."""
|
|
121
|
+
with self._lock_cond:
|
|
122
|
+
for key in connection._owned_keys:
|
|
123
|
+
if self._lock_owner.get(key) == connection.id:
|
|
124
|
+
del self._lock_owner[key]
|
|
125
|
+
connection._owned_keys.clear()
|
|
126
|
+
self._lock_cond.notify_all()
|
|
127
|
+
|
|
128
|
+
# -- statement dispatch -----------------------------------------------
|
|
129
|
+
|
|
130
|
+
def _execute(
|
|
131
|
+
self,
|
|
132
|
+
connection: _FakeConnection,
|
|
133
|
+
sql: str,
|
|
134
|
+
params: tuple[Any, ...],
|
|
135
|
+
timeout: float,
|
|
136
|
+
) -> _FakeCursor:
|
|
137
|
+
with self._struct_lock:
|
|
138
|
+
while self._pending_failures:
|
|
139
|
+
failure = self._pending_failures.pop(0)
|
|
140
|
+
if not isinstance(failure, _PassFailure):
|
|
141
|
+
raise failure
|
|
142
|
+
normalized = _normalize(sql)
|
|
143
|
+
if normalized.startswith("CREATE "):
|
|
144
|
+
# DDL is emulated: tables exist by construction.
|
|
145
|
+
return _FakeCursor(self, connection, [], 0)
|
|
146
|
+
name = self._statement_names.get(normalized)
|
|
147
|
+
if name is None:
|
|
148
|
+
raise AssertionError(f"fake pool does not recognize statement: {sql[:240]}")
|
|
149
|
+
self.statement_journal.append(name)
|
|
150
|
+
handler = HANDLERS[name]
|
|
151
|
+
rows, rowcount = handler(self, connection, params or (), timeout)
|
|
152
|
+
return _FakeCursor(self, connection, rows, rowcount)
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"""Statement-name to handler registry for the fake pool.
|
|
2
|
+
|
|
3
|
+
Each entry maps one production SQL template name (the exact key set of
|
|
4
|
+
:data:`SQL_TEMPLATES`) to its domain handler, so template drift between
|
|
5
|
+
the store and the fake is caught at the first unrecognized statement.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from collections.abc import Callable
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from .handlers_audit import (
|
|
14
|
+
_h_count_audit_evidence,
|
|
15
|
+
_h_insert_audit_entry,
|
|
16
|
+
_h_insert_publication_audit_evidence,
|
|
17
|
+
_h_list_audit_evidence,
|
|
18
|
+
_h_read_audit_entry,
|
|
19
|
+
_h_read_latest_publication_entry,
|
|
20
|
+
_h_read_publication_audit_entry,
|
|
21
|
+
_h_read_publication_audit_evidence,
|
|
22
|
+
)
|
|
23
|
+
from .handlers_drafts import (
|
|
24
|
+
_h_insert_assembly_draft,
|
|
25
|
+
_h_lock_assembly_draft,
|
|
26
|
+
_h_read_assembly_draft,
|
|
27
|
+
_h_replace_assembly_draft,
|
|
28
|
+
)
|
|
29
|
+
from .handlers_maintenance import (
|
|
30
|
+
_h_delete_expired_audit_entries,
|
|
31
|
+
_h_delete_expired_events,
|
|
32
|
+
_h_delete_expired_publications,
|
|
33
|
+
_h_delete_expired_snapshots,
|
|
34
|
+
_h_insert_event,
|
|
35
|
+
)
|
|
36
|
+
from .handlers_publications import (
|
|
37
|
+
_h_insert_accepted_manifest,
|
|
38
|
+
_h_insert_publication,
|
|
39
|
+
_h_insert_publish_audit,
|
|
40
|
+
_h_insert_verification_evidence,
|
|
41
|
+
_h_list_publications,
|
|
42
|
+
_h_lock_publication_series,
|
|
43
|
+
_h_read_accepted_manifest,
|
|
44
|
+
_h_read_publication,
|
|
45
|
+
_h_read_publication_by_fingerprint,
|
|
46
|
+
_h_read_publication_fingerprint,
|
|
47
|
+
_h_read_publish_audit,
|
|
48
|
+
_h_read_publish_by_idempotency_key,
|
|
49
|
+
_h_read_verification_evidence,
|
|
50
|
+
)
|
|
51
|
+
from .handlers_schema import _h_read_schema_version, _h_write_schema_version
|
|
52
|
+
from .handlers_snapshots import (
|
|
53
|
+
_h_list_snapshot_pointers,
|
|
54
|
+
_h_lock_snapshot_row,
|
|
55
|
+
_h_read_proposal_set,
|
|
56
|
+
_h_read_snapshot_envelope,
|
|
57
|
+
_h_read_snapshot_pointer,
|
|
58
|
+
_h_set_snapshot_state,
|
|
59
|
+
_h_snapshot_exists,
|
|
60
|
+
_h_upsert_proposal_set,
|
|
61
|
+
_h_upsert_snapshot,
|
|
62
|
+
_h_upsert_snapshot_pointer,
|
|
63
|
+
)
|
|
64
|
+
from .handlers_versions import (
|
|
65
|
+
_h_delete_history_top,
|
|
66
|
+
_h_insert_history,
|
|
67
|
+
_h_insert_published_version,
|
|
68
|
+
_h_insert_supersession_edge,
|
|
69
|
+
_h_list_bundle_pointers,
|
|
70
|
+
_h_list_orphan_active_versions,
|
|
71
|
+
_h_list_published_versions,
|
|
72
|
+
_h_lock_bundle_pointer,
|
|
73
|
+
_h_next_history_position,
|
|
74
|
+
_h_read_bundle_pointer,
|
|
75
|
+
_h_read_history_top,
|
|
76
|
+
_h_read_latest_version,
|
|
77
|
+
_h_read_published_version,
|
|
78
|
+
_h_set_published_version_state,
|
|
79
|
+
_h_trim_history,
|
|
80
|
+
_h_update_version_successor,
|
|
81
|
+
_h_upsert_bundle_pointer,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
HANDLERS: dict[str, Callable[..., tuple[list[dict[str, Any]], int]]] = {
|
|
85
|
+
"read_schema_version": _h_read_schema_version,
|
|
86
|
+
"write_schema_version": _h_write_schema_version,
|
|
87
|
+
"upsert_snapshot": _h_upsert_snapshot,
|
|
88
|
+
"read_snapshot_envelope": _h_read_snapshot_envelope,
|
|
89
|
+
"lock_snapshot_row": _h_lock_snapshot_row,
|
|
90
|
+
"set_snapshot_state": _h_set_snapshot_state,
|
|
91
|
+
"snapshot_exists": _h_snapshot_exists,
|
|
92
|
+
"upsert_snapshot_pointer": _h_upsert_snapshot_pointer,
|
|
93
|
+
"read_snapshot_pointer": _h_read_snapshot_pointer,
|
|
94
|
+
"list_snapshot_pointers": _h_list_snapshot_pointers,
|
|
95
|
+
"upsert_proposal_set": _h_upsert_proposal_set,
|
|
96
|
+
"read_proposal_set": _h_read_proposal_set,
|
|
97
|
+
"insert_assembly_draft": _h_insert_assembly_draft,
|
|
98
|
+
"read_assembly_draft": _h_read_assembly_draft,
|
|
99
|
+
"lock_assembly_draft": _h_lock_assembly_draft,
|
|
100
|
+
"replace_assembly_draft": _h_replace_assembly_draft,
|
|
101
|
+
"insert_publication": _h_insert_publication,
|
|
102
|
+
"read_publication": _h_read_publication,
|
|
103
|
+
"read_publication_fingerprint": _h_read_publication_fingerprint,
|
|
104
|
+
"read_publication_by_fingerprint": _h_read_publication_by_fingerprint,
|
|
105
|
+
"lock_publication_series": _h_lock_publication_series,
|
|
106
|
+
"list_publications": _h_list_publications,
|
|
107
|
+
"insert_accepted_manifest": _h_insert_accepted_manifest,
|
|
108
|
+
"read_accepted_manifest": _h_read_accepted_manifest,
|
|
109
|
+
"insert_verification_evidence": _h_insert_verification_evidence,
|
|
110
|
+
"read_verification_evidence": _h_read_verification_evidence,
|
|
111
|
+
"insert_publish_audit": _h_insert_publish_audit,
|
|
112
|
+
"read_publish_audit": _h_read_publish_audit,
|
|
113
|
+
"read_publish_by_idempotency_key": _h_read_publish_by_idempotency_key,
|
|
114
|
+
"read_latest_version": _h_read_latest_version,
|
|
115
|
+
"insert_published_version": _h_insert_published_version,
|
|
116
|
+
"update_version_successor": _h_update_version_successor,
|
|
117
|
+
"insert_supersession_edge": _h_insert_supersession_edge,
|
|
118
|
+
"read_published_version": _h_read_published_version,
|
|
119
|
+
"list_published_versions": _h_list_published_versions,
|
|
120
|
+
"set_published_version_state": _h_set_published_version_state,
|
|
121
|
+
"upsert_bundle_pointer": _h_upsert_bundle_pointer,
|
|
122
|
+
"read_bundle_pointer": _h_read_bundle_pointer,
|
|
123
|
+
"lock_bundle_pointer": _h_lock_bundle_pointer,
|
|
124
|
+
"next_history_position": _h_next_history_position,
|
|
125
|
+
"insert_history": _h_insert_history,
|
|
126
|
+
"read_history_top": _h_read_history_top,
|
|
127
|
+
"delete_history_top": _h_delete_history_top,
|
|
128
|
+
"trim_history": _h_trim_history,
|
|
129
|
+
"list_bundle_pointers": _h_list_bundle_pointers,
|
|
130
|
+
"list_orphan_active_versions": _h_list_orphan_active_versions,
|
|
131
|
+
"insert_event": _h_insert_event,
|
|
132
|
+
"insert_publication_audit_evidence": _h_insert_publication_audit_evidence,
|
|
133
|
+
"read_publication_audit_evidence": _h_read_publication_audit_evidence,
|
|
134
|
+
"insert_audit_entry": _h_insert_audit_entry,
|
|
135
|
+
"read_audit_entry": _h_read_audit_entry,
|
|
136
|
+
"read_publication_audit_entry": _h_read_publication_audit_entry,
|
|
137
|
+
"read_latest_publication_entry": _h_read_latest_publication_entry,
|
|
138
|
+
"count_audit_evidence": _h_count_audit_evidence,
|
|
139
|
+
"list_audit_evidence": _h_list_audit_evidence,
|
|
140
|
+
"delete_expired_snapshots": _h_delete_expired_snapshots,
|
|
141
|
+
"delete_expired_publications": _h_delete_expired_publications,
|
|
142
|
+
"delete_expired_events": _h_delete_expired_events,
|
|
143
|
+
"delete_expired_audit_entries": _h_delete_expired_audit_entries,
|
|
144
|
+
}
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
"""Bounded maintenance and startup revalidation for the durable catalog.
|
|
2
|
+
|
|
3
|
+
The store facade delegates these cross-domain sweeps here: ``cleanup``
|
|
4
|
+
removes expired inactive records in bounded batches while preserving
|
|
5
|
+
active content and required dependencies, and ``reload_active``
|
|
6
|
+
revalidates every active snapshot/Bundle pointer after startup so a
|
|
7
|
+
newer persisted schema or envelope version fails closed.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from datetime import datetime, timedelta
|
|
13
|
+
|
|
14
|
+
from nl2data_core.bundles.publication import (
|
|
15
|
+
LifecycleWitnessError,
|
|
16
|
+
PublishedVersionState,
|
|
17
|
+
validate_lifecycle_witness,
|
|
18
|
+
witness_cause_type,
|
|
19
|
+
)
|
|
20
|
+
from nl2data_core.metadata.catalog import CatalogReloadIssue, CatalogReloadReport
|
|
21
|
+
|
|
22
|
+
from .envelope import ArtifactKind
|
|
23
|
+
from .errors import SemanticCatalogError, SemanticCatalogErrorCode
|
|
24
|
+
from .repositories.evidence import EvidenceRepository
|
|
25
|
+
from .unit_of_work import CatalogUnitOfWork
|
|
26
|
+
|
|
27
|
+
__all__ = ["cleanup", "reload_active"]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def cleanup(uow: CatalogUnitOfWork, *, now: datetime | None = None) -> int:
|
|
31
|
+
"""Remove expired inactive records; preserves active content.
|
|
32
|
+
|
|
33
|
+
Expired snapshots that no pointer references, expired publications
|
|
34
|
+
that neither pointer nor history references (and that no publication
|
|
35
|
+
depends on), expired lifecycle events, and expired audit-evidence
|
|
36
|
+
entries (unless their bundle fingerprint belongs to a non-retired
|
|
37
|
+
published version, or a protected entry references them as a
|
|
38
|
+
predecessor) are removed in bounded batches. Active snapshots,
|
|
39
|
+
active Bundles, and required dependencies are never removed.
|
|
40
|
+
"""
|
|
41
|
+
config = uow.config
|
|
42
|
+
current = uow.now() if now is None else now
|
|
43
|
+
event_cutoff = current - timedelta(seconds=config.event_retention_seconds)
|
|
44
|
+
audit_cutoff = current - timedelta(seconds=config.audit_retention_seconds)
|
|
45
|
+
total = 0
|
|
46
|
+
with uow.transaction() as conn:
|
|
47
|
+
cursor = uow.execute(
|
|
48
|
+
conn,
|
|
49
|
+
"delete_expired_snapshots",
|
|
50
|
+
(current, config.cleanup_batch_size),
|
|
51
|
+
)
|
|
52
|
+
total += int(cursor.rowcount or 0)
|
|
53
|
+
cursor = uow.execute(
|
|
54
|
+
conn,
|
|
55
|
+
"delete_expired_publications",
|
|
56
|
+
(current, config.cleanup_batch_size),
|
|
57
|
+
)
|
|
58
|
+
total += int(cursor.rowcount or 0)
|
|
59
|
+
cursor = uow.execute(
|
|
60
|
+
conn,
|
|
61
|
+
"delete_expired_events",
|
|
62
|
+
(event_cutoff, config.cleanup_batch_size),
|
|
63
|
+
)
|
|
64
|
+
total += int(cursor.rowcount or 0)
|
|
65
|
+
cursor = uow.execute(
|
|
66
|
+
conn,
|
|
67
|
+
"delete_expired_audit_entries",
|
|
68
|
+
(audit_cutoff, config.cleanup_batch_size),
|
|
69
|
+
)
|
|
70
|
+
total += int(cursor.rowcount or 0)
|
|
71
|
+
uow.insert_event(
|
|
72
|
+
conn,
|
|
73
|
+
"cleanup",
|
|
74
|
+
None,
|
|
75
|
+
namespace="",
|
|
76
|
+
occurred_at=current,
|
|
77
|
+
)
|
|
78
|
+
return total
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def reload_active(uow: CatalogUnitOfWork, *, now: datetime | None = None) -> CatalogReloadReport:
|
|
82
|
+
"""Revalidate every active snapshot/Bundle pointer after startup.
|
|
83
|
+
|
|
84
|
+
A newer persisted schema or envelope version fails closed; active
|
|
85
|
+
pointers whose artifact no longer revalidates are reported as
|
|
86
|
+
rejected and are never exposed for query-time resolution (reads
|
|
87
|
+
revalidate independently and fail closed). When an active Bundle
|
|
88
|
+
publication carries verification evidence, its immutable frozen
|
|
89
|
+
binding, audit, and manifest cross-links are revalidated too, so a
|
|
90
|
+
corrupted or legacy evidence record is reported as rejected instead
|
|
91
|
+
of silently passing startup revalidation.
|
|
92
|
+
"""
|
|
93
|
+
checked_at = uow.now() if now is None else now
|
|
94
|
+
issues: list[CatalogReloadIssue] = []
|
|
95
|
+
snapshots_revalidated = 0
|
|
96
|
+
bundles_revalidated = 0
|
|
97
|
+
evidence = EvidenceRepository(uow)
|
|
98
|
+
with uow.transaction() as conn:
|
|
99
|
+
for pointer in uow.execute(conn, "list_snapshot_pointers").fetchall():
|
|
100
|
+
if len(issues) >= 16:
|
|
101
|
+
break
|
|
102
|
+
row = uow.execute(
|
|
103
|
+
conn,
|
|
104
|
+
"read_snapshot_envelope",
|
|
105
|
+
(
|
|
106
|
+
pointer["scope_namespace"],
|
|
107
|
+
pointer["snapshot_fingerprint"],
|
|
108
|
+
),
|
|
109
|
+
).fetchone()
|
|
110
|
+
try:
|
|
111
|
+
if row is None:
|
|
112
|
+
raise SemanticCatalogError(
|
|
113
|
+
SemanticCatalogErrorCode.ENVELOPE_REJECTED,
|
|
114
|
+
"active snapshot artifact is missing",
|
|
115
|
+
details={"cause_type": "MissingArtifact"},
|
|
116
|
+
)
|
|
117
|
+
envelope = uow.decode(
|
|
118
|
+
row["envelope"],
|
|
119
|
+
ArtifactKind.SNAPSHOT,
|
|
120
|
+
row_schema_version=row["schema_version"],
|
|
121
|
+
)
|
|
122
|
+
uow.snapshot_from_envelope(
|
|
123
|
+
envelope, discovered_at=row["discovered_at"]
|
|
124
|
+
)
|
|
125
|
+
snapshots_revalidated += 1
|
|
126
|
+
except SemanticCatalogError as error:
|
|
127
|
+
issues.append(
|
|
128
|
+
CatalogReloadIssue(
|
|
129
|
+
code=error.code.value,
|
|
130
|
+
message=error.message,
|
|
131
|
+
member_id=pointer["source_id"],
|
|
132
|
+
)
|
|
133
|
+
)
|
|
134
|
+
for pointer in uow.execute(conn, "list_bundle_pointers").fetchall():
|
|
135
|
+
if len(issues) >= 16:
|
|
136
|
+
break
|
|
137
|
+
row = uow.execute(
|
|
138
|
+
conn,
|
|
139
|
+
"read_publication",
|
|
140
|
+
(
|
|
141
|
+
pointer["scope_namespace"],
|
|
142
|
+
pointer["bundle_id"],
|
|
143
|
+
pointer["model_version"],
|
|
144
|
+
),
|
|
145
|
+
).fetchone()
|
|
146
|
+
try:
|
|
147
|
+
if row is None:
|
|
148
|
+
raise SemanticCatalogError(
|
|
149
|
+
SemanticCatalogErrorCode.ENVELOPE_REJECTED,
|
|
150
|
+
"active bundle artifact is missing",
|
|
151
|
+
details={"cause_type": "MissingArtifact"},
|
|
152
|
+
)
|
|
153
|
+
envelope = uow.decode(
|
|
154
|
+
row["envelope"],
|
|
155
|
+
ArtifactKind.BUNDLE,
|
|
156
|
+
row_schema_version=row["schema_version"],
|
|
157
|
+
)
|
|
158
|
+
bundle = uow.bundle_from_envelope(envelope)
|
|
159
|
+
version_row = uow.execute(
|
|
160
|
+
conn,
|
|
161
|
+
"read_published_version",
|
|
162
|
+
(
|
|
163
|
+
pointer["scope_namespace"],
|
|
164
|
+
pointer["bundle_id"],
|
|
165
|
+
bundle.fingerprint,
|
|
166
|
+
),
|
|
167
|
+
).fetchone()
|
|
168
|
+
if version_row is None:
|
|
169
|
+
# The pointer is a witness that this version was
|
|
170
|
+
# activated; a publication without its lifecycle row
|
|
171
|
+
# is corruption.
|
|
172
|
+
raise SemanticCatalogError(
|
|
173
|
+
SemanticCatalogErrorCode.ENVELOPE_REJECTED,
|
|
174
|
+
"active publication has no published version record",
|
|
175
|
+
details={"cause_type": "PublicationVersionMissing"},
|
|
176
|
+
)
|
|
177
|
+
try:
|
|
178
|
+
# The pointer carries redundant fingerprint and version
|
|
179
|
+
# witnesses and must agree with an ACTIVE version row.
|
|
180
|
+
validate_lifecycle_witness(
|
|
181
|
+
bundle,
|
|
182
|
+
witness="pointer",
|
|
183
|
+
witness_fingerprint=pointer["bundle_fingerprint"],
|
|
184
|
+
witness_model_version=pointer["model_version"],
|
|
185
|
+
lifecycle_state=PublishedVersionState(
|
|
186
|
+
version_row["lifecycle_state"]
|
|
187
|
+
),
|
|
188
|
+
require_state=PublishedVersionState.ACTIVE,
|
|
189
|
+
)
|
|
190
|
+
except LifecycleWitnessError as error:
|
|
191
|
+
raise SemanticCatalogError(
|
|
192
|
+
SemanticCatalogErrorCode.ENVELOPE_REJECTED,
|
|
193
|
+
error.message,
|
|
194
|
+
details={"cause_type": witness_cause_type(error.code)},
|
|
195
|
+
) from error
|
|
196
|
+
# Active publications must still satisfy their immutable
|
|
197
|
+
# frozen binding, audit, and manifest cross-links through
|
|
198
|
+
# the centralized integrity rule set; legacy compatibility
|
|
199
|
+
# records (plain Bundle, manifest-only) revalidate through
|
|
200
|
+
# the Bundle envelope and manifest bundle-match alone.
|
|
201
|
+
evidence.validated_publication_records(
|
|
202
|
+
conn,
|
|
203
|
+
pointer["scope_namespace"],
|
|
204
|
+
pointer["bundle_id"],
|
|
205
|
+
bundle.fingerprint,
|
|
206
|
+
audit_id=version_row["audit_id"],
|
|
207
|
+
)
|
|
208
|
+
bundles_revalidated += 1
|
|
209
|
+
except SemanticCatalogError as error:
|
|
210
|
+
issues.append(
|
|
211
|
+
CatalogReloadIssue(
|
|
212
|
+
code=error.code.value,
|
|
213
|
+
message=error.message,
|
|
214
|
+
member_id=pointer["bundle_id"],
|
|
215
|
+
)
|
|
216
|
+
)
|
|
217
|
+
# An ACTIVE version row without a pointer is invisible to the
|
|
218
|
+
# pointer sweep above; report it so orphaned lifecycle rows are
|
|
219
|
+
# never silently carried across a restart.
|
|
220
|
+
for row in uow.execute(conn, "list_orphan_active_versions", ()).fetchall():
|
|
221
|
+
if len(issues) >= 16:
|
|
222
|
+
break
|
|
223
|
+
issues.append(
|
|
224
|
+
CatalogReloadIssue(
|
|
225
|
+
code="orphan_active_version",
|
|
226
|
+
message="an active version exists without an active pointer",
|
|
227
|
+
member_id=row["bundle_id"],
|
|
228
|
+
)
|
|
229
|
+
)
|
|
230
|
+
return CatalogReloadReport(
|
|
231
|
+
checked_at=checked_at,
|
|
232
|
+
active_snapshots_revalidated=snapshots_revalidated,
|
|
233
|
+
active_bundles_revalidated=bundles_revalidated,
|
|
234
|
+
rejected=tuple(issues[:16]),
|
|
235
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Focused repositories over the shared catalog unit of work.
|
|
2
|
+
|
|
3
|
+
Repositories persist one domain each (metadata snapshots/proposal sets,
|
|
4
|
+
assembly drafts, Bundle publications, verification/audit evidence, and
|
|
5
|
+
activation/history). They operate on connections handed to them by a
|
|
6
|
+
transaction owner and never commit independently during atomic
|
|
7
|
+
cross-domain operations such as publication, activation, or rollback.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from .activation import ActivationRepository
|
|
13
|
+
from .audit_evidence import AuditEvidenceRepository
|
|
14
|
+
from .drafts import DraftRepository
|
|
15
|
+
from .evidence import EvidenceRepository
|
|
16
|
+
from .publications import PublicationRepository
|
|
17
|
+
from .snapshots import SnapshotRepository
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"ActivationRepository",
|
|
21
|
+
"AuditEvidenceRepository",
|
|
22
|
+
"DraftRepository",
|
|
23
|
+
"EvidenceRepository",
|
|
24
|
+
"PublicationRepository",
|
|
25
|
+
"SnapshotRepository",
|
|
26
|
+
]
|