smart-data-engine-sdk 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 (73) hide show
  1. sde/__init__.py +318 -0
  2. sde/_cutover_project.py +179 -0
  3. sde/_local_state.py +188 -0
  4. sde/_operator_deadline.py +50 -0
  5. sde/_usage.py +314 -0
  6. sde/bulk.py +79 -0
  7. sde/canonical.py +141 -0
  8. sde/capabilities.py +62 -0
  9. sde/cutover.py +286 -0
  10. sde/engines/__init__.py +0 -0
  11. sde/engines/_clickhouse_connection.py +224 -0
  12. sde/engines/_index_build.py +294 -0
  13. sde/engines/_operator.py +394 -0
  14. sde/engines/_staging.py +222 -0
  15. sde/engines/_storage.py +22 -0
  16. sde/engines/_write_fences.py +271 -0
  17. sde/engines/clickhouse.py +1115 -0
  18. sde/engines/orderbook.py +457 -0
  19. sde/engines/postgres.py +967 -0
  20. sde/entity.py +170 -0
  21. sde/errors.py +103 -0
  22. sde/explain.py +300 -0
  23. sde/frozen_verification.py +152 -0
  24. sde/generation.py +131 -0
  25. sde/groups.py +97 -0
  26. sde/hashing.py +242 -0
  27. sde/index_build.py +313 -0
  28. sde/index_operator.py +347 -0
  29. sde/infer.py +461 -0
  30. sde/inspection.py +62 -0
  31. sde/internal.py +90 -0
  32. sde/layout.py +669 -0
  33. sde/local_cutover.py +801 -0
  34. sde/logging.py +143 -0
  35. sde/migration.py +856 -0
  36. sde/model.py +482 -0
  37. sde/physical.py +531 -0
  38. sde/placement.py +1010 -0
  39. sde/provisioning.py +63 -0
  40. sde/py.typed +0 -0
  41. sde/query.py +521 -0
  42. sde/routing.py +85 -0
  43. sde/schema.py +466 -0
  44. sde/session.py +993 -0
  45. sde/shapes.py +153 -0
  46. sde/staging.py +264 -0
  47. sde/staging_operator.py +393 -0
  48. sde/telemetry.py +1087 -0
  49. sde/testing/__init__.py +14 -0
  50. sde/testing/loader.py +175 -0
  51. sde/testing/memory.py +331 -0
  52. sde/types.py +228 -0
  53. sde/verification.py +220 -0
  54. sde/watermark.py +222 -0
  55. sde/write_fence.py +283 -0
  56. sde_demo/__init__.py +1 -0
  57. sde_demo/__main__.py +183 -0
  58. sde_demo/diagnostics.py +92 -0
  59. sde_demo/model.py +75 -0
  60. sde_demo/project.py +312 -0
  61. sde_demo/py.typed +0 -0
  62. sde_demo/query_count.py +301 -0
  63. sde_demo/resources.py +969 -0
  64. sde_demo/runtime.py +419 -0
  65. sde_demo/verification.py +242 -0
  66. sde_operator/__init__.py +1 -0
  67. sde_operator/__main__.py +210 -0
  68. smart_data_engine_sdk-0.1.0.dist-info/METADATA +174 -0
  69. smart_data_engine_sdk-0.1.0.dist-info/RECORD +73 -0
  70. smart_data_engine_sdk-0.1.0.dist-info/WHEEL +4 -0
  71. smart_data_engine_sdk-0.1.0.dist-info/entry_points.txt +3 -0
  72. smart_data_engine_sdk-0.1.0.dist-info/licenses/LICENSE +201 -0
  73. smart_data_engine_sdk-0.1.0.dist-info/licenses/NOTICE +13 -0
