etlantic-sqlmodel 0.55.0__tar.gz → 0.56.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.
Files changed (36) hide show
  1. {etlantic_sqlmodel-0.55.0 → etlantic_sqlmodel-0.56.0}/PKG-INFO +6 -6
  2. {etlantic_sqlmodel-0.55.0 → etlantic_sqlmodel-0.56.0}/README.md +4 -4
  3. {etlantic_sqlmodel-0.55.0 → etlantic_sqlmodel-0.56.0}/pyproject.toml +2 -2
  4. {etlantic_sqlmodel-0.55.0 → etlantic_sqlmodel-0.56.0}/src/etlantic_sqlmodel/__init__.py +1 -1
  5. {etlantic_sqlmodel-0.55.0 → etlantic_sqlmodel-0.56.0}/src/etlantic_sqlmodel/control_plane/__init__.py +14 -0
  6. {etlantic_sqlmodel-0.55.0 → etlantic_sqlmodel-0.56.0}/src/etlantic_sqlmodel/control_plane/cp4_stores.py +103 -23
  7. etlantic_sqlmodel-0.56.0/src/etlantic_sqlmodel/control_plane/durable_stores.py +660 -0
  8. etlantic_sqlmodel-0.56.0/src/etlantic_sqlmodel/control_plane/input_resource_stores.py +650 -0
  9. {etlantic_sqlmodel-0.55.0 → etlantic_sqlmodel-0.56.0}/src/etlantic_sqlmodel/control_plane/models.py +91 -1
  10. {etlantic_sqlmodel-0.55.0 → etlantic_sqlmodel-0.56.0}/src/etlantic_sqlmodel/control_plane/registry_stores.py +195 -4
  11. etlantic_sqlmodel-0.56.0/src/etlantic_sqlmodel/control_plane/report_stores.py +238 -0
  12. {etlantic_sqlmodel-0.55.0 → etlantic_sqlmodel-0.56.0}/src/etlantic_sqlmodel/control_plane/schedule_stores.py +82 -8
  13. etlantic_sqlmodel-0.56.0/src/etlantic_sqlmodel/control_plane/stores.py +917 -0
  14. {etlantic_sqlmodel-0.55.0 → etlantic_sqlmodel-0.56.0}/src/etlantic_sqlmodel/migrations/__init__.py +9 -0
  15. etlantic_sqlmodel-0.56.0/src/etlantic_sqlmodel/migrations/versions/006_managed_definition_revisions_0_56.py +123 -0
  16. etlantic_sqlmodel-0.56.0/src/etlantic_sqlmodel/migrations/versions/007_managed_run_reports_0_56.py +27 -0
  17. etlantic_sqlmodel-0.56.0/src/etlantic_sqlmodel/migrations/versions/008_idempotent_run_events_0_56.py +54 -0
  18. etlantic_sqlmodel-0.56.0/src/etlantic_sqlmodel/migrations/versions/009_event_retention_tombstones_0_56.py +99 -0
  19. etlantic_sqlmodel-0.56.0/src/etlantic_sqlmodel/migrations/versions/010_immutable_input_resources_0_56.py +90 -0
  20. etlantic_sqlmodel-0.56.0/src/etlantic_sqlmodel/migrations/versions/011_run_artifact_retention_0_56.py +136 -0
  21. etlantic_sqlmodel-0.56.0/src/etlantic_sqlmodel/migrations/versions/012_bounded_event_tombstone_retention_0_56.py +106 -0
  22. etlantic_sqlmodel-0.56.0/src/etlantic_sqlmodel/migrations/versions/013_durable_submission_scope_backfill_0_56.py +105 -0
  23. etlantic_sqlmodel-0.56.0/src/etlantic_sqlmodel/migrations/versions/014_cp1_complete_principal_idempotency_0_56.py +326 -0
  24. etlantic_sqlmodel-0.55.0/src/etlantic_sqlmodel/control_plane/durable_stores.py +0 -366
  25. etlantic_sqlmodel-0.55.0/src/etlantic_sqlmodel/control_plane/stores.py +0 -502
  26. {etlantic_sqlmodel-0.55.0 → etlantic_sqlmodel-0.56.0}/.gitignore +0 -0
  27. {etlantic_sqlmodel-0.55.0 → etlantic_sqlmodel-0.56.0}/src/etlantic_sqlmodel/control_plane/registry_backup.py +0 -0
  28. {etlantic_sqlmodel-0.55.0 → etlantic_sqlmodel-0.56.0}/src/etlantic_sqlmodel/control_plane/registry_search.py +0 -0
  29. {etlantic_sqlmodel-0.55.0 → etlantic_sqlmodel-0.56.0}/src/etlantic_sqlmodel/control_plane/session.py +0 -0
  30. {etlantic_sqlmodel-0.55.0 → etlantic_sqlmodel-0.56.0}/src/etlantic_sqlmodel/migrations/versions/001_registry_cp2.py +0 -0
  31. {etlantic_sqlmodel-0.55.0 → etlantic_sqlmodel-0.56.0}/src/etlantic_sqlmodel/migrations/versions/002_durable_cp3.py +0 -0
  32. {etlantic_sqlmodel-0.55.0 → etlantic_sqlmodel-0.56.0}/src/etlantic_sqlmodel/migrations/versions/003_cp4_governance.py +0 -0
  33. {etlantic_sqlmodel-0.55.0 → etlantic_sqlmodel-0.56.0}/src/etlantic_sqlmodel/migrations/versions/004_schedules_0_47.py +0 -0
  34. {etlantic_sqlmodel-0.55.0 → etlantic_sqlmodel-0.56.0}/src/etlantic_sqlmodel/migrations/versions/005_cp1_reference.py +0 -0
  35. {etlantic_sqlmodel-0.55.0 → etlantic_sqlmodel-0.56.0}/src/etlantic_sqlmodel/migrations/versions/__init__.py +0 -0
  36. {etlantic_sqlmodel-0.55.0 → etlantic_sqlmodel-0.56.0}/src/etlantic_sqlmodel/py.typed +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: etlantic-sqlmodel
