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,415 @@
1
+ """Verification evidence, publish audit, and accepted-manifest repository.
2
+
3
+ Every read revalidates the persisted envelope and cross-checks the
4
+ persisted metadata against the requested publication identity, so tampered
5
+ or truncated records fail closed. Historical evidence validation uses
6
+ only immutable publication-time records (manifest, evidence, audit, frozen
7
+ release binding); the mutable draft store is never read here.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from typing import Any
13
+
14
+ from nl2data_core.assembly.audit_evidence import PublicationAuditEvidence
15
+ from nl2data_core.assembly.manifest import AcceptedAssertionManifest
16
+ from nl2data_core.bundles.publication import PublishAuditRecord
17
+ from nl2data_core.canonical import strict_sha256_fingerprint
18
+ from nl2data_core.control_plane.publication.contracts import (
19
+ FrozenReleaseBinding,
20
+ PublicationIntegrityError,
21
+ PublicationRecordSet,
22
+ validate_publication_integrity,
23
+ )
24
+ from nl2data_core.verification.models import VerificationSuiteEvidence
25
+
26
+ from ..envelope import ENVELOPE_SCHEMA_VERSION, ArtifactKind
27
+ from ..errors import SemanticCatalogError, SemanticCatalogErrorCode
28
+ from ..unit_of_work import CatalogUnitOfWork, _namespace
29
+
30
+ # Read paths translate centralized integrity codes into the established
31
+ # persisted-record cause types so callers see stable error details.
32
+ _CAUSE_TYPES = {
33
+ "manifest_mismatch": "ManifestMetadataMismatch",
34
+ "audit_mismatch": "AuditMetadataMismatch",
35
+ "verification_audit_mismatch": "VerificationAuditMismatch",
36
+ "verification_manifest_mismatch": "VerificationBindingMismatch",
37
+ "verification_binding_mismatch": "VerificationBindingMismatch",
38
+ "verification_evidence_mismatch": "VerificationEvidenceMetadataMismatch",
39
+ "verification_binding_audit_mismatch": "VerificationAuditMismatch",
40
+ "publication_audit_evidence_mismatch": "PublicationAuditEvidenceMismatch",
41
+ }
42
+
43
+
44
+ def _integrity_rejection(error: PublicationIntegrityError) -> SemanticCatalogError:
45
+ if error.code == "verification_binding_mismatch" and (
46
+ error.classification == "legacy_unverified"
47
+ ):
48
+ details: dict[str, str] = {
49
+ "classification": "legacy_unverified",
50
+ "cause_type": "LegacyVerificationEvidenceMissingFrozenBinding",
51
+ }
52
+ else:
53
+ details = {
54
+ "cause_type": _CAUSE_TYPES.get(error.code, "VerificationAuditMismatch")
55
+ }
56
+ return SemanticCatalogError(
57
+ SemanticCatalogErrorCode.ENVELOPE_REJECTED, error.message, details=details
58
+ )
59
+
60
+
61
+ class EvidenceRepository:
62
+ """Immutable publication-time lifecycle records for one Bundle."""
63
+
64
+ def __init__(self, uow: CatalogUnitOfWork) -> None:
65
+ self._uow = uow
66
+
67
+ def read_publish_audit(
68
+ self,
69
+ conn: Any,
70
+ namespace: str,
71
+ bundle_id: str,
72
+ fingerprint: str,
73
+ ) -> PublishAuditRecord | None:
74
+ """Load one audit row inside an existing transaction."""
75
+ row = self._uow.execute(
76
+ conn,
77
+ "read_publish_audit",
78
+ (namespace, bundle_id, fingerprint),
79
+ ).fetchone()
80
+ if row is None:
81
+ return None
82
+ envelope = self._uow.decode(
83
+ row["envelope"],
84
+ ArtifactKind.PUBLISH_AUDIT,
85
+ row_schema_version=row["schema_version"],
86
+ )
87
+ audit = self._uow.audit_from_envelope(envelope)
88
+ if audit.bundle_id != bundle_id or audit.bundle_fingerprint != fingerprint:
89
+ raise SemanticCatalogError(
90
+ SemanticCatalogErrorCode.ENVELOPE_REJECTED,
91
+ "persisted audit metadata does not match its publication",
92
+ details={"cause_type": "AuditMetadataMismatch"},
93
+ )
94
+ return audit
95
+
96
+ def read_release_binding(
97
+ self,
98
+ conn: Any,
99
+ namespace: str,
100
+ bundle_id: str,
101
+ fingerprint: str,
102
+ ) -> FrozenReleaseBinding | None:
103
+ """Load the frozen release binding persisted with one evidence row."""
104
+ row = self._uow.execute(
105
+ conn,
106
+ "read_verification_evidence",
107
+ (namespace, bundle_id, fingerprint),
108
+ ).fetchone()
109
+ if row is None:
110
+ return None
111
+ envelope = self._uow.decode(
112
+ row["envelope"],
113
+ ArtifactKind.VERIFICATION_SUITE_EVIDENCE,
114
+ row_schema_version=row["schema_version"],
115
+ )
116
+ return self._uow.release_binding_from_envelope(envelope)
117
+
118
+ def insert_publication_audit_evidence(
119
+ self,
120
+ conn: Any,
121
+ namespace: str,
122
+ bundle_id: str,
123
+ fingerprint: str,
124
+ binding: PublicationAuditEvidence,
125
+ *,
126
+ now: Any,
127
+ ) -> None:
128
+ """Persist the publication audit-evidence binding inside a transaction."""
129
+ payload = self._uow.publication_audit_evidence_payload(binding)
130
+ self._uow.execute(
131
+ conn,
132
+ "insert_publication_audit_evidence",
133
+ (
134
+ namespace,
135
+ bundle_id,
136
+ fingerprint,
137
+ binding.fingerprint,
138
+ ENVELOPE_SCHEMA_VERSION,
139
+ self._uow.encode(
140
+ ArtifactKind.PUBLICATION_AUDIT_EVIDENCE,
141
+ payload,
142
+ strict_sha256_fingerprint(payload),
143
+ ),
144
+ now,
145
+ ),
146
+ )
147
+
148
+ def read_publication_audit_evidence(
149
+ self,
150
+ conn: Any,
151
+ namespace: str,
152
+ bundle_id: str,
153
+ fingerprint: str,
154
+ ) -> PublicationAuditEvidence | None:
155
+ """Load one publication audit-evidence binding inside a transaction."""
156
+ row = self._uow.execute(
157
+ conn,
158
+ "read_publication_audit_evidence",
159
+ (namespace, bundle_id, fingerprint),
160
+ ).fetchone()
161
+ if row is None:
162
+ return None
163
+ envelope = self._uow.decode(
164
+ row["envelope"],
165
+ ArtifactKind.PUBLICATION_AUDIT_EVIDENCE,
166
+ row_schema_version=row["schema_version"],
167
+ )
168
+ binding = self._uow.publication_audit_evidence_from_envelope(envelope)
169
+ if (
170
+ binding.bundle_fingerprint != fingerprint
171
+ or binding.fingerprint != row["evidence_fingerprint"]
172
+ ):
173
+ raise SemanticCatalogError(
174
+ SemanticCatalogErrorCode.ENVELOPE_REJECTED,
175
+ "persisted publication audit evidence metadata does not "
176
+ "match publication",
177
+ details={"cause_type": "PublicationAuditEvidenceMismatch"},
178
+ )
179
+ return binding
180
+
181
+ def read_verification_evidence(
182
+ self,
183
+ conn: Any,
184
+ namespace: str,
185
+ bundle_id: str,
186
+ fingerprint: str,
187
+ ) -> VerificationSuiteEvidence | None:
188
+ """Load one evidence row inside an existing transaction."""
189
+ row = self._uow.execute(
190
+ conn,
191
+ "read_verification_evidence",
192
+ (namespace, bundle_id, fingerprint),
193
+ ).fetchone()
194
+ if row is None:
195
+ return None
196
+ envelope = self._uow.decode(
197
+ row["envelope"],
198
+ ArtifactKind.VERIFICATION_SUITE_EVIDENCE,
199
+ row_schema_version=row["schema_version"],
200
+ )
201
+ evidence = self._uow.evidence_from_envelope(envelope)
202
+ if (
203
+ evidence.bundle_fingerprint != fingerprint
204
+ or evidence.fingerprint != row["evidence_fingerprint"]
205
+ ):
206
+ raise SemanticCatalogError(
207
+ SemanticCatalogErrorCode.ENVELOPE_REJECTED,
208
+ "persisted verification evidence metadata does not match publication",
209
+ details={"cause_type": "VerificationEvidenceMetadataMismatch"},
210
+ )
211
+ return evidence
212
+
213
+ def accepted_assertion_manifest(
214
+ self,
215
+ bundle_id: str,
216
+ fingerprint: str,
217
+ *,
218
+ tenant_scope_fingerprint: str | None = None,
219
+ ) -> AcceptedAssertionManifest | None:
220
+ """Load the immutable accepted-assertion manifest for a publication."""
221
+ namespace = _namespace(tenant_scope_fingerprint)
222
+ with self._uow.transaction() as conn:
223
+ row = self._uow.execute(
224
+ conn,
225
+ "read_accepted_manifest",
226
+ (namespace, bundle_id, fingerprint),
227
+ ).fetchone()
228
+ if row is None:
229
+ return None
230
+ envelope = self._uow.decode(
231
+ row["envelope"],
232
+ ArtifactKind.ACCEPTED_ASSERTION_MANIFEST,
233
+ row_schema_version=row["schema_version"],
234
+ )
235
+ manifest = self._uow.manifest_from_envelope(envelope)
236
+ if manifest.bundle_id != bundle_id or manifest.bundle_fingerprint != fingerprint:
237
+ raise SemanticCatalogError(
238
+ SemanticCatalogErrorCode.ENVELOPE_REJECTED,
239
+ "persisted manifest metadata does not match its publication",
240
+ details={"cause_type": "ManifestMetadataMismatch"},
241
+ )
242
+ return manifest
243
+
244
+ def publish_audit(
245
+ self,
246
+ bundle_id: str,
247
+ fingerprint: str,
248
+ *,
249
+ tenant_scope_fingerprint: str | None = None,
250
+ ) -> PublishAuditRecord | None:
251
+ """Load the immutable safe audit record for a publication."""
252
+ namespace = _namespace(tenant_scope_fingerprint)
253
+ with self._uow.transaction() as conn:
254
+ return self.read_publish_audit(conn, namespace, bundle_id, fingerprint)
255
+
256
+ def validated_publication_records(
257
+ self,
258
+ conn: Any,
259
+ namespace: str,
260
+ bundle_id: str,
261
+ fingerprint: str,
262
+ *,
263
+ audit_id: str | None = None,
264
+ ) -> PublicationRecordSet:
265
+ """Load and centrally validate one publication's lifecycle records.
266
+
267
+ This is the single read-side integrity entry point: every reuse,
268
+ record, activation, rollback, and reload path wraps its loaded
269
+ rows in a ``PublicationRecordSet`` and validates through the
270
+ shared rule set instead of per-entry-point checks. Legacy
271
+ compatibility publications (plain Bundle, manifest-only) yield a
272
+ record set with ``audit`` and ``verification_evidence`` unset.
273
+ ``audit_id`` is the published-version row's independent witness
274
+ that a publish audit was written with the publication; a missing
275
+ row it claims existed is corruption and fails closed.
276
+ """
277
+ evidence = self.read_verification_evidence(
278
+ conn, namespace, bundle_id, fingerprint
279
+ )
280
+ audit = self.read_publish_audit(conn, namespace, bundle_id, fingerprint)
281
+ audit_evidence = self.read_publication_audit_evidence(
282
+ conn, namespace, bundle_id, fingerprint
283
+ )
284
+ manifest_row = self._uow.execute(
285
+ conn,
286
+ "read_accepted_manifest",
287
+ (namespace, bundle_id, fingerprint),
288
+ ).fetchone()
289
+ manifest = None
290
+ if manifest_row is not None:
291
+ manifest_envelope = self._uow.decode(
292
+ manifest_row["envelope"],
293
+ ArtifactKind.ACCEPTED_ASSERTION_MANIFEST,
294
+ row_schema_version=manifest_row["schema_version"],
295
+ )
296
+ manifest = self._uow.manifest_from_envelope(manifest_envelope)
297
+ binding = None
298
+ if evidence is not None:
299
+ binding = self.read_release_binding(
300
+ conn, namespace, bundle_id, fingerprint
301
+ )
302
+ if evidence is None and audit is None and audit_id is not None:
303
+ # The version row is an independent witness that a publish
304
+ # audit existed; a missing audit row is corruption.
305
+ raise SemanticCatalogError(
306
+ SemanticCatalogErrorCode.ENVELOPE_REJECTED,
307
+ "published version has lost its publish audit",
308
+ details={"cause_type": "MissingPublishAudit"},
309
+ )
310
+ records = PublicationRecordSet(
311
+ bundle_id=bundle_id,
312
+ bundle_fingerprint=fingerprint,
313
+ accepted_assertion_manifest=manifest,
314
+ audit=audit,
315
+ verification_evidence=evidence,
316
+ frozen_release_binding=binding,
317
+ audit_evidence=audit_evidence,
318
+ )
319
+ try:
320
+ validate_publication_integrity(records)
321
+ except PublicationIntegrityError as error:
322
+ if (
323
+ error.code == "verification_audit_mismatch"
324
+ and audit is not None
325
+ and evidence is None
326
+ ):
327
+ # The audit is an independent witness that the evidence
328
+ # row existed; a missing row is corruption.
329
+ raise SemanticCatalogError(
330
+ SemanticCatalogErrorCode.ENVELOPE_REJECTED,
331
+ "verification evidence row is missing for its publish audit",
332
+ details={"cause_type": "MissingVerificationEvidence"},
333
+ ) from error
334
+ if (
335
+ error.code == "verification_audit_mismatch"
336
+ and audit is None
337
+ and audit_id is not None
338
+ ):
339
+ # The version row is an independent witness that a publish
340
+ # audit existed; a missing audit row is corruption.
341
+ raise SemanticCatalogError(
342
+ SemanticCatalogErrorCode.ENVELOPE_REJECTED,
343
+ "published version has lost its publish audit",
344
+ details={"cause_type": "MissingPublishAudit"},
345
+ ) from error
346
+ raise _integrity_rejection(error) from error
347
+ if (
348
+ audit is not None
349
+ and audit_evidence is None
350
+ and self._uow.execute(
351
+ conn,
352
+ "read_publication_audit_entry",
353
+ (namespace, audit.audit_id),
354
+ ).fetchone()
355
+ is not None
356
+ ):
357
+ # The publication-kind trail entry witnesses that a binding row
358
+ # was persisted atomically with the publish audit; a binding
359
+ # row that vanished while its witness survives is corruption,
360
+ # never a legacy shape.
361
+ raise SemanticCatalogError(
362
+ SemanticCatalogErrorCode.ENVELOPE_REJECTED,
363
+ "publication audit evidence row is missing for its publish "
364
+ "audit",
365
+ details={"cause_type": "MissingPublicationAuditEvidence"},
366
+ )
367
+ return records
368
+
369
+ def validated_verification_evidence(
370
+ self,
371
+ conn: Any,
372
+ namespace: str,
373
+ bundle_id: str,
374
+ fingerprint: str,
375
+ ) -> VerificationSuiteEvidence | None:
376
+ """Load evidence with full immutable binding validation in one transaction.
377
+
378
+ Thin view over ``validated_publication_records``: evidence is valid
379
+ only when its frozen release binding, audit, and manifest
380
+ cross-links validate together. Tenant-scope checks against the
381
+ caller are applied by the transaction-owner entry points.
382
+ """
383
+ records = self.validated_publication_records(
384
+ conn, namespace, bundle_id, fingerprint
385
+ )
386
+ return records.verification_evidence
387
+
388
+ def verification_evidence(
389
+ self,
390
+ bundle_id: str,
391
+ fingerprint: str,
392
+ *,
393
+ tenant_scope_fingerprint: str | None = None,
394
+ ) -> VerificationSuiteEvidence | None:
395
+ """Load immutable bounded verification evidence for a publication."""
396
+ namespace = _namespace(tenant_scope_fingerprint)
397
+ with self._uow.transaction() as conn:
398
+ evidence = self.validated_verification_evidence(
399
+ conn, namespace, bundle_id, fingerprint
400
+ )
401
+ if evidence is None:
402
+ return None
403
+ if (
404
+ tenant_scope_fingerprint is None
405
+ or evidence.tenant_scope_fingerprint != tenant_scope_fingerprint
406
+ ):
407
+ raise SemanticCatalogError(
408
+ SemanticCatalogErrorCode.ENVELOPE_REJECTED,
409
+ "persisted verification evidence does not match publish audit",
410
+ details={"cause_type": "VerificationAuditMismatch"},
411
+ )
412
+ return evidence
413
+
414
+
415
+ __all__ = ["EvidenceRepository"]