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.
Files changed (34) hide show
  1. nl2data_semantic_catalog_postgres/__init__.py +18 -0
  2. nl2data_semantic_catalog_postgres/client.py +139 -0
  3. nl2data_semantic_catalog_postgres/config.py +136 -0
  4. nl2data_semantic_catalog_postgres/envelope.py +327 -0
  5. nl2data_semantic_catalog_postgres/errors.py +188 -0
  6. nl2data_semantic_catalog_postgres/fake_postgres/__init__.py +53 -0
  7. nl2data_semantic_catalog_postgres/fake_postgres/driver.py +209 -0
  8. nl2data_semantic_catalog_postgres/fake_postgres/handlers_audit.py +243 -0
  9. nl2data_semantic_catalog_postgres/fake_postgres/handlers_drafts.py +84 -0
  10. nl2data_semantic_catalog_postgres/fake_postgres/handlers_maintenance.py +263 -0
  11. nl2data_semantic_catalog_postgres/fake_postgres/handlers_publications.py +249 -0
  12. nl2data_semantic_catalog_postgres/fake_postgres/handlers_schema.py +23 -0
  13. nl2data_semantic_catalog_postgres/fake_postgres/handlers_snapshots.py +197 -0
  14. nl2data_semantic_catalog_postgres/fake_postgres/handlers_versions.py +330 -0
  15. nl2data_semantic_catalog_postgres/fake_postgres/keys.py +99 -0
  16. nl2data_semantic_catalog_postgres/fake_postgres/pool.py +152 -0
  17. nl2data_semantic_catalog_postgres/fake_postgres/registry.py +144 -0
  18. nl2data_semantic_catalog_postgres/maintenance.py +235 -0
  19. nl2data_semantic_catalog_postgres/py.typed +0 -0
  20. nl2data_semantic_catalog_postgres/repositories/__init__.py +26 -0
  21. nl2data_semantic_catalog_postgres/repositories/activation.py +849 -0
  22. nl2data_semantic_catalog_postgres/repositories/audit_evidence.py +221 -0
  23. nl2data_semantic_catalog_postgres/repositories/drafts.py +156 -0
  24. nl2data_semantic_catalog_postgres/repositories/evidence.py +415 -0
  25. nl2data_semantic_catalog_postgres/repositories/publications.py +486 -0
  26. nl2data_semantic_catalog_postgres/repositories/snapshots.py +372 -0
  27. nl2data_semantic_catalog_postgres/schema.py +337 -0
  28. nl2data_semantic_catalog_postgres/sql.py +520 -0
  29. nl2data_semantic_catalog_postgres/store.py +716 -0
  30. nl2data_semantic_catalog_postgres/unit_of_work.py +589 -0
  31. nl2data_semantic_catalog_postgres-0.1.0.dist-info/METADATA +76 -0
  32. nl2data_semantic_catalog_postgres-0.1.0.dist-info/RECORD +34 -0
  33. nl2data_semantic_catalog_postgres-0.1.0.dist-info/WHEEL +5 -0
  34. nl2data_semantic_catalog_postgres-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,221 @@