3
- Version: 0.55.0
3
+ Version: 0.56.0
4
4
  Summary: ContractModel ↔ SQLModel bridge for ETLantic.
5
5
  Project-URL: Homepage, https://github.com/eddiethedean/etlantic
6
6
  Project-URL: Documentation, https://github.com/eddiethedean/etlantic/tree/main/docs
@@ -18,7 +18,7 @@ Classifier: Programming Language :: Python :: 3.12
18
18
  Classifier: Programming Language :: Python :: 3.13
19
19
  Classifier: Typing :: Typed
20
20
  Requires-Python: >=3.11
21
- Requires-Dist: etlantic<0.56,>=0.55.0
21
+ Requires-Dist: etlantic<0.57,>=0.56.0
22
22
  Requires-Dist: sqlmodel<1,>=0.0.22
23
23
  Description-Content-Type: text/markdown
24
24
 
@@ -28,13 +28,13 @@ Optional bridge between ETLantic `Data` contracts and
28
28
  [SQLModel](https://sqlmodel.tiangolo.com/) table models, plus optional CP1
29
29
  control-plane reference stores. Install when you need `contract_to_sqlmodel`
30
30
  helpers or SQLModel-backed definition/submission stores for local CP1 demos.
31
- Package version is **0.55.0** — pin with core.
31
+ Package version is **0.56.0** — pin with core.
32
32
 
33
33
  ## Install
34
34
 
35
35
  ```bash
36
- pip install 'etlantic-sqlmodel==0.55.0'
37
- # pip install 'etlantic==0.55.0'
36
+ pip install 'etlantic-sqlmodel==0.56.0'
37
+ # pip install 'etlantic==0.56.0'
38
38
  ```
39
39
 
40
40
  ## Schema bridge
@@ -96,6 +96,6 @@ for isolation queries. Use separate engines/schemas per tenant for
96
96
 
97
97
  ## Links
98
98
 
99
- [Optional packages](https://etlantic.readthedocs.io/en/v0.55.0/10_REFERENCE/OPTIONAL_PACKAGES/) ·
99
+ [Optional packages](https://etlantic.readthedocs.io/en/v0.56.0/10_REFERENCE/OPTIONAL_PACKAGES/) ·
100
100
  [Source](https://github.com/eddiethedean/etlantic/tree/main/packages/etlantic-sqlmodel) ·
101
101
  [Issues](https://github.com/eddiethedean/etlantic/issues)
@@ -4,13 +4,13 @@ Optional bridge between ETLantic `Data` contracts and
4
4
  [SQLModel](https://sqlmodel.tiangolo.com/) table models, plus optional CP1
5
5
  control-plane reference stores. Install when you need `contract_to_sqlmodel`
6
6
  helpers or SQLModel-backed definition/submission stores for local CP1 demos.
7
- Package version is **0.55.0** — pin with core.
7
+ Package version is **0.56.0** — pin with core.
8
8
 
9
9
  ## Install
10
10
 
11
11
  ```bash
12
- pip install 'etlantic-sqlmodel==0.55.0'
13
- # pip install 'etlantic==0.55.0'
12
+ pip install 'etlantic-sqlmodel==0.56.0'
13
+ # pip install 'etlantic==0.56.0'
14
14
  ```
15
15
 
16
16
  ## Schema bridge
@@ -72,6 +72,6 @@ for isolation queries. Use separate engines/schemas per tenant for
72
72
 
73
73
  ## Links
74
74
 
75
- [Optional packages](https://etlantic.readthedocs.io/en/v0.55.0/10_REFERENCE/OPTIONAL_PACKAGES/) ·
75
+ [Optional packages](https://etlantic.readthedocs.io/en/v0.56.0/10_REFERENCE/OPTIONAL_PACKAGES/) ·
76
76
  [Source](https://github.com/eddiethedean/etlantic/tree/main/packages/etlantic-sqlmodel) ·
77
77
  [Issues](https://github.com/eddiethedean/etlantic/issues)
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "etlantic-sqlmodel"
3
- version = "0.55.0"
3
+ version = "0.56.0"
4
4
  description = "ContractModel ↔ SQLModel bridge for ETLantic."
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -17,7 +17,7 @@ classifiers = [
17
17
  "Typing :: Typed",
18
18
  ]
19
19
  dependencies = [
20
- "etlantic>=0.55.0,<0.56",
20
+ "etlantic>=0.56.0,<0.57",
21
21
  "sqlmodel>=0.0.22,<1",
22
22
  ]
23
23
 
@@ -52,7 +52,7 @@ from etlantic_sqlmodel.migrations import (
52
52
  )
53
53
  from sqlmodel import Field, SQLModel
54
54
 
55
- __version__ = "0.55.0"
55
+ __version__ = "0.56.0"
56
56
 
57
57
  __all__ = [
58
58
  "AliasRow",
@@ -25,6 +25,9 @@ from etlantic_sqlmodel.control_plane.durable_stores import (
25
25
  SQLModelDurableWorkStore,
26
26
  create_durable_tables,
27
27
  )
28
+ from etlantic_sqlmodel.control_plane.input_resource_stores import (
29
+ SqlModelInputResourceStore,
30
+ )
28
31
  from etlantic_sqlmodel.control_plane.models import (
29
32
  AliasRow,
30
33
  Cp4GovernanceSnapshotRow,
@@ -37,6 +40,7 @@ from etlantic_sqlmodel.control_plane.models import (
37
40
  LogicalIdentityRow,
38
41
  PromotionRow,
39
42
  RevisionRow,
43
+ RunReportRow,
40
44
  ScheduleSnapshotRow,
41
45
  SecurityDomainRow,
42
46
  SubmissionRow,
@@ -61,6 +65,11 @@ from etlantic_sqlmodel.control_plane.registry_stores import (
61
65
  SqlModelWorkspaceDirectory,
62
66
  create_registry_tables,
63
67
  )
68
+ from etlantic_sqlmodel.control_plane.report_stores import (
69
+ SqlModelRunReportStore,
70
+ SqlModelRunReportStoreProvider,
71
+ create_run_report_tables,
72
+ )
64
73
  from etlantic_sqlmodel.control_plane.schedule_stores import (
65
74
  SCHEDULE_TABLES,
66
75
  SQLModelScheduleStore,
@@ -97,6 +106,7 @@ __all__ = [
97
106
  "LogicalIdentityRow",
98
107
  "PromotionRow",
99
108
  "RevisionRow",
109
+ "RunReportRow",
100
110
  "SQLModelApprovalStore",
101
111
  "SQLModelAttestationStore",
102
112
  "SQLModelAuditEvidenceStore",
@@ -111,8 +121,11 @@ __all__ = [
111
121
  "ScheduleSnapshotRow",
112
122
  "SecurityDomainRow",
113
123
  "SqlModelEventStore",
124
+ "SqlModelInputResourceStore",
114
125
  "SqlModelRegistryProvider",
115
126
  "SqlModelRevisionRegistry",
127
+ "SqlModelRunReportStore",
128
+ "SqlModelRunReportStoreProvider",
116
129
  "SqlModelTenantDirectory",
117
130
  "SqlModelWorkspaceDirectory",
118
131
  "SubmissionRow",
@@ -124,6 +137,7 @@ __all__ = [
124
137
  "create_cp4_tables",
125
138
  "create_durable_tables",
126
139
  "create_registry_tables",
140
+ "create_run_report_tables",
127
141
  "create_schedule_tables",
128
142
  "create_sqlite_engine",
129
143
  "dump_registry_sqlite",
@@ -5,9 +5,11 @@ from __future__ import annotations
5
5
  import json
6
6
  from collections.abc import Callable, Mapping, Sequence
7
7
  from datetime import UTC, datetime
8
- from typing import Any, TypeVar
8
+ from typing import Any, TypeVar, cast
9
9
 
10
+ from sqlalchemy import Table, update
10
11
  from sqlalchemy.engine import Engine
12
+ from sqlalchemy.exc import IntegrityError
11
13
 
12
14
  from etlantic.control_plane.approval_memory import MemoryApprovalStore
13
15
  from etlantic.control_plane.approval_models import (
@@ -36,7 +38,12 @@ from etlantic.control_plane.objective_models import (
36
38
  )
37
39
  from etlantic.control_plane.policy_memory import MemoryPolicyProvider
38
40
  from etlantic.control_plane.quota_memory import MemoryQuotaProvider
39
- from etlantic.control_plane.quota_models import QuotaState
41
+ from etlantic.control_plane.quota_models import (
42
+ QuotaDecision,
43
+ QuotaEffect,
44
+ QuotaResource,
45
+ QuotaState,
46
+ )
40
47
  from etlantic_sqlmodel.control_plane.models import Cp4GovernanceSnapshotRow
41
48
  from etlantic_sqlmodel.control_plane.session import session_scope
42
49
  from sqlmodel import Session, SQLModel, select
@@ -112,6 +119,7 @@ class _SnapshotBackedStore:
112
119
 
113
120
  def _txn(self, fn: Callable[[Any], T]) -> T:
114
121
  with session_scope(self.engine) as session:
122
+ self._ensure_snapshot_row(session)
115
123
  mem, version = self._read(session, for_update=True)
116
124
  result = fn(mem)
117
125
  self._write(session, mem, expected_version=version)
@@ -137,33 +145,65 @@ class _SnapshotBackedStore:
137
145
  row.payload_version or 0
138
146
  )
139
147
 
140
- def _write(self, session: Session, store: Any, *, expected_version: int) -> None:
141
- payload = json.dumps(self._dump(store), sort_keys=True, default=str)
142
- row = session.exec(
148
+ def _ensure_snapshot_row(self, session: Session) -> None:
149
+ """Create a version-zero lock row without racing on first use.
150
+
151
+ ``SELECT FOR UPDATE`` cannot lock a row that does not exist. When two
152
+ processes perform the first governance write at once, both would
153
+ otherwise observe absence and race to insert the unique
154
+ ``(store_id, kind)`` row. Insert under a savepoint so the loser can
155
+ recover from that uniqueness race and continue by locking the winner's
156
+ committed row.
157
+ """
158
+ statement = (
143
159
  select(Cp4GovernanceSnapshotRow)
144
160
  .where(Cp4GovernanceSnapshotRow.store_id == self.store_id)
145
161
  .where(Cp4GovernanceSnapshotRow.kind == self.kind)
146
- .with_for_update()
147
- ).first()
148
- if row is None:
149
- if expected_version != 0:
150
- raise ControlPlaneError.conflict("CP4 snapshot conflict")
151
- session.add(
152
- Cp4GovernanceSnapshotRow(
153
- store_id=self.store_id,
154
- kind=self.kind,
155
- payload_json=payload,
156
- payload_version=1,
157
- updated_at=_utcnow_iso(),
162
+ )
163
+ if session.exec(statement).first() is not None:
164
+ return
165
+
166
+ try:
167
+ with session.begin_nested():
168
+ session.add(
169
+ Cp4GovernanceSnapshotRow(
170
+ store_id=self.store_id,
171
+ kind=self.kind,
172
+ payload_json=json.dumps(
173
+ self._dump(self._empty()), sort_keys=True, default=str
174
+ ),
175
+ payload_version=0,
176
+ updated_at=_utcnow_iso(),
177
+ )
158
178
  )
179
+ session.flush()
180
+ except IntegrityError:
181
+ # The conflict is recoverable only when another transaction created
182
+ # the exact scoped snapshot row. Other integrity errors still fail
183
+ # closed.
184
+ if session.exec(statement).first() is None:
185
+ raise
186
+
187
+ def _write(self, session: Session, store: Any, *, expected_version: int) -> None:
188
+ payload = json.dumps(self._dump(store), sort_keys=True, default=str)
189
+ # Compare the version in the UPDATE itself. SQLite ignores FOR UPDATE,
190
+ # so an ORM read followed by a plain write can overwrite another
191
+ # transaction's quota claims and release the wrong reservation.
192
+ # SQLModel adds this table attribute when the model class is mapped.
193
+ table = cast(Table, Cp4GovernanceSnapshotRow.__table__) # pyright: ignore[reportUnknownMemberType, reportAttributeAccessIssue]
194
+ result = session.connection().execute(
195
+ update(table)
196
+ .where(table.c.store_id == self.store_id)
197
+ .where(table.c.kind == self.kind)
198
+ .where(table.c.payload_version == expected_version)
199
+ .values(
200
+ payload_json=payload,
201
+ payload_version=expected_version + 1,
202
+ updated_at=_utcnow_iso(),
159
203
  )
160
- return
161
- if int(row.payload_version or 0) != expected_version:
204
+ )
205
+ if result.rowcount != 1:
162
206
  raise ControlPlaneError.conflict("CP4 snapshot conflict")
163
- row.payload_json = payload
164
- row.payload_version = expected_version + 1
165
- row.updated_at = _utcnow_iso()
166
- session.add(row)
167
207
 
168
208
 
169
209
  class SQLModelAuditEvidenceStore(_SnapshotBackedStore):
@@ -321,6 +361,22 @@ class SQLModelQuotaProvider(_SnapshotBackedStore):
321
361
  "default_limits": dict(mem.default_limits),
322
362
  "weights": {f"{t}|{w}": wt for (t, w), wt in mem.weights.items()},
323
363
  "states": {f"{t}|{w}": s.to_dict() for (t, w), s in mem._states.items()},
364
+ "admissions": {
365
+ f"{t}|{w}|{key}": {
366
+ "resource": resource,
367
+ "units": units,
368
+ "decision": decision.to_dict(),
369
+ }
370
+ for (t, w, key), (resource, units, decision) in mem._admissions.items()
371
+ },
372
+ "releases": {
373
+ f"{t}|{w}|{key}": {"resource": resource, "units": units}
374
+ for (t, w, key), (resource, units) in mem._releases.items() # pyright: ignore[reportPrivateUsage]
375
+ },
376
+ "claims": {
377
+ f"{t}|{w}|{key}": sorted(claims)
378
+ for (t, w, key), claims in mem._claims.items() # pyright: ignore[reportPrivateUsage]
379
+ },
324
380
  "rr_cursor": int(mem._rr_cursor),
325
381
  "shared_pressure": bool(getattr(mem, "shared_pressure", False)),
326
382
  }
@@ -348,7 +404,31 @@ class SQLModelQuotaProvider(_SnapshotBackedStore):
348
404
  if raw.get("updated_at")
349
405
  else datetime.now(UTC),
350
406
  )
407
+ for key, raw in dict(payload.get("admissions") or {}).items():
408
+ t, w, idem = str(key).split("|", 2)
409
+ decision = raw["decision"]
410
+ mem._admissions[(t, w, idem)] = (
411
+ cast(QuotaResource, str(raw["resource"])),
412
+ int(raw["units"]),
413
+ QuotaDecision(
414
+ effect=cast(QuotaEffect, str(decision["effect"])),
415
+ resource=cast(QuotaResource, str(decision["resource"])),
416
+ limit=int(decision["limit"]),
417
+ used=int(decision["used"]),
418
+ reason=str(decision["reason"]),
419
+ metadata=dict(decision.get("metadata") or {}),
420
+ ),
421
+ )
422
+ for key, raw in dict(payload.get("releases") or {}).items():
423
+ t, w, idem = str(key).split("|", 2)
424
+ mem._releases[(t, w, idem)] = ( # pyright: ignore[reportPrivateUsage]
425
+ cast(QuotaResource, str(raw["resource"])),
426
+ int(raw["units"]),
427
+ )
351
428
  mem._rr_cursor = int(payload.get("rr_cursor") or 0)
429
+ for key, claims in dict(payload.get("claims") or {}).items():
430
+ t, w, idem = str(key).split("|", 2)
431
+ mem._claims[(t, w, idem)] = set(claims) # pyright: ignore[reportPrivateUsage]
352
432
  mem.shared_pressure = bool(payload.get("shared_pressure"))
353
433
  return mem
354
434