etlantic-sqlmodel 0.45.0__tar.gz → 0.47.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 (22) hide show
  1. {etlantic_sqlmodel-0.45.0 → etlantic_sqlmodel-0.47.0}/PKG-INFO +6 -6
  2. {etlantic_sqlmodel-0.45.0 → etlantic_sqlmodel-0.47.0}/README.md +4 -4
  3. {etlantic_sqlmodel-0.45.0 → etlantic_sqlmodel-0.47.0}/pyproject.toml +2 -2
  4. {etlantic_sqlmodel-0.45.0 → etlantic_sqlmodel-0.47.0}/src/etlantic_sqlmodel/__init__.py +1 -1
  5. {etlantic_sqlmodel-0.45.0 → etlantic_sqlmodel-0.47.0}/src/etlantic_sqlmodel/control_plane/__init__.py +10 -0
  6. {etlantic_sqlmodel-0.45.0 → etlantic_sqlmodel-0.47.0}/src/etlantic_sqlmodel/control_plane/models.py +16 -0
  7. etlantic_sqlmodel-0.47.0/src/etlantic_sqlmodel/control_plane/schedule_stores.py +252 -0
  8. {etlantic_sqlmodel-0.45.0 → etlantic_sqlmodel-0.47.0}/src/etlantic_sqlmodel/migrations/__init__.py +1 -0
  9. etlantic_sqlmodel-0.47.0/src/etlantic_sqlmodel/migrations/versions/004_schedules_0_47.py +29 -0
  10. {etlantic_sqlmodel-0.45.0 → etlantic_sqlmodel-0.47.0}/.gitignore +0 -0
  11. {etlantic_sqlmodel-0.45.0 → etlantic_sqlmodel-0.47.0}/src/etlantic_sqlmodel/control_plane/cp4_stores.py +0 -0
  12. {etlantic_sqlmodel-0.45.0 → etlantic_sqlmodel-0.47.0}/src/etlantic_sqlmodel/control_plane/durable_stores.py +0 -0
  13. {etlantic_sqlmodel-0.45.0 → etlantic_sqlmodel-0.47.0}/src/etlantic_sqlmodel/control_plane/registry_backup.py +0 -0
  14. {etlantic_sqlmodel-0.45.0 → etlantic_sqlmodel-0.47.0}/src/etlantic_sqlmodel/control_plane/registry_search.py +0 -0
  15. {etlantic_sqlmodel-0.45.0 → etlantic_sqlmodel-0.47.0}/src/etlantic_sqlmodel/control_plane/registry_stores.py +0 -0
  16. {etlantic_sqlmodel-0.45.0 → etlantic_sqlmodel-0.47.0}/src/etlantic_sqlmodel/control_plane/session.py +0 -0
  17. {etlantic_sqlmodel-0.45.0 → etlantic_sqlmodel-0.47.0}/src/etlantic_sqlmodel/control_plane/stores.py +0 -0
  18. {etlantic_sqlmodel-0.45.0 → etlantic_sqlmodel-0.47.0}/src/etlantic_sqlmodel/migrations/versions/001_registry_cp2.py +0 -0
  19. {etlantic_sqlmodel-0.45.0 → etlantic_sqlmodel-0.47.0}/src/etlantic_sqlmodel/migrations/versions/002_durable_cp3.py +0 -0
  20. {etlantic_sqlmodel-0.45.0 → etlantic_sqlmodel-0.47.0}/src/etlantic_sqlmodel/migrations/versions/003_cp4_governance.py +0 -0
  21. {etlantic_sqlmodel-0.45.0 → etlantic_sqlmodel-0.47.0}/src/etlantic_sqlmodel/migrations/versions/__init__.py +0 -0
  22. {etlantic_sqlmodel-0.45.0 → etlantic_sqlmodel-0.47.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.45.0
3
+ Version: 0.47.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.46,>=0.45.0
21
+ Requires-Dist: etlantic<0.48,>=0.47.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.45.0** — pin with core.
31
+ Package version is **0.47.0** — pin with core.
32
32
 
33
33
  ## Install
34
34
 
35
35
  ```bash
36
- pip install 'etlantic-sqlmodel==0.45.0'
37
- # pip install 'etlantic==0.45.0'
36
+ pip install 'etlantic-sqlmodel==0.47.0'
37
+ # pip install 'etlantic==0.47.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.45.0/10_REFERENCE/OPTIONAL_PACKAGES/) ·
99
+ [Optional packages](https://etlantic.readthedocs.io/en/v0.47.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.45.0** — pin with core.
7
+ Package version is **0.47.0** — pin with core.
8
8
 
9
9
  ## Install
10
10
 
11
11
  ```bash
12
- pip install 'etlantic-sqlmodel==0.45.0'
13
- # pip install 'etlantic==0.45.0'
12
+ pip install 'etlantic-sqlmodel==0.47.0'
13
+ # pip install 'etlantic==0.47.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.45.0/10_REFERENCE/OPTIONAL_PACKAGES/) ·
75
+ [Optional packages](https://etlantic.readthedocs.io/en/v0.47.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.45.0"
3
+ version = "0.47.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.45.0,<0.46",
20
+ "etlantic>=0.47.0,<0.48",
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.45.0"
55
+ __version__ = "0.47.0"
56
56
 
57
57
  __all__ = [
58
58
  "AliasRow",
@@ -37,6 +37,7 @@ from etlantic_sqlmodel.control_plane.models import (
37
37
  LogicalIdentityRow,
38
38
  PromotionRow,
39
39
  RevisionRow,
40
+ ScheduleSnapshotRow,
40
41
  SecurityDomainRow,
41
42
  SubmissionRow,
42
43
  TenantRow,
@@ -60,6 +61,11 @@ from etlantic_sqlmodel.control_plane.registry_stores import (
60
61
  SqlModelWorkspaceDirectory,
61
62
  create_registry_tables,
62
63
  )
64
+ from etlantic_sqlmodel.control_plane.schedule_stores import (
65
+ SCHEDULE_TABLES,
66
+ SQLModelScheduleStore,
67
+ create_schedule_tables,
68
+ )
63
69
  from etlantic_sqlmodel.control_plane.session import (
64
70
  create_sqlite_engine,
65
71
  make_session_factory,
@@ -78,6 +84,7 @@ __all__ = [
78
84
  "CP4_TABLES",
79
85
  "DURABLE_TABLES",
80
86
  "REGISTRY_TABLES",
87
+ "SCHEDULE_TABLES",
81
88
  "AliasRow",
82
89
  "BackupTranscript",
83
90
  "Cp4GovernanceSnapshotRow",
@@ -99,7 +106,9 @@ __all__ = [
99
106
  "SQLModelObjectiveStore",
100
107
  "SQLModelPolicyProvider",
101
108
  "SQLModelQuotaProvider",
109
+ "SQLModelScheduleStore",
102
110
  "SQLModelSubmissionStore",
111
+ "ScheduleSnapshotRow",
103
112
  "SecurityDomainRow",
104
113
  "SqlModelEventStore",
105
114
  "SqlModelRegistryProvider",
@@ -115,6 +124,7 @@ __all__ = [
115
124
  "create_cp4_tables",
116
125
  "create_durable_tables",
117
126
  "create_registry_tables",
127
+ "create_schedule_tables",
118
128
  "create_sqlite_engine",
119
129
  "dump_registry_sqlite",
120
130
  "load_registry_sqlite",
@@ -353,6 +353,21 @@ class Cp4GovernanceSnapshotRow(SQLModel, table=True):
353
353
  updated_at: str | None = None
354
354
 
355
355
 
356
+ class ScheduleSnapshotRow(SQLModel, table=True):
357
+ """Transactional 0.47 schedule-store snapshot (reference provider)."""
358
+
359
+ __tablename__ = "cp_schedule_snapshot"
360
+ __table_args__ = (UniqueConstraint("store_id", name="uq_cp_schedule_snapshot"),)
361
+
362
+ id: int | None = Field(default=None, primary_key=True)
363
+ store_id: str = Field(
364
+ sa_column=Column(String(), index=True, nullable=False), default="default"
365
+ )
366
+ payload_json: str = "{}"
367
+ payload_version: int = Field(default=0, sa_column=Column(Integer(), nullable=False))
368
+ updated_at: str | None = None
369
+
370
+
356
371
  __all__ = [
357
372
  "AliasRow",
358
373
  "Cp4GovernanceSnapshotRow",
@@ -365,6 +380,7 @@ __all__ = [
365
380
  "LogicalIdentityRow",
366
381
  "PromotionRow",
367
382
  "RevisionRow",
383
+ "ScheduleSnapshotRow",
368
384
  "SecurityDomainRow",
369
385
  "SubmissionRow",
370
386
  "TenantRow",
@@ -0,0 +1,252 @@
1
+ """SQLModel-backed ScheduleStore (0.47).
2
+
3
+ Reference provider: each mutating call loads the schedule snapshot inside a
4
+ database transaction, applies MemoryScheduleStore semantics, and writes the
5
+ snapshot back. When the paired DurableWorkStore is the SQLModel provider on
6
+ the same engine, firing claim and durable accept share one commit.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ from collections.abc import Callable, Sequence
13
+ from datetime import UTC, datetime
14
+ from typing import Any, TypeVar
15
+
16
+ from sqlalchemy.engine import Engine
17
+
18
+ from etlantic.control_plane.durable_protocols import DurableWorkStore
19
+ from etlantic.control_plane.errors import ControlPlaneError
20
+ from etlantic.control_plane.models import ControlPlaneContext
21
+ from etlantic.control_plane.schedule_memory import MemoryScheduleStore
22
+ from etlantic.control_plane.schedule_models import (
23
+ FiringRecord,
24
+ ScheduleRecord,
25
+ ScheduleSpec,
26
+ )
27
+ from etlantic.control_plane.schedule_protocols import SchedulerLeaderLease
28
+ from etlantic_sqlmodel.control_plane.durable_stores import SQLModelDurableWorkStore
29
+ from etlantic_sqlmodel.control_plane.models import ScheduleSnapshotRow
30
+ from etlantic_sqlmodel.control_plane.session import session_scope
31
+ from sqlmodel import Session, SQLModel, select
32
+
33
+ T = TypeVar("T")
34
+
35
+ SCHEDULE_TABLES = (ScheduleSnapshotRow,)
36
+
37
+
38
+ def create_schedule_tables(engine: Engine) -> None:
39
+ """Create schedule snapshot tables (tests/demos only)."""
40
+ SQLModel.metadata.create_all(
41
+ engine,
42
+ tables=[cls.__table__ for cls in SCHEDULE_TABLES], # type: ignore[list-item]
43
+ )
44
+
45
+
46
+ def _utcnow_iso() -> str:
47
+ return datetime.now(UTC).isoformat().replace("+00:00", "Z")
48
+
49
+
50
+ class SQLModelScheduleStore:
51
+ """Transactional ScheduleStore backed by a SQL snapshot row."""
52
+
53
+ def __init__(self, engine: Engine, *, store_id: str = "default") -> None:
54
+ self.engine = engine
55
+ self.store_id = store_id
56
+
57
+ def _txn(self, fn: Callable[[MemoryScheduleStore], T]) -> T:
58
+ with session_scope(self.engine) as session:
59
+ mem, version = self._read(session, for_update=True)
60
+ result = fn(mem)
61
+ self._write(session, mem, expected_version=version)
62
+ return result
63
+
64
+ def _read_only(self, fn: Callable[[MemoryScheduleStore], T]) -> T:
65
+ with session_scope(self.engine) as session:
66
+ mem, _version = self._read(session, for_update=False)
67
+ return fn(mem)
68
+
69
+ def _read(
70
+ self, session: Session, *, for_update: bool
71
+ ) -> tuple[MemoryScheduleStore, int]:
72
+ stmt = select(ScheduleSnapshotRow).where(
73
+ ScheduleSnapshotRow.store_id == self.store_id
74
+ )
75
+ if for_update:
76
+ stmt = stmt.with_for_update()
77
+ row = session.exec(stmt).first()
78
+ store = MemoryScheduleStore()
79
+ if row is None:
80
+ return store, 0
81
+ store.load(json.loads(row.payload_json or "{}"))
82
+ return store, int(row.payload_version or 0)
83
+
84
+ def _write(
85
+ self,
86
+ session: Session,
87
+ store: MemoryScheduleStore,
88
+ *,
89
+ expected_version: int,
90
+ ) -> None:
91
+ payload = json.dumps(store.dump(), sort_keys=True)
92
+ row = session.exec(
93
+ select(ScheduleSnapshotRow)
94
+ .where(ScheduleSnapshotRow.store_id == self.store_id)
95
+ .with_for_update()
96
+ ).first()
97
+ if row is None:
98
+ if expected_version != 0:
99
+ raise ControlPlaneError.conflict(
100
+ "Schedule snapshot version conflict (missing row)"
101
+ )
102
+ session.add(
103
+ ScheduleSnapshotRow(
104
+ store_id=self.store_id,
105
+ payload_json=payload,
106
+ payload_version=1,
107
+ updated_at=_utcnow_iso(),
108
+ )
109
+ )
110
+ return
111
+ current = int(row.payload_version or 0)
112
+ if current != expected_version:
113
+ raise ControlPlaneError.conflict(
114
+ "Schedule snapshot version conflict (stale write)"
115
+ )
116
+ row.payload_json = payload
117
+ row.payload_version = current + 1
118
+ row.updated_at = _utcnow_iso()
119
+ session.add(row)
120
+
121
+ def create(
122
+ self,
123
+ ctx: ControlPlaneContext,
124
+ *,
125
+ definition_id: str,
126
+ profile_name: str,
127
+ spec: ScheduleSpec,
128
+ schedule_id: str | None = None,
129
+ policy_fingerprint: str = "",
130
+ parameter_refs: dict[str, str] | None = None,
131
+ secret_refs: dict[str, str] | None = None,
132
+ next_fire_at: str | None = None,
133
+ ) -> ScheduleRecord:
134
+ return self._txn(
135
+ lambda m: m.create(
136
+ ctx,
137
+ definition_id=definition_id,
138
+ profile_name=profile_name,
139
+ spec=spec,
140
+ schedule_id=schedule_id,
141
+ policy_fingerprint=policy_fingerprint,
142
+ parameter_refs=parameter_refs,
143
+ secret_refs=secret_refs,
144
+ next_fire_at=next_fire_at,
145
+ )
146
+ )
147
+
148
+ def get(self, ctx: ControlPlaneContext, schedule_id: str) -> ScheduleRecord:
149
+ return self._read_only(lambda m: m.get(ctx, schedule_id))
150
+
151
+ def list_schedules(self, ctx: ControlPlaneContext) -> Sequence[ScheduleRecord]:
152
+ return self._read_only(lambda m: m.list_schedules(ctx))
153
+
154
+ def pause(self, ctx: ControlPlaneContext, schedule_id: str) -> ScheduleRecord:
155
+ return self._txn(lambda m: m.pause(ctx, schedule_id))
156
+
157
+ def resume(self, ctx: ControlPlaneContext, schedule_id: str) -> ScheduleRecord:
158
+ return self._txn(lambda m: m.resume(ctx, schedule_id))
159
+
160
+ def delete(self, ctx: ControlPlaneContext, schedule_id: str) -> ScheduleRecord:
161
+ return self._txn(lambda m: m.delete(ctx, schedule_id))
162
+
163
+ def acquire_leader_lease(
164
+ self, ctx: ControlPlaneContext, *, owner_id: str, ttl_seconds: int
165
+ ) -> SchedulerLeaderLease:
166
+ return self._txn(
167
+ lambda m: m.acquire_leader_lease(
168
+ ctx, owner_id=owner_id, ttl_seconds=ttl_seconds
169
+ )
170
+ )
171
+
172
+ def heartbeat_leader(
173
+ self,
174
+ ctx: ControlPlaneContext,
175
+ *,
176
+ owner_id: str,
177
+ fencing_token: int,
178
+ ttl_seconds: int,
179
+ ) -> SchedulerLeaderLease:
180
+ return self._txn(
181
+ lambda m: m.heartbeat_leader(
182
+ ctx,
183
+ owner_id=owner_id,
184
+ fencing_token=fencing_token,
185
+ ttl_seconds=ttl_seconds,
186
+ )
187
+ )
188
+
189
+ def release_leader(
190
+ self, ctx: ControlPlaneContext, *, owner_id: str, fencing_token: int
191
+ ) -> None:
192
+ self._txn(
193
+ lambda m: m.release_leader(
194
+ ctx, owner_id=owner_id, fencing_token=fencing_token
195
+ )
196
+ )
197
+
198
+ def due_schedules(
199
+ self, ctx: ControlPlaneContext, *, now: str
200
+ ) -> Sequence[ScheduleRecord]:
201
+ return self._read_only(lambda m: m.due_schedules(ctx, now=now))
202
+
203
+ def list_firings(
204
+ self, ctx: ControlPlaneContext, schedule_id: str
205
+ ) -> Sequence[FiringRecord]:
206
+ return self._read_only(lambda m: m.list_firings(ctx, schedule_id))
207
+
208
+ def claim_firing(
209
+ self,
210
+ ctx: ControlPlaneContext,
211
+ *,
212
+ schedule_id: str,
213
+ revision_id: str,
214
+ nominal_fire_time: str,
215
+ owner_id: str,
216
+ fencing_token: int,
217
+ plan_fingerprint: str,
218
+ durable: DurableWorkStore | None = None,
219
+ next_fire_at: str | None = None,
220
+ require_leader_lease: bool = True,
221
+ skip_status: str | None = None,
222
+ ) -> tuple[FiringRecord, bool]:
223
+ kwargs: dict[str, Any] = {
224
+ "schedule_id": schedule_id,
225
+ "revision_id": revision_id,
226
+ "nominal_fire_time": nominal_fire_time,
227
+ "owner_id": owner_id,
228
+ "fencing_token": fencing_token,
229
+ "plan_fingerprint": plan_fingerprint,
230
+ "next_fire_at": next_fire_at,
231
+ "require_leader_lease": require_leader_lease,
232
+ "skip_status": skip_status,
233
+ }
234
+ if (
235
+ isinstance(durable, SQLModelDurableWorkStore)
236
+ and durable.engine is self.engine
237
+ ):
238
+ with session_scope(self.engine) as session:
239
+ sched, sv = self._read(session, for_update=True)
240
+ dur_mem, dv = durable._read(session, for_update=True)
241
+ result = sched.claim_firing(ctx, durable=dur_mem, **kwargs)
242
+ self._write(session, sched, expected_version=sv)
243
+ durable._write(session, dur_mem, expected_version=dv)
244
+ return result
245
+ return self._txn(lambda m: m.claim_firing(ctx, durable=durable, **kwargs))
246
+
247
+
248
+ __all__ = [
249
+ "SCHEDULE_TABLES",
250
+ "SQLModelScheduleStore",
251
+ "create_schedule_tables",
252
+ ]
@@ -18,6 +18,7 @@ VERSIONS: Sequence[str] = (
18
18
  "001_registry_cp2",
19
19
  "002_durable_cp3",
20
20
  "003_cp4_governance",
21
+ "004_schedules_0_47",
21
22
  )
22
23
 
23
24
 
@@ -0,0 +1,29 @@
1
+ """004 — schedule snapshot storage for 0.47 ScheduleStore."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from sqlalchemy.engine import Engine
6
+
7
+ from etlantic_sqlmodel.control_plane.models import ScheduleSnapshotRow
8
+ from sqlmodel import SQLModel
9
+
10
+ TABLES = (ScheduleSnapshotRow,)
11
+
12
+
13
+ def upgrade(engine: Engine) -> None:
14
+ """Create schedule snapshot tables."""
15
+ SQLModel.metadata.create_all(
16
+ engine,
17
+ tables=[cls.__table__ for cls in TABLES], # type: ignore[list-item]
18
+ )
19
+
20
+
21
+ def downgrade(engine: Engine) -> None:
22
+ """Drop schedule snapshot tables."""
23
+ SQLModel.metadata.drop_all(
24
+ engine,
25
+ tables=[cls.__table__ for cls in reversed(TABLES)], # type: ignore[list-item]
26
+ )
27
+
28
+
29
+ __all__ = ["downgrade", "upgrade"]