smart-data-engine-sdk 0.1.0.dev0__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.
sde/__init__.py ADDED
@@ -0,0 +1,226 @@
1
+ """Smart Data Engine - client library.
2
+
3
+ You declare entities and relations. We decide which database engine each colocation group lives in,
4
+ what its physical layout is there, and when it should move - and your code never names a table or an
5
+ engine, which is exactly what lets us change both without touching it.
6
+
7
+ from datetime import datetime from decimal import Decimal from typing import Annotated from uuid
8
+ import UUID import sde
9
+
10
+ @sde.entity
11
+ class User:
12
+ id: UUID email: str
13
+
14
+ class Meta:
15
+ pii = ["email"]
16
+
17
+ @sde.entity
18
+ class Order:
19
+ id: UUID user: sde.Ref[User] total: Annotated[Decimal, sde.precision(12, 2)] created_at:
20
+ datetime
21
+
22
+ class Meta:
23
+ atomic_with = ["Payment"] residency = "EU"
24
+
25
+ Everything about storage is our decision. The four things you declare - atomicity, residency,
26
+ personal data and a cost ceiling - are the ones that cannot be read from traffic no matter how long
27
+ we watch it.
28
+
29
+ This library is Apache-2.0 and it works without an account: hand it a placement map you wrote
30
+ yourself and it will route, create schema and run, with no key and no network. That is a supported
31
+ mode, not a loophole.
32
+ """
33
+
34
+ from __future__ import annotations
35
+
36
+ from .canonical import CanonicalError, canonical_bytes, canonical_str, digest16
37
+ from .capabilities import members_of, satisfies
38
+ from .entity import Ref, clear_registry, entity, registry
39
+ from .errors import (
40
+ DeclarationError,
41
+ EngineError,
42
+ MapError,
43
+ MapRolledBack,
44
+ MigrationRefused,
45
+ ModelPlanningError,
46
+ SdeError,
47
+ )
48
+ from .explain import Cost, Explains, PlanFinding, QueryPlan, QueryPlanRefused, explain
49
+ from .groups import Group, colocation_groups, group_of
50
+ from .hashing import NameMap, hash_identifiers, load_or_create_salt
51
+ from .infer import InferredModel, Note, infer_model, infer_models
52
+ from .internal import internal_failures, reset_internal_failures
53
+ from .layout import (
54
+ DIALECTS,
55
+ FIXED_SCHEMA,
56
+ ORDERBOOK_KEY,
57
+ ORDERBOOK_SHAPE,
58
+ ORDERBOOK_TABLE,
59
+ DerivedLayout,
60
+ can_store,
61
+ default_layout,
62
+ denormalized_layout,
63
+ fixed_schema_mismatch,
64
+ group_columns,
65
+ snake_case,
66
+ stored_types,
67
+ )
68
+ from .migration import (
69
+ BACKFILL_TABLE,
70
+ CHUNK_ROWS,
71
+ DIALECT_PRECISION,
72
+ PRECISION_INDEPENDENT,
73
+ BackfillProgress,
74
+ Difference,
75
+ EntityProgress,
76
+ Migratable,
77
+ VerifyReport,
78
+ backfill,
79
+ precision_refusal,
80
+ verify,
81
+ )
82
+ from .model import CONTRACT, LogicalModel, build_model, neutral_declaration
83
+ from .placement import (
84
+ ALSO_WRITE_SINCE,
85
+ MAP_CONTRACT,
86
+ MAP_CONTRACT_FLOOR,
87
+ RESERVED_TABLES,
88
+ GroupPlacement,
89
+ Materialization,
90
+ PhysicalLayout,
91
+ PlacementMap,
92
+ load_map,
93
+ )
94
+ from .routing import Router, resolve
95
+ from .schema import CompatibilityViews, compatibility_views, schema_is_fixed, schema_statements
96
+ from .session import Engine, Session
97
+ from .shapes import SHAPE_KINDS, WRITE_KINDS, OperationShape, enumerate_shapes
98
+ from .telemetry import (
99
+ MEASURED_FIELDS,
100
+ CopyFreshness,
101
+ FanOutStats,
102
+ GroupFeatures,
103
+ Histogram,
104
+ Recorder,
105
+ ShapeStats,
106
+ Window,
107
+ has_time_dimension,
108
+ )
109
+ from .types import Float32, Int32, Json, Timestamp, precision
110
+ from .watermark import (
111
+ WATERMARK_TABLE,
112
+ Protection,
113
+ WatermarkCheck,
114
+ WatermarkStore,
115
+ enforce_forward_only,
116
+ )
117
+
118
+ __version__ = "0.1.0.dev0"
119
+
120
+ __all__ = [
121
+ "ALSO_WRITE_SINCE",
122
+ "BACKFILL_TABLE",
123
+ "CHUNK_ROWS",
124
+ "CONTRACT",
125
+ "DIALECTS",
126
+ "DIALECT_PRECISION",
127
+ "FIXED_SCHEMA",
128
+ "MAP_CONTRACT",
129
+ "MAP_CONTRACT_FLOOR",
130
+ "MEASURED_FIELDS",
131
+ "ORDERBOOK_KEY",
132
+ "ORDERBOOK_SHAPE",
133
+ "ORDERBOOK_TABLE",
134
+ "PRECISION_INDEPENDENT",
135
+ "RESERVED_TABLES",
136
+ "SHAPE_KINDS",
137
+ "WATERMARK_TABLE",
138
+ "WRITE_KINDS",
139
+ "BackfillProgress",
140
+ "CanonicalError",
141
+ "CompatibilityViews",
142
+ "CopyFreshness",
143
+ "Cost",
144
+ "DeclarationError",
145
+ "DerivedLayout",
146
+ "Difference",
147
+ "Engine",
148
+ "EngineError",
149
+ "EntityProgress",
150
+ "Explains",
151
+ "FanOutStats",
152
+ "Float32",
153
+ "Group",
154
+ "GroupFeatures",
155
+ "GroupPlacement",
156
+ "Histogram",
157
+ "InferredModel",
158
+ "Int32",
159
+ "Json",
160
+ "LogicalModel",
161
+ "MapError",
162
+ "MapRolledBack",
163
+ "Materialization",
164
+ "Migratable",
165
+ "MigrationRefused",
166
+ "ModelPlanningError",
167
+ "NameMap",
168
+ "Note",
169
+ "OperationShape",
170
+ "PhysicalLayout",
171
+ "PlacementMap",
172
+ "PlanFinding",
173
+ "Protection",
174
+ "QueryPlan",
175
+ "QueryPlanRefused",
176
+ "Recorder",
177
+ "Ref",
178
+ "Router",
179
+ "SdeError",
180
+ "Session",
181
+ "ShapeStats",
182
+ "Timestamp",
183
+ "VerifyReport",
184
+ "WatermarkCheck",
185
+ "WatermarkStore",
186
+ "Window",
187
+ "__version__",
188
+ "backfill",
189
+ "build_model",
190
+ "can_store",
191
+ "canonical_bytes",
192
+ "canonical_str",
193
+ "clear_registry",
194
+ "colocation_groups",
195
+ "compatibility_views",
196
+ "default_layout",
197
+ "denormalized_layout",
198
+ "digest16",
199
+ "enforce_forward_only",
200
+ "entity",
201
+ "enumerate_shapes",
202
+ "explain",
203
+ "fixed_schema_mismatch",
204
+ "group_columns",
205
+ "group_of",
206
+ "has_time_dimension",
207
+ "hash_identifiers",
208
+ "infer_model",
209
+ "infer_models",
210
+ "internal_failures",
211
+ "load_map",
212
+ "load_or_create_salt",
213
+ "members_of",
214
+ "neutral_declaration",
215
+ "precision",
216
+ "precision_refusal",
217
+ "registry",
218
+ "reset_internal_failures",
219
+ "resolve",
220
+ "satisfies",
221
+ "schema_is_fixed",
222
+ "schema_statements",
223
+ "snake_case",
224
+ "stored_types",
225
+ "verify",
226
+ ]
sde/canonical.py ADDED
@@ -0,0 +1,141 @@
1
+ """Canonical encoding: the one place where the cross-language contract lives.
2
+
3
+ Every SDE library has to produce byte-identical output from this. A definition amounting to
4
+ "whatever the Python implementation does" is not a definition, so the rules are spelled out here and
5
+ in ``docs/format-contract.md``, and the conformance vectors pin them.
6
+
7
+ The rules, in the order they are applied:
8
+
9
+ 1. UTF-8, no byte order mark.
10
+ 2. Object keys are NFC-normalised, then sorted by Unicode code point. Normalise first: sorting first
11
+ would order ``é`` (U+00E9) and ``e`` + U+0301 differently while both normalise to the same key.
12
+ 3. No insignificant whitespace. ``{"a":1,"b":[2,3]}`` and nothing else.
13
+ 4. Every string is NFC-normalised.
14
+ 5. Escaping is minimal and exhaustive: only ``"``, ``\\`` and C0 control characters are escaped,
15
+ controls using the short forms where JSON defines them and ``\\u00XX`` otherwise. Everything else
16
+ is emitted as raw UTF-8. This matters because JSON writers disagree: some escape all non-ASCII,
17
+ some escape U+2028, some escape forward slashes. Any of those would change the hash.
18
+ 6. Floating point values are rejected outright. Their textual form differs across languages and the
19
+ difference is unfixable after the fact. Note the distinction that cost a spec revision: a *field*
20
+ may have type ``float64``; the IR records the *name* of that type, which is a string. There is no
21
+ float literal anywhere in the encoding.
22
+ 7. Integers are emitted as their shortest decimal form, no leading ``+``, no exponent.
23
+
24
+ Ordering of arrays is not this module's business. Where order carries no meaning the caller sorts;
25
+ where it does, the caller records an explicit index in each element rather than relying on position.
26
+ """
27
+
28
+ from __future__ import annotations
29
+
30
+ import hashlib
31
+ import unicodedata
32
+ from typing import Final
33
+
34
+ __all__ = ["CanonicalError", "canonical_bytes", "canonical_str", "digest16"]
35
+
36
+ # Short escape forms JSON defines. Everything else below 0x20 gets \u00XX.
37
+ _SHORT_ESCAPES: Final[dict[int, str]] = {
38
+ 0x08: "\\b",
39
+ 0x09: "\\t",
40
+ 0x0A: "\\n",
41
+ 0x0C: "\\f",
42
+ 0x0D: "\\r",
43
+ 0x22: '\\"',
44
+ 0x5C: "\\\\",
45
+ }
46
+
47
+
48
+ class CanonicalError(ValueError):
49
+ """A value cannot be encoded canonically.
50
+
51
+ Raised rather than coerced on purpose: silently accepting a float, a NaN or a non-string key
52
+ would produce bytes that another implementation cannot reproduce, and the failure would surface
53
+ much later as two versions of the same model.
54
+ """
55
+
56
+
57
+ def _escape(text: str) -> str:
58
+ normalised = unicodedata.normalize("NFC", text)
59
+ out: list[str] = ['"']
60
+ for char in normalised:
61
+ code = ord(char)
62
+ short = _SHORT_ESCAPES.get(code)
63
+ if short is not None:
64
+ out.append(short)
65
+ elif code < 0x20:
66
+ out.append(f"\\u{code:04x}")
67
+ else:
68
+ out.append(char)
69
+ out.append('"')
70
+ return "".join(out)
71
+
72
+
73
+ def _encode(value: object, path: str) -> str:
74
+ # bool before int: bool is a subclass of int in Python and would otherwise encode as 1/0.
75
+ if value is True:
76
+ return "true"
77
+ if value is False:
78
+ return "false"
79
+ if value is None:
80
+ return "null"
81
+ if isinstance(value, str):
82
+ return _escape(value)
83
+ if isinstance(value, int):
84
+ return str(value)
85
+ if isinstance(value, float):
86
+ raise CanonicalError(
87
+ f"float at {path}: floating point is not representable in canonical form, "
88
+ "because its textual form differs between languages. Use an integer, or a decimal "
89
+ "string, or the name of a float type if you meant to describe a type."
90
+ )
91
+ if isinstance(value, (list, tuple)):
92
+ items = [_encode(item, f"{path}[{i}]") for i, item in enumerate(value)]
93
+ return "[" + ",".join(items) + "]"
94
+ if isinstance(value, dict):
95
+ pairs: list[tuple[str, object]] = []
96
+ for key, item in value.items():
97
+ if not isinstance(key, str):
98
+ raise CanonicalError(
99
+ f"non-string key {key!r} at {path}: object keys must be strings, because "
100
+ "key ordering is defined over Unicode code points"
101
+ )
102
+ pairs.append((unicodedata.normalize("NFC", key), item))
103
+ # Sort after normalising, on the normalised key.
104
+ pairs.sort(key=lambda pair: pair[0])
105
+ seen: set[str] = set()
106
+ parts: list[str] = []
107
+ for key, item in pairs:
108
+ if key in seen:
109
+ raise CanonicalError(
110
+ f"duplicate key {key!r} at {path} after NFC normalisation: two keys that "
111
+ "differ "
112
+ "only in Unicode composition are the same key here"
113
+ )
114
+ seen.add(key)
115
+ parts.append(_escape(key) + ":" + _encode(item, f"{path}.{key}"))
116
+ return "{" + ",".join(parts) + "}"
117
+ raise CanonicalError(
118
+ f"{type(value).__name__} at {path} has no canonical form. The canonical encoding accepts "
119
+ "only null, bool, int, str, list and dict; anything richer has to be reduced to those by "
120
+ "the caller, so that the reduction is visible and testable."
121
+ )
122
+
123
+
124
+ def canonical_str(value: object) -> str:
125
+ """Canonical form as text. Prefer :func:`canonical_bytes` for hashing."""
126
+ return _encode(value, "$")
127
+
128
+
129
+ def canonical_bytes(value: object) -> bytes:
130
+ """Canonical form as UTF-8 bytes. This is what gets hashed and what vectors compare."""
131
+ return canonical_str(value).encode("utf-8")
132
+
133
+
134
+ def digest16(value: object) -> str:
135
+ """The identifier form used for ``model_version`` and ``shape.id``.
136
+
137
+ Lowercase hex, first 8 bytes of SHA-256 over the canonical bytes. Sixteen characters is short
138
+ enough to appear in logs and error messages without wrapping, and 64 bits of collision
139
+ resistance is ample for the number of models and shapes one application declares.
140
+ """
141
+ return hashlib.sha256(canonical_bytes(value)).hexdigest()[:16]
sde/capabilities.py ADDED
@@ -0,0 +1,62 @@
1
+ """Asking an engine adapter whether it will answer a call, which is not the same as its type.
2
+
3
+ Two optional protocols decide whether an engine takes part in something: :class:`
4
+ sde.watermark.WatermarkStore` for the forward-only map check, and :class:`sde.migration.Migratable`
5
+ for a migration. Both were originally asked with ``isinstance(engine, Protocol)``, which is the
6
+ obvious spelling and the wrong question.
7
+
8
+ A ``runtime_checkable`` protocol resolves its members with ``hasattr`` up to Python 3.11 and with
9
+ :func:`inspect.getattr_static` from 3.12 onwards - and the second deliberately ignores
10
+ ``__getattr__``. So an object that forwards to a wrapped adapter answers ``hasattr`` for every
11
+ member of the protocol, passes ``isinstance`` on 3.11, and fails it on 3.12. This library supports
12
+ and tests all three, which makes that **the same client, the same wrapper, and a different answer
13
+ per interpreter**.
14
+
15
+ Wrapping an engine adapter is an ordinary thing for a client to do - metrics, logging, a retry, a
16
+ connection pool - and the consequence was two wrong diagnoses shipped as helpful messages: "this
17
+ engine has nowhere to keep the bookkeeping, so you have no rollback protection", and "this engine
18
+ cannot take part in a migration". Both named the client's *engine* for a property of their own
19
+ wrapper, and the second refuses a migration outright.
20
+
21
+ Found from a test, not from review: a two-line proxy over the real ClickHouse adapter, written to
22
+ make one write fail, was refused as an engine with no row-level operations. The version dependence
23
+ came from CI on 3.11, which is the part no single machine would have shown.
24
+
25
+ So the question asked here is the one that matters - **will this object respond to these calls** -
26
+ and it is asked with ordinary attribute access, which honours every way Python has of providing an
27
+ attribute.
28
+ """
29
+
30
+ from __future__ import annotations
31
+
32
+ from typing import Any
33
+
34
+ __all__ = ["members_of", "satisfies"]
35
+
36
+
37
+ def members_of(protocol: type) -> tuple[str, ...]:
38
+ """The public members a protocol names: its methods and its annotated data attributes.
39
+
40
+ Both halves are needed and neither is enough. Methods have class attributes, so ``dir`` finds
41
+ them; an annotated attribute with no value - ``dialect: str`` - exists only in
42
+ ``__annotations__``. Assembled from two public sources rather than from
43
+ ``__protocol_attrs__``, which is an implementation detail that did not exist in every version
44
+ this library supports.
45
+ """
46
+ named = {name for name in dir(protocol) if not name.startswith("_")}
47
+ named.update(
48
+ name
49
+ for name in getattr(protocol, "__annotations__", {})
50
+ if not name.startswith("_")
51
+ )
52
+ return tuple(sorted(named))
53
+
54
+
55
+ def satisfies(obj: Any, protocol: type) -> bool:
56
+ """Whether every member the protocol names can be reached on this object.
57
+
58
+ Presence rather than callability, deliberately. A member that exists and is not callable fails
59
+ at the call with a message naming it, which is a better failure than a capability check that
60
+ quietly answers "no" and sends the reader to look at their engine.
61
+ """
62
+ return all(hasattr(obj, name) for name in members_of(protocol))
File without changes