sde/write_fence.py ADDED
@@ -0,0 +1,283 @@
1
+ """Engine-enforced write generations and named barriers, entirely on the client's side.
2
+
3
+ These are primitives for a cutover executor, not permission to switch a placement map. The caller
4
+ must still close the source and target, compare their stable contents and durably commit its choice.
5
+ A generation travels with each write; checking it before an INSERT would leave a paused process
6
+ able to commit against a stale placement. Native CHECK constraints make that test part of INSERT.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import re
12
+ from collections.abc import Mapping
13
+ from dataclasses import dataclass
14
+ from typing import Any, Literal, Protocol
15
+
16
+ from .errors import MigrationRefused
17
+ from .generation import DRAIN_TABLE as DRAIN_TABLE
18
+ from .generation import EPOCH_COLUMN as EPOCH_COLUMN
19
+ from .generation import MAX_EPOCH as MAX_EPOCH
20
+ from .generation import check_epoch as check_epoch
21
+ from .placement import BACKFILL_TABLE, WATERMARK_TABLE
22
+
23
+ FENCE_PREFIX = "__sde_f_"
24
+ SETUP = FENCE_PREFIX + "setup"
25
+ ColumnState = Literal["absent", "valid", "conflict"]
26
+
27
+
28
+ @dataclass(frozen=True)
29
+ class FenceMetadata:
30
+ identity: str
31
+ column: ColumnState
32
+ constraints: Mapping[str, str]
33
+
34
+
35
+ class FenceBackend(Protocol):
36
+ """DDL capability held by provisioning/execution roles, never required by a runtime role."""
37
+
38
+ def metadata(self, table: str) -> FenceMetadata: ...
39
+ def add_column(self, table: str) -> None: ...
40
+ def add_constraint(self, table: str, name: str, expression: str) -> None: ...
41
+ def drop_constraint(self, table: str, name: str) -> None: ...
42
+ def drain(self, table: str, *, project_id: str, hold: str) -> None: ...
43
+ def restore(self, table: str, *, project_id: str, hold: str) -> None: ...
44
+
45
+
46
+ def _identity(value: str, label: str) -> None:
47
+ if not isinstance(value, str) or re.fullmatch("[0-9a-f]{32}", value) is None:
48
+ raise MigrationRefused(f"write fence {label} must be 32 lowercase hexadecimal digits")
49
+
50
+
51
+ def _predicate(value: str) -> str:
52
+ """Normalize only our predicate grammar, preserving quoted identifiers and their spaces."""
53
+ value = value.strip()
54
+ if value.startswith("CHECK"):
55
+ value = value[5:].strip()
56
+ value = re.sub(r"\s+NOT\s+VALID$", "", value)
57
+ while value.startswith("(") and value.endswith(")"):
58
+ value = value[1:-1].strip()
59
+ if value in ("true", "1"):
60
+ return "1"
61
+ if value in ("false", "0"):
62
+ return "0"
63
+ matched = re.fullmatch(
64
+ r'\(*\s*(?:"__sde_write_epoch"|`__sde_write_epoch`|__sde_write_epoch)'
65
+ r"\s*\)*\s*(>=|<=)\s*\(*\s*(?:([0-9]+)|'([0-9]+)'::bigint)\s*\)*",
66
+ value,
67
+ )
68
+ if matched is None:
69
+ return "<unrecognized>"
70
+ number = matched.group(2) or matched.group(3)
71
+ return EPOCH_COLUMN + matched.group(1) + str(int(number))
72
+
73
+
74
+ @dataclass(frozen=True)
75
+ class FenceState:
76
+ identity: str
77
+ project_id: str | None
78
+ column: ColumnState
79
+ minimums: tuple[int, ...]
80
+ maximums: tuple[int, ...]
81
+ holds: tuple[str, ...]
82
+ retired: tuple[str, ...]
83
+
84
+ @property
85
+ def lower_epoch(self) -> int | None:
86
+ return max(self.minimums) if self.minimums else None
87
+
88
+ @property
89
+ def upper_epoch(self) -> int | None:
90
+ return min(self.maximums) if self.maximums else None
91
+
92
+ @property
93
+ def complete(self) -> bool:
94
+ return (
95
+ self.project_id is not None
96
+ and self.column == "valid"
97
+ and bool(self.minimums and self.maximums)
98
+ )
99
+
100
+ @property
101
+ def closed(self) -> bool:
102
+ low, high = self.lower_epoch, self.upper_epoch
103
+ return bool(self.holds) or (low is not None and high is not None and low > high)
104
+
105
+ @property
106
+ def epoch(self) -> int | None:
107
+ if self.complete and self.lower_epoch == self.upper_epoch:
108
+ return self.lower_epoch
109
+ return None
110
+
111
+ def as_record(self) -> dict[str, Any]:
112
+ return {
113
+ "identity": self.identity,
114
+ "project_id": self.project_id,
115
+ "column": self.column,
116
+ "lower_epoch": self.lower_epoch,
117
+ "upper_epoch": self.upper_epoch,
118
+ "holds": list(self.holds),
119
+ "retired": list(self.retired),
120
+ "closed": self.closed,
121
+ }
122
+
123
+
124
+ def fence_state(metadata: FenceMetadata) -> FenceState:
125
+ owners: list[str] = []
126
+ minima: list[int] = []
127
+ maxima: list[int] = []
128
+ holds: list[str] = []
129
+ retired: list[str] = []
130
+ for name, raw in sorted(metadata.constraints.items()):
131
+ if not name.startswith(FENCE_PREFIX):
132
+ continue
133
+ suffix = name[len(FENCE_PREFIX) :]
134
+ predicate = _predicate(raw)
135
+ if re.fullmatch("owner_[0-9a-f]{32}", suffix):
136
+ owners.append(suffix[6:])
137
+ expected = "1"
138
+ elif re.fullmatch("retired_[0-9a-f]{32}", suffix):
139
+ retired.append(suffix[8:])
140
+ expected = "1"
141
+ elif suffix == "setup" or re.fullmatch("hold_[0-9a-f]{32}", suffix):
142
+ holds.append("setup" if suffix == "setup" else suffix[5:])
143
+ expected = "0"
144
+ elif re.fullmatch("(?:min|max)_[1-9][0-9]*", suffix):
145
+ value = int(suffix[4:])
146
+ check_epoch(value)
147
+ (minima if suffix.startswith("min_") else maxima).append(value)
148
+ expected = EPOCH_COLUMN + (">=" if suffix.startswith("min_") else "<=") + str(value)
149
+ else:
150
+ raise MigrationRefused("unrecognized constraint in the reserved write-fence namespace")
151
+ if predicate != expected:
152
+ raise MigrationRefused(f"write fence constraint {name} has an unexpected predicate")
153
+ if len(owners) > 1:
154
+ raise MigrationRefused("the table carries write fences from more than one project")
155
+ if not owners and (minima or maxima or holds or retired):
156
+ raise MigrationRefused("write fence constraints have no project owner")
157
+ return FenceState(
158
+ metadata.identity,
159
+ owners[0] if owners else None,
160
+ metadata.column,
161
+ tuple(sorted(minima)),
162
+ tuple(sorted(maxima)),
163
+ tuple(sorted(holds)),
164
+ tuple(sorted(retired)),
165
+ )
166
+
167
+
168
+ class WriteFence:
169
+ """A table's provisioning capability. Serialize executor commands per project state directory.
170
+
171
+ Writes carry a number from their own immutable placement. Replaying
172
+ a stale administrative command cannot lower the native minimum. A partial command can leave the
173
+ table closed, so an I/O error calls for inspection/resumption, not an assumption of rollback.
174
+ """
175
+
176
+ def __init__(self, backend: FenceBackend, table: str, *, project_id: str) -> None:
177
+ _identity(project_id, "project_id")
178
+ if not isinstance(table, str) or not table or "\0" in table:
179
+ raise MigrationRefused("write fence table must be a nonempty identifier")
180
+ if table in (DRAIN_TABLE, BACKFILL_TABLE, WATERMARK_TABLE):
181
+ raise MigrationRefused("write fences cannot take over an SDK metadata table")
182
+ self._backend = backend
183
+ self.table = table
184
+ self.project_id = project_id
185
+
186
+ def state(self) -> FenceState:
187
+ state = fence_state(self._backend.metadata(self.table))
188
+ if state.project_id is not None and state.project_id != self.project_id:
189
+ raise MigrationRefused("the write fence belongs to another project")
190
+ if state.column == "conflict":
191
+ raise MigrationRefused("the reserved write-epoch column has an incompatible definition")
192
+ return state
193
+
194
+ def _ready(self) -> FenceState:
195
+ state = self.state()
196
+ if not state.complete:
197
+ raise MigrationRefused("the write fence is not fully provisioned")
198
+ return state
199
+
200
+ def prepare(self, epoch: int) -> FenceState:
201
+ epoch = check_epoch(epoch)
202
+ state = self.state()
203
+ if state.complete and "setup" not in state.holds:
204
+ if state.epoch != epoch:
205
+ raise MigrationRefused("provisioning cannot change an existing write epoch")
206
+ return state
207
+ if state.project_id is None and state.column != "absent":
208
+ raise MigrationRefused("the reserved write-epoch column is not owned by this project")
209
+ if state.lower_epoch is not None and state.lower_epoch > epoch:
210
+ raise MigrationRefused("a write epoch cannot move backwards")
211
+ if state.project_id is None:
212
+ self._backend.add_constraint(self.table, FENCE_PREFIX + "owner_" + self.project_id, "1")
213
+ self._backend.add_constraint(self.table, SETUP, "0")
214
+ self._backend.add_column(self.table)
215
+ self._bounds(epoch)
216
+ self._backend.drain(self.table, project_id=self.project_id, hold="setup")
217
+ self._backend.drop_constraint(self.table, SETUP)
218
+ return self._ready()
219
+
220
+ def freeze(self, request_id: str) -> FenceState:
221
+ _identity(request_id, "request_id")
222
+ state = self._ready()
223
+ if request_id in state.retired:
224
+ raise MigrationRefused("a completed write barrier id cannot be reused")
225
+ self._backend.add_constraint(self.table, FENCE_PREFIX + "hold_" + request_id, "0")
226
+ # Always drain, even on a retry: existence of the constraint proves admission is closed,
227
+ # not that an INSERT which captured older metadata has finished.
228
+ self._backend.drain(self.table, project_id=self.project_id, hold=request_id)
229
+ return self._ready()
230
+
231
+ def resume(self, request_id: str) -> FenceState:
232
+ _identity(request_id, "request_id")
233
+ self._backend.restore(self.table, project_id=self.project_id, hold=request_id)
234
+ return self.freeze(request_id)
235
+
236
+ def resume_prepare(self, epoch: int) -> FenceState:
237
+ epoch = check_epoch(epoch)
238
+ self._backend.restore(self.table, project_id=self.project_id, hold="setup")
239
+ return self.prepare(epoch)
240
+
241
+ def advance(self, epoch: int) -> FenceState:
242
+ epoch = check_epoch(epoch)
243
+ state = self._ready()
244
+ if not state.holds:
245
+ raise MigrationRefused("changing a write epoch needs a named write barrier")
246
+ if state.lower_epoch is not None and epoch < state.lower_epoch:
247
+ raise MigrationRefused("a write epoch cannot move backwards")
248
+ self._bounds(epoch)
249
+ return self._ready()
250
+
251
+ def _bounds(self, epoch: int) -> None:
252
+ self._backend.add_constraint(
253
+ self.table, FENCE_PREFIX + "min_" + str(epoch), f"{EPOCH_COLUMN} >= {epoch}"
254
+ )
255
+ self._backend.add_constraint(
256
+ self.table, FENCE_PREFIX + "max_" + str(epoch), f"{EPOCH_COLUMN} <= {epoch}"
257
+ )
258
+ # Remove only bounds weaker/older than the one just installed, by their exact names. A
259
+ # stale caller must never remove a newer constraint discovered after its earlier check.
260
+ state = self.state()
261
+ if state.lower_epoch is not None and state.lower_epoch > epoch:
262
+ raise MigrationRefused(
263
+ "another executor installed a newer write epoch; table stays closed"
264
+ )
265
+ for old in state.maximums:
266
+ if old < epoch:
267
+ self._backend.drop_constraint(self.table, FENCE_PREFIX + "max_" + str(old))
268
+ for old in state.minimums:
269
+ if old < epoch:
270
+ self._backend.drop_constraint(self.table, FENCE_PREFIX + "min_" + str(old))
271
+
272
+ def release(self, request_id: str) -> FenceState:
273
+ _identity(request_id, "request_id")
274
+ state = self._ready()
275
+ if state.epoch is None:
276
+ raise MigrationRefused("cannot release a barrier with an incomplete epoch change")
277
+ if request_id not in state.holds:
278
+ return state
279
+ # Record completion before releasing admission. A delayed invocation must never recreate
280
+ # a completed hold, nor reuse its id for a later generation and inherit an old release.
281
+ self._backend.add_constraint(self.table, FENCE_PREFIX + "retired_" + request_id, "1")
282
+ self._backend.drop_constraint(self.table, FENCE_PREFIX + "hold_" + request_id)
283
+ return self._ready()
sde_demo/__init__.py ADDED
@@ -0,0 +1 @@
1
+ """Customer-side Weather starter; data and local credentials stay in this process."""
sde_demo/__main__.py ADDED
@@ -0,0 +1,183 @@
1
+ """Run the customer-side Weather starter; engine credentials and values stay local."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+ import os
8
+ import sys
9
+ from collections.abc import Sequence
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+ import sde
14
+ from sde._local_state import transaction
15
+
16
+ from .model import WORKLOADS, model
17
+ from .project import DemoRefused, config, connections, public_keys, read, setup, write
18
+ from .resources import ResourceRefused
19
+
20
+
21
+ def operator(root: Path, action: str, plan: Path | None) -> dict[str, Any]:
22
+ from sde.local_cutover import _local_status
23
+
24
+ from . import resources
25
+
26
+ with transaction(root):
27
+ settings, logical = config(root, ready=action != "status"), model()
28
+ keys = public_keys(settings["public_keys"])
29
+ if action == "status":
30
+ return _local_status(
31
+ root / "state", model=logical, project_id=settings["project_id"], public_key=keys
32
+ )
33
+ resources.verify(root)
34
+ with connections(root, settings) as (operators, runtime):
35
+ local = sde.LocalCutover(
36
+ root / "state",
37
+ model=logical,
38
+ project_id=settings["project_id"],
39
+ public_key=keys,
40
+ operators=operators,
41
+ runtime={name: [adapter] for name, adapter in runtime.items()},
42
+ )
43
+ if action == "resume":
44
+ return local.resume().as_record()
45
+ if action == "abandon":
46
+ return local.abandon().as_record()
47
+ if plan is None:
48
+ raise DemoRefused("Stage, index and execute require --plan from the controller.")
49
+ if action == "index":
50
+ return local.index(
51
+ sde.load_index_plan(
52
+ read(plan),
53
+ model=logical,
54
+ project_id=settings["project_id"],
55
+ public_key=keys,
56
+ )
57
+ ).as_record()
58
+ if action == "stage":
59
+ return local.stage(
60
+ sde.load_staging_plan(
61
+ read(plan),
62
+ model=logical,
63
+ project_id=settings["project_id"],
64
+ public_key=keys,
65
+ )
66
+ ).as_record()
67
+ return local.execute(
68
+ sde.load_cutover_plan(
69
+ read(plan), model=logical, project_id=settings["project_id"], public_key=keys
70
+ )
71
+ ).as_record()
72
+
73
+
74
+ def main(argv: Sequence[str] | None = None) -> int:
75
+ parser = argparse.ArgumentParser(description=__doc__)
76
+ parser.add_argument("--directory", type=Path)
77
+ commands = parser.add_subparsers(dest="command", required=True)
78
+ commands.add_parser("model", help="print the neutral Weather model for the controller")
79
+ start = commands.add_parser("setup", help="allocate loopback-only demo resources and enroll")
80
+ start.add_argument("--bootstrap", type=Path, required=True)
81
+ reset = commands.add_parser(
82
+ "reset", help="remove this allocation after stopping demo processes"
83
+ )
84
+ reset.add_argument("--confirm-allocation", required=True)
85
+ for command in (start, reset):
86
+ command.add_argument("--postgres-admin-env", default="SDE_POSTGRES_DSN")
87
+ command.add_argument("--clickhouse-admin-env", default="SDE_CLICKHOUSE_DSN")
88
+ commands.add_parser("doctor", help="check the local map, connections and runtime grants")
89
+ workload = commands.add_parser("run", help="run a bounded logical workload with runtime rights")
90
+ workload.add_argument("--iterations", type=int, default=10)
91
+ workload.add_argument("--batch-size", type=int, default=10)
92
+ workload.add_argument("--interval-ms", type=int, default=100)
93
+ workload.add_argument("--recovery-ms", type=int, default=10000)
94
+ workload.add_argument("--workload", choices=WORKLOADS, default="mixed")
95
+ verify = commands.add_parser(
96
+ "verify-runs", help="verify earlier completed Python/TypeScript runs"
97
+ )
98
+ verify.add_argument("--run-id", action="append", required=True)
99
+ query = commands.add_parser(
100
+ "query-count", help="execute the current approved Weather COUNT locally"
101
+ )
102
+ query.add_argument("--record", type=Path, required=True)
103
+ op = commands.add_parser(
104
+ "operator", help="use the existing local staging/index/cutover executor"
105
+ )
106
+ op.add_argument("action", choices=("status", "stage", "index", "execute", "resume", "abandon"))
107
+ op.add_argument("--plan", type=Path)
108
+ args = parser.parse_args(argv)
109
+ try:
110
+ if args.command == "model":
111
+ result = sde.neutral_declaration(model())
112
+ else:
113
+ if args.directory is None:
114
+ raise DemoRefused("Provide --directory for this customer-side demo allocation.")
115
+ root = args.directory.absolute()
116
+ if args.command in ("setup", "reset"):
117
+ admin = {
118
+ dialect: os.environ[variable]
119
+ for dialect, variable in (
120
+ ("postgres", args.postgres_admin_env),
121
+ ("clickhouse", args.clickhouse_admin_env),
122
+ )
123
+ if os.environ.get(variable)
124
+ }
125
+ if args.command == "setup":
126
+ result = setup(root, read(args.bootstrap), admin)
127
+ else:
128
+ from .resources import reset as reset_resources
129
+
130
+ with transaction(root):
131
+ allocation = read(root / "resources.json")["allocation_id"]
132
+ if args.confirm_allocation != allocation:
133
+ raise DemoRefused(
134
+ "Confirm the allocation_id from local resources.json."
135
+ )
136
+ write(root / "reset-request.json", {"allocation_id": allocation}, once=True)
137
+ result = reset_resources(root, admin)
138
+ elif args.command == "doctor":
139
+ from .diagnostics import doctor
140
+
141
+ result = doctor(root)
142
+ elif args.command == "operator":
143
+ result = operator(root, args.action, args.plan)
144
+ elif args.command == "verify-runs":
145
+ from .verification import verify_runs
146
+
147
+ result = verify_runs(root, args.run_id)
148
+ elif args.command == "query-count":
149
+ from .query_count import run_count_query
150
+
151
+ result = run_count_query(root, read(args.record))
152
+ else:
153
+ from .runtime import run
154
+
155
+ result = run(
156
+ root,
157
+ iterations=args.iterations,
158
+ batch_size=args.batch_size,
159
+ interval_ms=args.interval_ms,
160
+ recovery_ms=args.recovery_ms,
161
+ workload=args.workload,
162
+ )
163
+ print(json.dumps(result, sort_keys=True, allow_nan=False))
164
+ return 0
165
+ except (DemoRefused, ResourceRefused) as exc:
166
+ print(json.dumps({"error": "demo_refused", "message": str(exc)}), file=sys.stderr)
167
+ return 2
168
+ except Exception as exc:
169
+ print(
170
+ json.dumps(
171
+ {
172
+ "error": type(exc).__name__,
173
+ "message": "Operation incomplete. Inspect local state and the run report "
174
+ "before retrying; uncertain writes must not be replayed.",
175
+ }
176
+ ),
177
+ file=sys.stderr,
178
+ )
179
+ return 3
180
+
181
+
182
+ if __name__ == "__main__":
183
+ raise SystemExit(main())
@@ -0,0 +1,92 @@
1
+ """Local diagnostics using existing native identity and grant qualification."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import platform
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ import sde
10
+ from sde._local_state import transaction
11
+ from sde.engines._operator import NativeOperator
12
+
13
+ from . import resources
14
+ from .model import model
15
+ from .project import config, connections, public_keys, require_drivers
16
+
17
+
18
+ def doctor(root: Path) -> dict[str, Any]:
19
+ with transaction(root):
20
+ settings = config(root)
21
+ require_drivers(binding["dialect"] for binding in settings["engines"].values())
22
+ resources.verify(root)
23
+ return _diagnose(root, settings)
24
+
25
+
26
+ def _diagnose(root: Path, settings: dict[str, Any]) -> dict[str, Any]:
27
+ logical = model()
28
+ placement = sde.load_local_map(
29
+ root / "state",
30
+ model=logical,
31
+ project_id=settings["project_id"],
32
+ public_key=public_keys(settings["public_keys"]),
33
+ )
34
+ tables: dict[str, set[str]] = {name: {"sde_map_state"} for name in settings["engines"]}
35
+ for group in placement.groups.values():
36
+ for material in group.all():
37
+ tables[material.engine].update(material.layout.tables.values())
38
+ results = {}
39
+ with connections(root, settings) as (operators, runtime):
40
+ for name, operator in operators.items():
41
+ native = NativeOperator(operator, [runtime[name]])
42
+ native.qualify(sorted(tables[name]), sorted(tables[name]))
43
+ if operator.dialect == "postgres":
44
+ version = str(native.rows("SHOW server_version")[0][0])
45
+ tls = bool(
46
+ native.rows(
47
+ "SELECT ssl FROM pg_stat_ssl WHERE pid=pg_backend_pid()",
48
+ engine=runtime[name],
49
+ )[0][0]
50
+ )
51
+ else:
52
+ version = str(native.rows("SELECT version()")[0][0])
53
+ tls = runtime[name]._cx.url.startswith("https://")
54
+ results[name] = {
55
+ "dialect": operator.dialect,
56
+ "version": version,
57
+ "native_identity": "matched",
58
+ "runtime_privileges": "qualified",
59
+ "connection_tls": tls,
60
+ }
61
+ with sde.Session(
62
+ logical, placement, runtime, project_id=settings["project_id"]
63
+ ) as session:
64
+ storage = session.measure_storage()
65
+ # Whether each engine can give the window a group's size, by the class of the reason -
66
+ # never the size itself. An engine that holds no group's source has nothing to measure.
67
+ measured = {size.engine for size in storage.sizes}
68
+ for name, result in results.items():
69
+ sources = [
70
+ group for group, spot in placement.groups.items() if spot.source.engine == name
71
+ ]
72
+ reasons = sorted(
73
+ {storage.unavailable[group] for group in sources if group in storage.unavailable}
74
+ )
75
+ result["storage_telemetry"] = (
76
+ "no_source" if not sources
77
+ else "available" if name in measured and not reasons
78
+ else "unavailable: " + ", ".join(reasons)
79
+ )
80
+ return {
81
+ "protocol": 1,
82
+ "status": "ready",
83
+ "scope": "local-weather-demo",
84
+ "sdk_version": sde.__version__,
85
+ "python_version": platform.python_version(),
86
+ "project_id": settings["project_id"],
87
+ "model_version": logical.version,
88
+ "map_version": placement.map_version,
89
+ "signature": "verified",
90
+ "engines": results,
91
+ "production_qualification": "not_performed",
92
+ }
sde_demo/model.py ADDED
@@ -0,0 +1,75 @@
1
+ """The fixed, portable model used by the customer Weather starter."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ from datetime import UTC, datetime, timedelta
7
+ from decimal import Decimal
8
+ from typing import Any
9
+ from uuid import UUID
10
+
11
+ import sde
12
+ from sde.testing.loader import model_from_neutral
13
+
14
+ DECLARATION: dict[str, Any] = {
15
+ "entities": [
16
+ {
17
+ "name": "WeatherReading",
18
+ "fields": [
19
+ {"name": "at", "type": "timestamptz"},
20
+ {"name": "celsius", "type": "decimal(8,2)"},
21
+ {"name": "humidity", "type": "int64"},
22
+ {"name": "id", "type": "uuid"},
23
+ {"name": "station", "type": "string"},
24
+ ],
25
+ "key": ["station", "at"],
26
+ "residency": "EU",
27
+ }
28
+ ],
29
+ "relations": [],
30
+ "atomic": [],
31
+ "cost_ceiling": {"amount": "500.00", "currency": "EUR"},
32
+ }
33
+ BASE_TIME = datetime(2026, 1, 1, tzinfo=UTC)
34
+ STATION_TEMPLATE = "weather-{run_id}-{worker}"
35
+ SEED_TEMPLATE = "sde-weather-v1:{run_id}:{worker}:{sequence}"
36
+ CELSIUS_BASE_CENTS, CELSIUS_MODULUS = 1525, 1000
37
+ HUMIDITY_BASE, HUMIDITY_MODULUS = 30, 70
38
+ UUID_VERSION = 4
39
+ WORKLOADS = ("mixed", "point", "analytics", "fleet", "alerts")
40
+ """What a run can drive. The CLI offers and the runtime accepts this one tuple."""
41
+ GENERATOR_SPEC = {
42
+ "kind": "sde-weather-generator",
43
+ "version": 1,
44
+ "worker": 0,
45
+ "station_template": STATION_TEMPLATE,
46
+ "seed_template": SEED_TEMPLATE,
47
+ "base_time": BASE_TIME.isoformat(timespec="microseconds").replace("+00:00", "Z"),
48
+ "timestamp_step_microseconds": 1,
49
+ "celsius_base_cents": CELSIUS_BASE_CENTS,
50
+ "celsius_modulus": CELSIUS_MODULUS,
51
+ "humidity_base": HUMIDITY_BASE,
52
+ "humidity_modulus": HUMIDITY_MODULUS,
53
+ "id": "first 16 bytes of SHA256(UTF8(seed)), RFC4122 UUID version bits",
54
+ "uuid_version": UUID_VERSION,
55
+ }
56
+ GENERATOR_ID = "weather-v1:" + hashlib.sha256(sde.canonical_bytes(GENERATOR_SPEC)).hexdigest()
57
+
58
+
59
+ def model() -> sde.LogicalModel:
60
+ return model_from_neutral(DECLARATION)
61
+
62
+
63
+ def reading(run_id: str, worker: int, sequence: int) -> dict[str, Any]:
64
+ """Deterministic synthetic values; the run namespace prevents keys crossing runs."""
65
+ identity = hashlib.sha256(
66
+ SEED_TEMPLATE.format(run_id=run_id, worker=worker, sequence=sequence).encode()
67
+ ).digest()[:16]
68
+ cents = CELSIUS_BASE_CENTS + sequence % CELSIUS_MODULUS
69
+ return {
70
+ "station": STATION_TEMPLATE.format(run_id=run_id, worker=worker),
71
+ "at": BASE_TIME + timedelta(microseconds=sequence),
72
+ "celsius": Decimal(f"{cents // 100}.{cents % 100:02d}"),
73
+ "humidity": HUMIDITY_BASE + sequence % HUMIDITY_MODULUS,
74
+ "id": UUID(bytes=identity, version=UUID_VERSION),
75
+ }