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/logging.py ADDED
@@ -0,0 +1,143 @@
1
+ """Structured logging, with one rule: every event has a name from a closed vocabulary.
2
+
3
+ We never have access to a client's machine. When something goes wrong there, the log is the only
4
+ diagnostic we will ever see, so it has to be greppable and stable rather than prose that changes
5
+ with each refactor. Hence ``sde.``-prefixed event names, structured fields, and no interpolated
6
+ values in the event name itself.
7
+
8
+ No dependency on a logging framework, and no configuration required. The library emits through the
9
+ standard :mod:`logging` module under the ``sde`` logger and stays silent unless the application
10
+ configures a handler - a library that prints on import is a library people vendor and patch.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import logging
16
+ from typing import Any, Final
17
+
18
+ __all__ = ["EVENTS", "log", "logger"]
19
+
20
+ logger: Final = logging.getLogger("sde")
21
+
22
+ EVENTS: Final[frozenset[str]] = frozenset(
23
+ {
24
+ # Once per process, and the single most useful line here: it names the model_version every
25
+ # other artefact keys on. "Why was my map refused for model version X" is answerable from
26
+ # this line and unanswerable without it.
27
+ "sde.model.built",
28
+ # A map parsed and was accepted. `signed` says which kind of document it was and
29
+ # `forward_only` whether the rollback check applies - fields rather than two event names,
30
+ # so that the account-free mode emits nothing the account mode does not.
31
+ "sde.map.loaded",
32
+ # A map was refused, with the error class and the structural reason. The exception goes to
33
+ # the application; this goes to whoever is looking at why it will not start.
34
+ "sde.map.rejected",
35
+ # A signed map was accepted and is not older than any already applied here. Emitted once
36
+ # per session, so the fields are the ones an operator wants when a start is refused later.
37
+ "sde.map.forward_only",
38
+ # No engine in this map can keep the bookkeeping, so a map that goes backwards cannot be
39
+ # recognised. Not a failure - our own orderbook engine has a fixed schema and nowhere to
40
+ # put it - but it is the one state where a documented protection is genuinely absent.
41
+ "sde.map.rollback_unprotected",
42
+ "sde.route.resolved",
43
+ "sde.route.fallback",
44
+ "sde.schema.applied",
45
+ # extra_columns fires when a table the map describes has columns the map does not
46
+ # name. Allowed rather than refused - a client may have added one outside SDE and
47
+ # writes are unaffected - but logged, because the alternative to refusing is saying
48
+ # nothing, and a schema that has quietly diverged is worth one line.
49
+ "sde.schema.extra_columns",
50
+ # physical_mismatch fires when a session finds tables whose sort key, partition or
51
+ # indexes differ from the physical design the map declares. Reported, not refused: the
52
+ # rows are the same and a running application must not stop over performance.
53
+ "sde.schema.physical_mismatch",
54
+ "sde.write.failed",
55
+ # A dual-write fan-out did not reach a copy. Not an application failure: the row is in the
56
+ # source, which is authoritative, and VERIFY is the gate that refuses to switch reads while
57
+ # any divergence remains. This is the only place in the library that swallows a write error.
58
+ "sde.migration.divergence",
59
+ # One backfill chunk landed in a copy and the marker moved. Emitted per chunk rather than
60
+ # per run, because a backfill is the one operation here that can take hours and an operator
61
+ # watching it needs to see it move. The marker itself is a row count and never a key value:
62
+ # a log line is the last place a client's own data should turn up.
63
+ "sde.migration.backfill_progress",
64
+ # The orderbook engine only. A write there is invisible to a query until flush(), and
65
+ # flush() in local mode tears the engine down and reopens it - 3.4 ms measured. Reads flush
66
+ # lazily, so this line is where the cost shows up and how many rows it bought.
67
+ "sde.orderbook.flushed",
68
+ # A ClickHouse server whose analyzer does not answer EXPLAIN QUERY TREE, so the table
69
+ # names behind a plan come from EXPLAIN ESTIMATE instead. Not a failure - the plan is
70
+ # returned - but the ReplacingMergeTree finding is a property of a table, so an operator
71
+ # who was expecting one and did not get it should be able to see why.
72
+ "sde.explain.no_query_tree",
73
+ "sde.internal.error",
74
+ # Telemetry. `window_closed` fires when a period ends and its aggregate is buffered for
75
+ # the application to collect; it was called `window_sent` until somebody read the log to
76
+ # check whether this library phones home and found a line saying it had. Nothing here
77
+ # sends anything - there is no channel, no address and no dependency that could open one -
78
+ # so the old name described an event that cannot happen, and a closed vocabulary exists so
79
+ # that an alert built on a name keeps working, not so that a wrong name outlives the
80
+ # reading of it. Renamed while the cost of renaming is zero.
81
+ "sde.telemetry.window_closed",
82
+ # dropped fires when the buffer is full and the oldest window is discarded - telemetry is
83
+ # the thing that gets lost when we run out of room, never an operation.
84
+ "sde.telemetry.dropped",
85
+ # A storage sample that is not two non-negative integers with the index part inside the
86
+ # total - a caller feeding Recorder.record_storage by hand; dropped, never repaired.
87
+ "sde.telemetry.storage_rejected",
88
+ # A group whose size the engine's catalogue could not give - no catalogue in the adapter,
89
+ # a refused read (ClickHouse without the system.parts grant), a failed one or a missing
90
+ # table. Its size stays unknown in the window; the application is not told by an exception.
91
+ "sde.telemetry.storage_unavailable",
92
+ }
93
+ )
94
+
95
+
96
+ def log(event: str, /, **fields: Any) -> None:
97
+ """Emit one structured event.
98
+
99
+ Nothing here can raise. The handler belongs to the client's application: it might write to a
100
+ socket that just closed, or be a custom formatter with a bug in it. A library that fails a
101
+ request because its own log line could not be written has no business being in somebody else's
102
+ process.
103
+
104
+ An unknown event name used to raise, on the argument that it is our programming error and the
105
+ test suite is where it should surface. **Measured, it did not.** The names that go unnoticed
106
+ are the ones on rare paths, and a rare path is precisely where no test goes:
107
+ ``sde.explain.no_query_tree`` shipped absent from the vocabulary behind a ``pragma: no cover``,
108
+ and a client on a ClickHouse without the new analyzer got ``ValueError: ... is not a known
109
+ event. Add it to EVENTS ...`` - a note addressed to us, delivered to them, instead of the query
110
+ plan, on the one path written to degrade gracefully.
111
+
112
+ So the guard moved to where it can be total: a static test reads every ``log()`` call site in
113
+ the package and requires the vocabulary and the code to agree in both directions. That fails in
114
+ CI on every path rather than in production on one. Here an unknown name is counted through the
115
+ same registry as a broken handler and nothing is emitted - so the vocabulary stays closed, a
116
+ client can still see that it happened, and it costs them no call.
117
+ """
118
+ if event not in EVENTS:
119
+ _count("logging.unknown_event")
120
+ return
121
+ # Checked before anything is built. Routing logs on every operation and an unconfigured logger
122
+ # is
123
+ # the normal case in production, so the cost of a log line nobody consumes has to be one
124
+ # comparison rather than a dictionary construction.
125
+ if not logger.isEnabledFor(logging.INFO):
126
+ return
127
+ try:
128
+ logger.info(event, extra={"sde_event": event, "sde_fields": fields})
129
+ except BaseException as exc:
130
+ if isinstance(exc, (KeyboardInterrupt, SystemExit)):
131
+ raise
132
+ _count("logging.emit")
133
+
134
+
135
+ def _count(what: str) -> None:
136
+ """Record one of our own failures without going through :func:`~sde.internal.guard`.
137
+
138
+ guard() reports through log(), so calling it from here would be a loop. Same registry, by hand.
139
+ """
140
+ from .internal import _failures, _lock
141
+
142
+ with _lock:
143
+ _failures[what] = _failures.get(what, 0) + 1