1
+ """Assembly audit-evidence trail repository over the shared unit of work.
2
+
3
+ Entries persist as bounded canonical envelopes whose fingerprint excludes
4
+ the presentation ``occurred_at`` timestamp, so the row column is applied
5
+ after reconstruction and the recomputed fingerprint is the tamper witness.
6
+ Every lookup is scoped by the opaque tenant namespace and returns a
7
+ deterministic, bounded trail page ordered by ``(occurred_at, event_id)``.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from collections.abc import Sequence
13
+ from typing import Any
14
+
15
+ from nl2data_core.assembly.audit_evidence import (
16
+ MAX_TRAIL_ENTRIES,
17
+ AssemblyAuditEvidenceEntry,
18
+ AuditTrail,
19
+ )
20
+
21
+ from ..envelope import ENVELOPE_SCHEMA_VERSION, ArtifactKind
22
+ from ..errors import SemanticCatalogError, SemanticCatalogErrorCode
23
+ from ..unit_of_work import CatalogUnitOfWork, _namespace
24
+
25
+
26
+ class AuditEvidenceRepository:
27
+ """Bounded, tenant-scoped audit-evidence trail persistence."""
28
+
29
+ def __init__(self, uow: CatalogUnitOfWork) -> None:
30
+ self._uow = uow
31
+
32
+ def insert_audit_entries(
33
+ self,
34
+ conn: Any,
35
+ namespace: str,
36
+ entries: Sequence[AssemblyAuditEvidenceEntry],
37
+ ) -> None:
38
+ """Persist entries inside the caller's transaction; idempotent by id.
39
+
40
+ A conflicting re-record (same event id, different fingerprint) is
41
+ corruption and fails closed instead of silently no-opping.
42
+ """
43
+ for entry in entries:
44
+ existing = self._uow.execute(
45
+ conn, "read_audit_entry", (namespace, entry.event_id)
46
+ ).fetchone()
47
+ if existing is not None:
48
+ if existing["entry_fingerprint"] != entry.fingerprint:
49
+ raise SemanticCatalogError(
50
+ SemanticCatalogErrorCode.ENVELOPE_REJECTED,
51
+ "audit-evidence event id conflict",
52
+ details={"cause_type": "AuditEvidenceEventIdConflict"},
53
+ )
54
+ continue
55
+ self._uow.execute(
56
+ conn,
57
+ "insert_audit_entry",
58
+ (
59
+ namespace,
60
+ entry.event_id,
61
+ entry.event_kind.value,
62
+ entry.subject_kind.value,
63
+ entry.subject_reference,
64
+ entry.draft_id,
65
+ entry.draft_revision,
66
+ entry.assertion_id,
67
+ entry.bundle_fingerprint,
68
+ entry.lifecycle_reference,
69
+ entry.fingerprint,
70
+ ENVELOPE_SCHEMA_VERSION,
71
+ self._uow.encode(
72
+ ArtifactKind.ASSEMBLY_AUDIT_EVIDENCE,
73
+ entry.canonical_payload(),
74
+ entry.fingerprint,
75
+ ),
76
+ entry.occurred_at,
77
+ ),
78
+ )
79
+
80
+ def record_audit_entries(
81
+ self,
82
+ entries: Sequence[AssemblyAuditEvidenceEntry],
83
+ *,
84
+ tenant_scope_fingerprint: str,
85
+ ) -> None:
86
+ """Record externally supplied entries under one tenant scope."""
87
+ for entry in entries:
88
+ if entry.tenant_scope_fingerprint != tenant_scope_fingerprint:
89
+ raise ValueError(
90
+ "audit evidence entries must match the tenant scope"
91
+ )
92
+ if not entry.verify_fingerprint():
93
+ raise ValueError("audit evidence entry fingerprint mismatch")
94
+ namespace = _namespace(tenant_scope_fingerprint)
95
+ with self._uow.transaction() as conn:
96
+ self.insert_audit_entries(conn, namespace, entries)
97
+
98
+ def find_publication_entry(
99
+ self,
100
+ conn: Any,
101
+ namespace: str,
102
+ bundle_fingerprint: str,
103
+ ) -> AssemblyAuditEvidenceEntry | None:
104
+ """The latest publication-kind entry recorded for one fingerprint."""
105
+ row = self._uow.execute(
106
+ conn,
107
+ "read_latest_publication_entry",
108
+ (namespace, bundle_fingerprint),
109
+ ).fetchone()
110
+ if row is None:
111
+ return None
112
+ return self.read_audit_entry(conn, namespace, row["event_id"])
113
+
114
+ def read_audit_entry(
115
+ self,
116
+ conn: Any,
117
+ namespace: str,
118
+ event_id: str,
119
+ ) -> AssemblyAuditEvidenceEntry | None:
120
+ """Load one entry inside an existing transaction, revalidated."""
121
+ row = self._uow.execute(
122
+ conn, "read_audit_entry", (namespace, event_id)
123
+ ).fetchone()
124
+ if row is None:
125
+ return None
126
+ envelope = self._uow.decode(
127
+ row["envelope"],
128
+ ArtifactKind.ASSEMBLY_AUDIT_EVIDENCE,
129
+ row_schema_version=row["schema_version"],
130
+ )
131
+ return self._uow.audit_entry_from_envelope(
132
+ envelope,
133
+ occurred_at=row["occurred_at"],
134
+ entry_fingerprint=row["entry_fingerprint"],
135
+ )
136
+
137
+ def audit_entries(
138
+ self,
139
+ *,
140
+ tenant_scope_fingerprint: str | None = None,
141
+ draft_id: str | None = None,
142
+ draft_revision_min: int | None = None,
143
+ draft_revision_max: int | None = None,
144
+ assertion_id: str | None = None,
145
+ bundle_fingerprint: str | None = None,
146
+ lifecycle_reference: str | None = None,
147
+ predecessor_event_id: str | None = None,
148
+ limit: int = MAX_TRAIL_ENTRIES,
149
+ cursor: str | None = None,
150
+ ) -> AuditTrail:
151
+ """One deterministic, bounded trail page under the requested scope."""
152
+ if limit < 1 or limit > MAX_TRAIL_ENTRIES:
153
+ raise ValueError("audit trail limit must be between 1 and the maximum")
154
+ namespace = _namespace(tenant_scope_fingerprint)
155
+ # The cursor is the last returned event id; its persisted position
156
+ # keys the deterministic page continuation. A pruned or unknown
157
+ # cursor restarts from the beginning rather than failing.
158
+ cursor_at: Any = None
159
+ cursor_id: str | None = None
160
+ if cursor is not None:
161
+ with self._uow.transaction() as conn:
162
+ row = self._uow.execute(
163
+ conn, "read_audit_entry", (namespace, cursor)
164
+ ).fetchone()
165
+ if row is not None:
166
+ cursor_at = row["occurred_at"]
167
+ cursor_id = cursor
168
+ # Each optional equality filter is passed twice, mirroring the
169
+ # ``(%s IS NULL OR col = %s)`` guard: an unbound filter never
170
+ # compares a column against itself, so rows with NULL columns stay
171
+ # visible exactly like the in-memory trail.
172
+ filters = (
173
+ namespace,
174
+ draft_id,
175
+ draft_id,
176
+ assertion_id,
177
+ assertion_id,
178
+ bundle_fingerprint,
179
+ bundle_fingerprint,
180
+ lifecycle_reference,
181
+ lifecycle_reference,
182
+ draft_revision_min,
183
+ draft_revision_min,
184
+ draft_revision_max,
185
+ draft_revision_max,
186
+ predecessor_event_id,
187
+ predecessor_event_id,
188
+ cursor_at,
189
+ cursor_at,
190
+ cursor_id,
191
+ )
192
+ with self._uow.transaction() as conn:
193
+ count_row = self._uow.execute(
194
+ conn, "count_audit_evidence", filters
195
+ ).fetchone()
196
+ total = int(count_row["total"])
197
+ rows = self._uow.execute(
198
+ conn, "list_audit_evidence", (*filters, limit)
199
+ ).fetchall()
200
+ page = [
201
+ self._uow.audit_entry_from_envelope(
202
+ self._uow.decode(
203
+ row["envelope"],
204
+ ArtifactKind.ASSEMBLY_AUDIT_EVIDENCE,
205
+ row_schema_version=row["schema_version"],
206
+ ),
207
+ occurred_at=row["occurred_at"],
208
+ entry_fingerprint=row["entry_fingerprint"],
209
+ )
210
+ for row in rows
211
+ ]
212
+ has_more = total > len(page)
213
+ return AuditTrail(
214
+ entries=tuple(page),
215
+ total_count=total,
216
+ next_cursor=(page[-1].event_id if page and has_more else None),
217
+ has_more=has_more,
218
+ )
219
+
220
+
221
+ __all__ = ["AuditEvidenceRepository"]
@@ -0,0 +1,156 @@
1
+ """Assembly draft persistence repository.
2
+
3
+ Drafts are mutable control-plane records: creates are idempotent-failing,
4
+ reads revalidate the persisted envelope against the draft identifier, and
5
+ replacements are optimistic-concurrency (revision CAS) operations. The
6
+ repository never participates in publication transactions beyond exposing
7
+ the draft-lock reads the publication repository performs under the
8
+ publication transaction.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from nl2data_core.assembly.models import AssemblyDraft, DraftRevisionConflict
14
+ from nl2data_core.canonical import strict_sha256_fingerprint
15
+ from nl2data_core.control_plane.publication.contracts import PublicationDraftBinding
16
+
17
+ from ..envelope import ENVELOPE_SCHEMA_VERSION, ArtifactKind
18
+ from ..errors import SemanticCatalogError, SemanticCatalogErrorCode
19
+ from ..unit_of_work import CatalogUnitOfWork, _namespace
20
+
21
+
22
+ class DraftRepository:
23
+ """Tenant-scoped assembly draft persistence with revision CAS."""
24
+
25
+ def __init__(self, uow: CatalogUnitOfWork) -> None:
26
+ self._uow = uow
27
+
28
+ def create(
29
+ self,
30
+ draft: AssemblyDraft,
31
+ *,
32
+ tenant_scope_fingerprint: str,
33
+ ) -> None:
34
+ """Persist a new tenant-scoped assembly draft."""
35
+ namespace = _namespace(tenant_scope_fingerprint)
36
+ payload = draft.file_payload()
37
+ envelope = self._uow.encode(
38
+ ArtifactKind.ASSEMBLY_DRAFT,
39
+ payload,
40
+ strict_sha256_fingerprint(payload),
41
+ )
42
+ with self._uow.transaction() as conn:
43
+ cursor = self._uow.execute(
44
+ conn,
45
+ "insert_assembly_draft",
46
+ (
47
+ namespace,
48
+ draft.draft_id,
49
+ draft.bundle_id,
50
+ draft.source_id,
51
+ draft.draft_revision,
52
+ draft.state.value,
53
+ ENVELOPE_SCHEMA_VERSION,
54
+ envelope,
55
+ self._uow.now(),
56
+ ),
57
+ )
58
+ if cursor.rowcount == 0:
59
+ raise ValueError(f"assembly draft '{draft.draft_id}' already exists")
60
+
61
+ def get_draft(
62
+ self,
63
+ draft_id: str,
64
+ *,
65
+ tenant_scope_fingerprint: str,
66
+ ) -> AssemblyDraft | None:
67
+ """Load a tenant-scoped assembly draft by opaque identifier."""
68
+ namespace = _namespace(tenant_scope_fingerprint)
69
+ with self._uow.transaction() as conn:
70
+ row = self._uow.execute(
71
+ conn,
72
+ "read_assembly_draft",
73
+ (namespace, draft_id),
74
+ ).fetchone()
75
+ if row is None:
76
+ return None
77
+ envelope = self._uow.decode(
78
+ row["envelope"],
79
+ ArtifactKind.ASSEMBLY_DRAFT,
80
+ row_schema_version=row["schema_version"],
81
+ )
82
+ draft = self._uow.draft_from_envelope(envelope)
83
+ if draft.draft_id != draft_id or draft.draft_revision != int(
84
+ row["draft_revision"]
85
+ ):
86
+ raise SemanticCatalogError(
87
+ SemanticCatalogErrorCode.ENVELOPE_REJECTED,
88
+ "persisted assembly draft metadata does not match its envelope",
89
+ details={"cause_type": "DraftMetadataMismatch"},
90
+ )
91
+ return draft
92
+
93
+ def authoritative_release_binding_matches(
94
+ self,
95
+ binding: PublicationDraftBinding,
96
+ ) -> bool:
97
+ """Preflight the exact persisted draft before external verification work."""
98
+ authoritative = self.get_draft(
99
+ binding.draft_id,
100
+ tenant_scope_fingerprint=binding.tenant_scope_fingerprint,
101
+ )
102
+ return (
103
+ authoritative is not None
104
+ and authoritative.draft_revision == binding.draft_revision
105
+ and strict_sha256_fingerprint(authoritative.file_payload())
106
+ == binding.draft_payload_fingerprint
107
+ )
108
+
109
+ def replace(
110
+ self,
111
+ draft: AssemblyDraft,
112
+ *,
113
+ expected_revision: int,
114
+ tenant_scope_fingerprint: str,
115
+ ) -> None:
116
+ """Replace a draft only when its persisted revision matches."""
117
+ if draft.draft_revision != expected_revision + 1:
118
+ raise DraftRevisionConflict(
119
+ expected=expected_revision + 1,
120
+ actual=draft.draft_revision,
121
+ )
122
+ namespace = _namespace(tenant_scope_fingerprint)
123
+ payload = draft.file_payload()
124
+ envelope = self._uow.encode(
125
+ ArtifactKind.ASSEMBLY_DRAFT,
126
+ payload,
127
+ strict_sha256_fingerprint(payload),
128
+ )
129
+ with self._uow.transaction() as conn:
130
+ cursor = self._uow.execute(
131
+ conn,
132
+ "replace_assembly_draft",
133
+ (
134
+ draft.bundle_id,
135
+ draft.source_id,
136
+ draft.draft_revision,
137
+ draft.state.value,
138
+ ENVELOPE_SCHEMA_VERSION,
139
+ envelope,
140
+ self._uow.now(),
141
+ namespace,
142
+ draft.draft_id,
143
+ expected_revision,
144
+ ),
145
+ )
146
+ if cursor.rowcount == 0:
147
+ current = self._uow.execute(
148
+ conn,
149
+ "read_assembly_draft",
150
+ (namespace, draft.draft_id),
151
+ ).fetchone()
152
+ actual = -1 if current is None else int(current["draft_revision"])
153
+ raise DraftRevisionConflict(expected=expected_revision, actual=actual)
154
+
155
+
156
+ __all__ = ["DraftRepository"]