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,486 @@
1
+ """Bundle publication repository over the shared catalog unit of work.
2
+
3
+ The repository performs the atomic publication writes for one Bundle -
4
+ publication row, accepted manifest, verification evidence, publish audit,
5
+ published version, and supersession bookkeeping - inside a transaction
6
+ owned by the store facade. It never opens or commits a transaction for
7
+ these writes and never receives mutable ``AssemblyDraft`` objects: the
8
+ publication-time draft facts cross the boundary only as the immutable
9
+ :class:`PublicationDraftBinding` contract.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from typing import Any
15
+
16
+ from nl2data_core.assembly.audit_evidence import publication_audit_entry
17
+ from nl2data_core.bundles.catalog import (
18
+ BundleCatalogOutcome,
19
+ _expected_snapshot_fingerprint,
20
+ _failure,
21
+ _failure_from_activation_check,
22
+ _failure_from_validation,
23
+ _success,
24
+ )
25
+ from nl2data_core.bundles.models import BUNDLE_SCHEMA_VERSION, SemanticModelBundle
26
+ from nl2data_core.bundles.publication import (
27
+ PublishedVersionState,
28
+ PublishIdempotencyStatus,
29
+ )
30
+ from nl2data_core.bundles.validation import validate_bundle
31
+ from nl2data_core.canonical import strict_sha256_fingerprint
32
+ from nl2data_core.control_plane.publication.contracts import (
33
+ PublicationDraftBinding,
34
+ PublicationRecordSet,
35
+ verification_evidence_reference,
36
+ )
37
+ from nl2data_core.metadata.policy import ProductionActivationContext
38
+
39
+ from ..envelope import ENVELOPE_SCHEMA_VERSION, ArtifactKind
40
+ from ..errors import SemanticCatalogError
41
+ from ..unit_of_work import CatalogUnitOfWork, _namespace
42
+ from .audit_evidence import AuditEvidenceRepository
43
+ from .evidence import EvidenceRepository
44
+
45
+
46
+ class PublicationRepository:
47
+ """Atomic Bundle publication and immutable publication reads."""
48
+
49
+ def __init__(
50
+ self,
51
+ uow: CatalogUnitOfWork,
52
+ evidence: EvidenceRepository,
53
+ audit: AuditEvidenceRepository,
54
+ ) -> None:
55
+ self._uow = uow
56
+ self._evidence = evidence
57
+ self._audit = audit
58
+
59
+ def publish(
60
+ self,
61
+ conn: Any,
62
+ bundle: SemanticModelBundle,
63
+ *,
64
+ namespace: str,
65
+ now: Any,
66
+ records: PublicationRecordSet,
67
+ production: ProductionActivationContext | None = None,
68
+ publication_binding: PublicationDraftBinding | None = None,
69
+ idempotency_key: str | None = None,
70
+ ) -> BundleCatalogOutcome:
71
+ """Publish one Bundle and its validated lifecycle records atomically.
72
+
73
+ The connection is provided by the transaction owner; every write in
74
+ this method rolls back together when any statement fails. The
75
+ record set arrives already validated by the centralized integrity
76
+ validator (``build_publication_records`` at the facade boundary);
77
+ repositories never accept per-record compatibility arguments.
78
+ """
79
+ result = validate_bundle(
80
+ bundle,
81
+ supported_schema_versions=(BUNDLE_SCHEMA_VERSION,),
82
+ expected_snapshot_fingerprint=_expected_snapshot_fingerprint(production),
83
+ )
84
+ if not result.valid:
85
+ return _failure_from_validation(result)
86
+ if production is not None:
87
+ check = production.check()
88
+ if not check.allowed:
89
+ return _failure_from_activation_check(check)
90
+ accepted_assertion_manifest = records.accepted_assertion_manifest
91
+ audit = records.audit
92
+ verification_evidence = records.verification_evidence
93
+ frozen_release_binding = records.frozen_release_binding
94
+ evidence_reference = (
95
+ verification_evidence_reference(verification_evidence.fingerprint)
96
+ if verification_evidence is not None
97
+ else None
98
+ )
99
+ bundle_payload = bundle.file_payload()
100
+ envelope = self._uow.encode(
101
+ ArtifactKind.BUNDLE,
102
+ bundle_payload,
103
+ strict_sha256_fingerprint(bundle_payload),
104
+ )
105
+ manifest_envelope = None
106
+ if accepted_assertion_manifest is not None:
107
+ manifest_payload = accepted_assertion_manifest.canonical_payload()
108
+ manifest_envelope = self._uow.encode(
109
+ ArtifactKind.ACCEPTED_ASSERTION_MANIFEST,
110
+ manifest_payload,
111
+ strict_sha256_fingerprint(manifest_payload),
112
+ )
113
+ audit_envelope = None
114
+ if audit is not None:
115
+ audit_payload = audit.safe_payload()
116
+ audit_envelope = self._uow.encode(
117
+ ArtifactKind.PUBLISH_AUDIT,
118
+ audit_payload,
119
+ strict_sha256_fingerprint(audit_payload),
120
+ )
121
+ evidence_envelope = None
122
+ if verification_evidence is not None and frozen_release_binding is not None:
123
+ evidence_payload = self._uow.verification_evidence_payload(
124
+ verification_evidence,
125
+ frozen_release_binding,
126
+ )
127
+ evidence_envelope = self._uow.encode(
128
+ ArtifactKind.VERIFICATION_SUITE_EVIDENCE,
129
+ evidence_payload,
130
+ strict_sha256_fingerprint(evidence_payload),
131
+ )
132
+ if publication_binding is not None:
133
+ persisted = self._uow.execute(
134
+ conn,
135
+ "lock_assembly_draft",
136
+ (namespace, publication_binding.draft_id),
137
+ ).fetchone()
138
+ if persisted is None:
139
+ return _failure(
140
+ "conflict",
141
+ "draft_not_found",
142
+ "assembly draft is not persisted in this tenant scope",
143
+ )
144
+ if int(persisted["draft_revision"]) != publication_binding.draft_revision:
145
+ return _failure(
146
+ "conflict",
147
+ "draft_revision_conflict",
148
+ "assembly draft revision changed before publication",
149
+ )
150
+ persisted_envelope = self._uow.decode(
151
+ persisted["envelope"],
152
+ ArtifactKind.ASSEMBLY_DRAFT,
153
+ row_schema_version=persisted["schema_version"],
154
+ )
155
+ persisted_draft = self._uow.draft_from_envelope(persisted_envelope)
156
+ if strict_sha256_fingerprint(persisted_draft.file_payload()) != (
157
+ publication_binding.draft_payload_fingerprint
158
+ ):
159
+ return _failure(
160
+ "conflict",
161
+ "draft_changed",
162
+ "assembly draft content changed before publication",
163
+ )
164
+ if verification_evidence is not None and (
165
+ verification_evidence.draft_id != publication_binding.draft_id
166
+ or verification_evidence.draft_revision
167
+ != publication_binding.draft_revision
168
+ or verification_evidence.plan_fingerprint
169
+ != publication_binding.approved_plan_fingerprint
170
+ or verification_evidence.tenant_scope_fingerprint
171
+ != publication_binding.tenant_scope_fingerprint
172
+ or verification_evidence.source_scope_fingerprint
173
+ != publication_binding.source_scope_fingerprint
174
+ ):
175
+ return _failure(
176
+ "rejected",
177
+ "verification_evidence_mismatch",
178
+ "verification evidence does not match the locked draft scope",
179
+ )
180
+ self._uow.execute(
181
+ conn,
182
+ "lock_publication_series",
183
+ (namespace, bundle.bundle_id),
184
+ )
185
+ if idempotency_key is not None:
186
+ idempotent = self._uow.execute(
187
+ conn,
188
+ "read_publish_by_idempotency_key",
189
+ (namespace, idempotency_key),
190
+ ).fetchone()
191
+ if idempotent is not None and (
192
+ idempotent["bundle_id"] != bundle.bundle_id
193
+ or idempotent["bundle_fingerprint"] != bundle.fingerprint
194
+ ):
195
+ return _failure(
196
+ "conflict",
197
+ "idempotency_key_reused",
198
+ "idempotency key is already bound to other semantic content",
199
+ )
200
+ existing = self._uow.execute(
201
+ conn,
202
+ "read_publication_by_fingerprint",
203
+ (namespace, bundle.bundle_id, bundle.fingerprint),
204
+ ).fetchone()
205
+ if existing is not None:
206
+ existing_envelope = self._uow.decode(
207
+ existing["envelope"],
208
+ ArtifactKind.BUNDLE,
209
+ row_schema_version=existing["schema_version"],
210
+ )
211
+ existing_bundle = self._uow.bundle_from_envelope(existing_envelope)
212
+ version_record = self._uow.execute(
213
+ conn,
214
+ "read_published_version",
215
+ (namespace, bundle.bundle_id, bundle.fingerprint),
216
+ ).fetchone()
217
+ if version_record is None:
218
+ # Reuse without a durable version record would report success
219
+ # for a publication that no longer exists.
220
+ return _failure(
221
+ "conflict",
222
+ "publication_version_missing",
223
+ "published version record is missing for this publication",
224
+ )
225
+ try:
226
+ # Reuse re-validates the persisted records through the
227
+ # centralized integrity rule set, so a tampered or
228
+ # truncated record is never returned as reused.
229
+ persisted = self._evidence.validated_publication_records(
230
+ conn,
231
+ namespace,
232
+ bundle.bundle_id,
233
+ bundle.fingerprint,
234
+ audit_id=version_record["audit_id"],
235
+ )
236
+ except SemanticCatalogError as error:
237
+ if (error.details or {}).get("cause_type") == "MissingPublishAudit":
238
+ return _failure(
239
+ "conflict",
240
+ "verification_audit_missing",
241
+ "persisted publish audit referenced by the version "
242
+ "record is missing",
243
+ )
244
+ return _failure(
245
+ "conflict",
246
+ "verification_evidence_mismatch",
247
+ "persisted verification evidence no longer validates",
248
+ )
249
+ existing_audit = persisted.audit
250
+ if (
251
+ verification_evidence is not None
252
+ and persisted.verification_evidence is None
253
+ ):
254
+ # An evidence-free record must never silently absorb a
255
+ # verified publication request.
256
+ return _failure(
257
+ "conflict",
258
+ "publication_state_conflict",
259
+ "existing publication has no verification evidence to reuse",
260
+ )
261
+ return _success(
262
+ "reused",
263
+ existing_bundle,
264
+ audit_reference=(
265
+ existing_audit.audit_id if existing_audit is not None else None
266
+ ),
267
+ verification_evidence_reference=(
268
+ existing_audit.verification.evidence_reference
269
+ if existing_audit is not None
270
+ else None
271
+ ),
272
+ superseded_fingerprint=version_record["predecessor_fingerprint"],
273
+ idempotency_status=PublishIdempotencyStatus.REUSED,
274
+ )
275
+ version_match = self._uow.execute(
276
+ conn,
277
+ "read_publication_fingerprint",
278
+ (namespace, bundle.bundle_id, bundle.model_version),
279
+ ).fetchone()
280
+ if version_match is not None:
281
+ return _failure(
282
+ "conflict",
283
+ "version_exists",
284
+ f"bundle '{bundle.bundle_id}' version "
285
+ f"'{bundle.model_version}' is already published",
286
+ )
287
+ predecessor = self._uow.execute(
288
+ conn,
289
+ "read_latest_version",
290
+ (namespace, bundle.bundle_id),
291
+ ).fetchone()
292
+ predecessor_fingerprint = (
293
+ predecessor["bundle_fingerprint"] if predecessor is not None else None
294
+ )
295
+ self._uow.execute(
296
+ conn,
297
+ "insert_publication",
298
+ (
299
+ namespace,
300
+ bundle.bundle_id,
301
+ bundle.model_version,
302
+ bundle.fingerprint,
303
+ ENVELOPE_SCHEMA_VERSION,
304
+ envelope,
305
+ now,
306
+ ),
307
+ )
308
+ if manifest_envelope is not None:
309
+ self._uow.execute(
310
+ conn,
311
+ "insert_accepted_manifest",
312
+ (
313
+ namespace,
314
+ bundle.bundle_id,
315
+ bundle.fingerprint,
316
+ ENVELOPE_SCHEMA_VERSION,
317
+ manifest_envelope,
318
+ now,
319
+ ),
320
+ )
321
+ if evidence_envelope is not None and verification_evidence is not None:
322
+ self._uow.execute(
323
+ conn,
324
+ "insert_verification_evidence",
325
+ (
326
+ namespace,
327
+ bundle.bundle_id,
328
+ bundle.fingerprint,
329
+ verification_evidence.fingerprint,
330
+ ENVELOPE_SCHEMA_VERSION,
331
+ evidence_envelope,
332
+ now,
333
+ ),
334
+ )
335
+ if audit_envelope is not None and audit is not None:
336
+ self._uow.execute(
337
+ conn,
338
+ "insert_publish_audit",
339
+ (
340
+ namespace,
341
+ bundle.bundle_id,
342
+ bundle.fingerprint,
343
+ audit.audit_id,
344
+ idempotency_key,
345
+ ENVELOPE_SCHEMA_VERSION,
346
+ audit_envelope,
347
+ now,
348
+ ),
349
+ )
350
+ if records.audit_evidence is not None:
351
+ # The release-readiness binding and its publication trail entry
352
+ # commit atomically with the publish audit they cross-link to.
353
+ self._evidence.insert_publication_audit_evidence(
354
+ conn,
355
+ namespace,
356
+ bundle.bundle_id,
357
+ bundle.fingerprint,
358
+ records.audit_evidence,
359
+ now=now,
360
+ )
361
+ predecessor_entry = (
362
+ self._audit.find_publication_entry(
363
+ conn, namespace, predecessor_fingerprint
364
+ )
365
+ if predecessor_fingerprint is not None
366
+ else None
367
+ )
368
+ self._audit.insert_audit_entries(
369
+ conn,
370
+ namespace,
371
+ [
372
+ publication_audit_entry(
373
+ records.audit_evidence,
374
+ predecessor_event_ids=(
375
+ ()
376
+ if predecessor_entry is None
377
+ else (predecessor_entry.event_id,)
378
+ ),
379
+ occurred_at=now,
380
+ )
381
+ ],
382
+ )
383
+ self._uow.execute(
384
+ conn,
385
+ "insert_published_version",
386
+ (
387
+ namespace,
388
+ bundle.bundle_id,
389
+ bundle.fingerprint,
390
+ bundle.model_version,
391
+ PublishedVersionState.AVAILABLE.value,
392
+ predecessor_fingerprint,
393
+ None,
394
+ audit.audit_id if audit is not None else None,
395
+ now,
396
+ ),
397
+ )
398
+ if predecessor_fingerprint is not None:
399
+ self._uow.execute(
400
+ conn,
401
+ "update_version_successor",
402
+ (
403
+ bundle.fingerprint,
404
+ namespace,
405
+ bundle.bundle_id,
406
+ predecessor_fingerprint,
407
+ ),
408
+ )
409
+ self._uow.execute(
410
+ conn,
411
+ "insert_supersession_edge",
412
+ (
413
+ namespace,
414
+ bundle.bundle_id,
415
+ predecessor_fingerprint,
416
+ bundle.fingerprint,
417
+ now,
418
+ ),
419
+ )
420
+ self._uow.insert_event(
421
+ conn,
422
+ "bundle_published",
423
+ bundle.bundle_id,
424
+ namespace=namespace,
425
+ occurred_at=now,
426
+ )
427
+ return _success(
428
+ "published",
429
+ bundle,
430
+ audit_reference=audit.audit_id if audit is not None else None,
431
+ verification_evidence_reference=evidence_reference,
432
+ superseded_fingerprint=predecessor_fingerprint,
433
+ idempotency_status=PublishIdempotencyStatus.CREATED,
434
+ )
435
+
436
+ def get(
437
+ self,
438
+ bundle_id: str,
439
+ version: str,
440
+ /,
441
+ *,
442
+ tenant_scope_fingerprint: str | None = None,
443
+ ) -> SemanticModelBundle | None:
444
+ """Load a published Bundle by id and version."""
445
+ namespace = _namespace(tenant_scope_fingerprint)
446
+ with self._uow.transaction() as conn:
447
+ row = self._uow.execute(
448
+ conn,
449
+ "read_publication",
450
+ (namespace, bundle_id, version),
451
+ ).fetchone()
452
+ if row is None:
453
+ return None
454
+ envelope = self._uow.decode(
455
+ row["envelope"],
456
+ ArtifactKind.BUNDLE,
457
+ row_schema_version=row["schema_version"],
458
+ )
459
+ return self._uow.bundle_from_envelope(envelope)
460
+
461
+ def get_by_fingerprint(
462
+ self,
463
+ bundle_id: str,
464
+ fingerprint: str,
465
+ *,
466
+ tenant_scope_fingerprint: str | None = None,
467
+ ) -> SemanticModelBundle | None:
468
+ """Load an immutable Bundle by semantic fingerprint."""
469
+ namespace = _namespace(tenant_scope_fingerprint)
470
+ with self._uow.transaction() as conn:
471
+ row = self._uow.execute(
472
+ conn,
473
+ "read_publication_by_fingerprint",
474
+ (namespace, bundle_id, fingerprint),
475
+ ).fetchone()
476
+ if row is None:
477
+ return None
478
+ envelope = self._uow.decode(
479
+ row["envelope"],
480
+ ArtifactKind.BUNDLE,
481
+ row_schema_version=row["schema_version"],
482
+ )
483
+ return self._uow.bundle_from_envelope(envelope)
484
+
485
+
486
+ __all__ = ["PublicationRepository"]