weaverstack 0.1.1__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 (127) hide show
  1. weaver/__init__.py +59 -0
  2. weaver/build_bundle/__init__.py +109 -0
  3. weaver/build_bundle/aliases.py +325 -0
  4. weaver/build_bundle/bundle.py +359 -0
  5. weaver/build_bundle/catalogue_actions.py +275 -0
  6. weaver/build_bundle/changes.py +186 -0
  7. weaver/build_bundle/endpoints.py +83 -0
  8. weaver/build_bundle/executors/__init__.py +69 -0
  9. weaver/build_bundle/executors/alias.py +202 -0
  10. weaver/build_bundle/executors/base.py +132 -0
  11. weaver/build_bundle/executors/folder.py +71 -0
  12. weaver/build_bundle/executors/load_file.py +205 -0
  13. weaver/build_bundle/executors/spark_case.py +26 -0
  14. weaver/build_bundle/executors/spark_schema.py +60 -0
  15. weaver/build_bundle/executors/spark_sql.py +59 -0
  16. weaver/build_bundle/executors/spark_sql_batch.py +57 -0
  17. weaver/build_bundle/executors/spark_table.py +213 -0
  18. weaver/build_bundle/executors/sql_endpoint_refresh.py +34 -0
  19. weaver/build_bundle/executors/tsql.py +81 -0
  20. weaver/build_bundle/incremental.py +288 -0
  21. weaver/build_bundle/installer.py +384 -0
  22. weaver/build_bundle/models.py +288 -0
  23. weaver/build_bundle/payloads.py +34 -0
  24. weaver/build_bundle/physical.py +625 -0
  25. weaver/build_bundle/planner.py +389 -0
  26. weaver/build_bundle/prune.py +620 -0
  27. weaver/build_bundle/report.py +108 -0
  28. weaver/build_bundle/stages.py +196 -0
  29. weaver/build_bundle/targets.py +272 -0
  30. weaver/build_bundle/workflow.py +585 -0
  31. weaver/catalogue/__init__.py +73 -0
  32. weaver/catalogue/builtin.py +238 -0
  33. weaver/catalogue/claims.py +121 -0
  34. weaver/catalogue/projection.py +437 -0
  35. weaver/catalogue/reader.py +152 -0
  36. weaver/catalogue/reconcile.py +231 -0
  37. weaver/catalogue/render.py +410 -0
  38. weaver/catalogue/state.py +660 -0
  39. weaver/catalogue/tables.py +648 -0
  40. weaver/config.py +178 -0
  41. weaver/declaration/__init__.py +171 -0
  42. weaver/declaration/columns.py +223 -0
  43. weaver/declaration/ddl.py +266 -0
  44. weaver/declaration/dependencies.py +544 -0
  45. weaver/declaration/graph.py +240 -0
  46. weaver/declaration/item_dependencies.py +292 -0
  47. weaver/declaration/load.py +191 -0
  48. weaver/declaration/metadata.py +1405 -0
  49. weaver/declaration/model.py +448 -0
  50. weaver/declaration/references.py +294 -0
  51. weaver/declaration/repository.py +959 -0
  52. weaver/declaration/schemas.py +135 -0
  53. weaver/declaration/source.py +674 -0
  54. weaver/declaration/spark_load.py +759 -0
  55. weaver/declaration/sql_shaping.py +591 -0
  56. weaver/declaration/templates/ddl/declared_create_table.sql +64 -0
  57. weaver/declaration/templates/ddl/infer_create_table.sql +97 -0
  58. weaver/declaration/templates/ddl/metadata_column_validation.sql +30 -0
  59. weaver/declaration/templates/load/column_metadata.sql +40 -0
  60. weaver/declaration/templates/load/full_replace_body.sql +21 -0
  61. weaver/declaration/templates/load/install_load_procedure.sql +27 -0
  62. weaver/declaration/templates/load/load_procedure.sql +48 -0
  63. weaver/declaration/templates/load/primary_key_body.sql +113 -0
  64. weaver/declaration/tsql_ddl.py +468 -0
  65. weaver/declaration/tsql_load.py +417 -0
  66. weaver/declaration/warehouse_type_mapping.yml +93 -0
  67. weaver/diagnostics.py +247 -0
  68. weaver/errors.py +61 -0
  69. weaver/etl.py +469 -0
  70. weaver/fabric/__init__.py +107 -0
  71. weaver/fabric/auth.py +137 -0
  72. weaver/fabric/capacity.py +143 -0
  73. weaver/fabric/client.py +147 -0
  74. weaver/fabric/environment.py +460 -0
  75. weaver/fabric/livy.py +478 -0
  76. weaver/fabric/notebooks.py +201 -0
  77. weaver/fabric/onelake.py +263 -0
  78. weaver/fabric/resolution.py +344 -0
  79. weaver/fabric/resources.py +245 -0
  80. weaver/fabric/session.py +148 -0
  81. weaver/fabric/shortcuts.py +120 -0
  82. weaver/fabric/sql.py +118 -0
  83. weaver/fabric/store.py +198 -0
  84. weaver/initialise.py +209 -0
  85. weaver/lakehouse.py +386 -0
  86. weaver/load.py +474 -0
  87. weaver/load_execution.py +483 -0
  88. weaver/load_plan.py +912 -0
  89. weaver/load_report.py +330 -0
  90. weaver/load_resolution.py +386 -0
  91. weaver/locations.py +164 -0
  92. weaver/objects.py +392 -0
  93. weaver/operations.py +757 -0
  94. weaver/physical_wipe.py +369 -0
  95. weaver/push.py +76 -0
  96. weaver/resolution.py +292 -0
  97. weaver/runtime/__init__.py +30 -0
  98. weaver/runtime/folder_load.py +402 -0
  99. weaver/runtime/load_contract.py +245 -0
  100. weaver/runtime/load_result.py +104 -0
  101. weaver/runtime/spark_load.py +152 -0
  102. weaver/runtime/table_load.py +497 -0
  103. weaver/spark/__init__.py +49 -0
  104. weaver/spark/catalogue.py +245 -0
  105. weaver/spark/destination.py +195 -0
  106. weaver/spark/session.py +84 -0
  107. weaver/spark/tokens.py +138 -0
  108. weaver/sql/__init__.py +40 -0
  109. weaver/sql/authentication.py +38 -0
  110. weaver/sql/connection.py +90 -0
  111. weaver/sql/errors.py +25 -0
  112. weaver/sql/execution.py +123 -0
  113. weaver/sql/pool.py +174 -0
  114. weaver/sql/wipe.py +156 -0
  115. weaver/store.py +209 -0
  116. weaver/targets.py +257 -0
  117. weaver/task_logging.py +215 -0
  118. weaver/unbind.py +74 -0
  119. weaver/workspaces.py +175 -0
  120. weaver_cli/__init__.py +12 -0
  121. weaver_cli/__main__.py +7 -0
  122. weaver_cli/main.py +626 -0
  123. weaverstack-0.1.1.dist-info/METADATA +113 -0
  124. weaverstack-0.1.1.dist-info/RECORD +127 -0
  125. weaverstack-0.1.1.dist-info/WHEEL +4 -0
  126. weaverstack-0.1.1.dist-info/entry_points.txt +2 -0
  127. weaverstack-0.1.1.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,483 @@
1
+ """Running a resolved load plan, one node at a time, in topological order.
2
+
3
+ Sequential by design for this phase, and the design does not prevent parallelism
4
+ later: the executor asks the graph what is ready and runs it, so the only thing
5
+ that would change is how many it runs at once. No concurrency abstraction is
6
+ introduced now, because an unused one is a guess about the shape of a problem
7
+ nobody has met yet.
8
+
9
+ **The dispatcher dispatches.** Merge semantics, identity handling, rejection
10
+ policy, folder replacement and source comparison all live in the primitives, each
11
+ of which is runnable on its own and tested on its own. What happens here is the
12
+ translation between one resolved node and one normalised result — and the two
13
+ places that translation can go wrong are the two this module is about: reading a
14
+ transport's answer, and surviving a transport that does not answer at all.
15
+
16
+ **Two levels of fault tolerance, and they are not the same thing.** The requested
17
+ value is passed *into* the primitive, which is what governs its own rejection
18
+ behaviour. Around that, the whole dispatch boundary is wrapped, because the
19
+ failures orchestration must survive are the ones no primitive normalises: a
20
+ module that will not import, a Warehouse that will not connect, a result that is
21
+ not a result. Both feed the same message stream.
22
+
23
+ **Failure propagation is by graph, never by position.** A node whose upstream
24
+ failed is ``blocked`` whatever ``fault_tolerant`` says — tolerance decides
25
+ whether *independent* branches continue, and never whether a node may run on a
26
+ dependency that did not. Nothing here can execute a node whose upstream failed,
27
+ because the check is against the recorded status of its upstream nodes rather
28
+ than against a flag.
29
+ """
30
+
31
+ from __future__ import annotations
32
+
33
+ from datetime import datetime, timezone
34
+ from typing import Callable
35
+
36
+ from .errors import LoadError
37
+ from .etl import LOAD_ROOT, load_procedure_name
38
+ from .load_plan import (
39
+ ENDPOINT_REFRESH,
40
+ PYTHON_FOLDER,
41
+ PYTHON_TABLE,
42
+ SPARK_SQL_FILE,
43
+ WAREHOUSE_PROCEDURE,
44
+ )
45
+ from .load_report import (
46
+ BLOCKED,
47
+ DEPENDENCY_BLOCKED,
48
+ DISPATCH_EXCEPTION,
49
+ ENDPOINT_REFRESH_FAILURE,
50
+ FAILED,
51
+ PENDING,
52
+ PRIMITIVE_FAILURE,
53
+ PRIMITIVE_REJECTS,
54
+ RESULT_CONTRACT_INVALID,
55
+ SKIPPED,
56
+ SUCCEEDED,
57
+ SUCCEEDED_WITH_REJECTS,
58
+ LoadMessage,
59
+ LoadNodeReport,
60
+ LoadResult,
61
+ error,
62
+ info,
63
+ warning,
64
+ )
65
+ from .load_resolution import LoadEnvironment, ResolvedLoadNode, ResolvedLoadPlan
66
+ from .locations import Location
67
+ from .targets import ItemRef
68
+
69
+ #: Statuses an upstream node may have and still let its dependants run. Rejects
70
+ #: are reported failure of the *rows*, not of the step: the valid work completed
71
+ #: and what it wrote is there to be read.
72
+ _CLEARS_DOWNSTREAM = frozenset({SUCCEEDED, SUCCEEDED_WITH_REJECTS, SKIPPED})
73
+
74
+ #: Statuses that propagate. Deliberately narrower than "not cleared": a node that
75
+ #: never started because fail-fast stopped the run has not *failed*, so its
76
+ #: dependants are unstarted too. Marking them blocked would report a failure that
77
+ #: nothing observed and hide which node actually broke.
78
+ _PROPAGATES = frozenset({FAILED, BLOCKED})
79
+
80
+
81
+ def execute_load_plan(
82
+ plan: ResolvedLoadPlan,
83
+ *,
84
+ fault_tolerant: bool = False,
85
+ environment: LoadEnvironment,
86
+ dispatch: Callable[..., LoadResult] | None = None,
87
+ on_step: Callable[[LoadNodeReport], None] | None = None,
88
+ ) -> tuple[LoadNodeReport, ...]:
89
+ """Execute a resolved plan sequentially and report every planned node.
90
+
91
+ ``dispatch`` is injectable so orchestration can be proven over prepared nodes
92
+ and fake results — these are orchestration claims, not primitive ones, and a
93
+ test that had to stand up four engines to assert an ordering would be
94
+ asserting the engines.
95
+
96
+ ``on_step`` receives each executed step's report as it completes, which is
97
+ how task evidence reaches storage without this module knowing what a file is.
98
+ """
99
+
100
+ dispatch = dispatch or dispatch_load_node
101
+ statuses: dict[str, str] = {node.node_id: PENDING for node in plan.nodes}
102
+ reports: dict[str, LoadNodeReport] = {}
103
+ stopped = False
104
+
105
+ for resolved in plan.order:
106
+ node = resolved.node
107
+ blocking = _blocking(plan, node.node_id, statuses)
108
+ if blocking:
109
+ reports[node.node_id] = _blocked_report(resolved, blocking)
110
+ statuses[node.node_id] = BLOCKED
111
+ continue
112
+ if stopped:
113
+ # Fail-fast: nothing new is scheduled. This node's own dependencies
114
+ # were fine, so it is not blocked — it simply never started, and
115
+ # saying so is more useful than inventing a failure for it.
116
+ reports[node.node_id] = _pending_report(resolved)
117
+ continue
118
+ if not resolved.valid:
119
+ reports[node.node_id] = _invalid_report(resolved)
120
+ statuses[node.node_id] = FAILED
121
+ if not fault_tolerant:
122
+ stopped = True
123
+ continue
124
+ if resolved.unsupported:
125
+ reports[node.node_id] = _skipped_report(resolved)
126
+ statuses[node.node_id] = SKIPPED
127
+ if on_step is not None:
128
+ on_step(reports[node.node_id])
129
+ continue
130
+
131
+ started = _now()
132
+ result, messages = _guarded(
133
+ dispatch,
134
+ resolved,
135
+ fault_tolerant=fault_tolerant,
136
+ environment=environment,
137
+ )
138
+ status = _status_for(result)
139
+ report = LoadNodeReport(
140
+ node_id=node.node_id,
141
+ logical_id=str(node.logical_id) if node.logical_id else None,
142
+ physical_target=str(node.physical_target),
143
+ primitive_kind=node.primitive_kind,
144
+ dispatch_location=resolved.dispatch_location,
145
+ status=status,
146
+ executed=True,
147
+ messages=messages,
148
+ result=result,
149
+ started_at=started,
150
+ finished_at=_now(),
151
+ )
152
+ reports[node.node_id] = report
153
+ statuses[node.node_id] = status
154
+ if on_step is not None:
155
+ on_step(report)
156
+ if status == FAILED and not fault_tolerant:
157
+ stopped = True
158
+
159
+ return tuple(reports[node.node.node_id] for node in plan.order)
160
+
161
+
162
+ def _blocking(plan: ResolvedLoadPlan, node_id: str, statuses) -> tuple[str, ...]:
163
+ return tuple(
164
+ sorted(
165
+ upstream
166
+ for upstream in plan.dag.upstream(node_id)
167
+ if statuses.get(upstream) in _PROPAGATES
168
+ )
169
+ )
170
+
171
+
172
+ def _status_for(result: LoadResult) -> str:
173
+ if result.succeeded:
174
+ return SUCCEEDED
175
+ # A primitive that refused rows and was asked to tolerate them wrote the
176
+ # valid ones and reported the refusal. That is not a failed step; a step that
177
+ # failed without refusing anything is.
178
+ return SUCCEEDED_WITH_REJECTS if result.rows_rejected else FAILED
179
+
180
+
181
+ def _guarded(
182
+ dispatch, resolved: ResolvedLoadNode, *, fault_tolerant: bool, environment
183
+ ) -> tuple[LoadResult, tuple[LoadMessage, ...]]:
184
+ """Dispatch one node, converting anything it throws into a failed result.
185
+
186
+ The orchestrator's own fault tolerance, and it is unconditional: an
187
+ unexpected exception becomes data whatever ``fault_tolerant`` says, because
188
+ the run has to record what happened before it decides what to do about it.
189
+ """
190
+
191
+ node = resolved.node
192
+ try:
193
+ result = dispatch(
194
+ resolved, fault_tolerant=fault_tolerant, environment=environment
195
+ )
196
+ except LoadError as exc:
197
+ carried = getattr(exc, "result", None)
198
+ result = (
199
+ carried
200
+ if isinstance(carried, LoadResult)
201
+ else LoadResult.failure(str(exc))
202
+ )
203
+ return result, (
204
+ error(
205
+ _failure_code(node.primitive_kind),
206
+ f"{node.node_id} failed: {exc}",
207
+ source=node.primitive_kind,
208
+ ),
209
+ )
210
+ except Exception as exc: # noqa: BLE001 - the boundary exists to catch these
211
+ return LoadResult.failure(f"{type(exc).__name__}: {exc}"), (
212
+ error(
213
+ DISPATCH_EXCEPTION,
214
+ f"{node.node_id} raised {type(exc).__name__}: {exc}",
215
+ source="load_execution",
216
+ ),
217
+ )
218
+ if not isinstance(result, LoadResult):
219
+ return LoadResult.failure(
220
+ f"the primitive returned {type(result).__name__}, not a load result"
221
+ ), (
222
+ error(
223
+ RESULT_CONTRACT_INVALID,
224
+ f"{node.node_id} returned {type(result).__name__} rather than a "
225
+ "load result",
226
+ source=node.primitive_kind,
227
+ ),
228
+ )
229
+ return result, _result_messages(resolved, result)
230
+
231
+
232
+ def _failure_code(primitive_kind: str) -> str:
233
+ return (
234
+ ENDPOINT_REFRESH_FAILURE
235
+ if primitive_kind == ENDPOINT_REFRESH
236
+ else PRIMITIVE_FAILURE
237
+ )
238
+
239
+
240
+ def _result_messages(
241
+ resolved: ResolvedLoadNode, result: LoadResult
242
+ ) -> tuple[LoadMessage, ...]:
243
+ node = resolved.node
244
+ if result.succeeded:
245
+ return ()
246
+ if result.rows_rejected:
247
+ return (
248
+ warning(
249
+ PRIMITIVE_REJECTS,
250
+ f"{node.node_id} rejected {result.rows_rejected} row(s): "
251
+ f"{result.error_message}",
252
+ source=node.primitive_kind,
253
+ ),
254
+ )
255
+ return (
256
+ error(
257
+ _failure_code(node.primitive_kind),
258
+ f"{node.node_id} reported failure: {result.error_message}",
259
+ source=node.primitive_kind,
260
+ ),
261
+ )
262
+
263
+
264
+ def _blocked_report(resolved: ResolvedLoadNode, blocking) -> LoadNodeReport:
265
+ return _report(
266
+ resolved,
267
+ BLOCKED,
268
+ (
269
+ error(
270
+ DEPENDENCY_BLOCKED,
271
+ f"{resolved.node_id} did not run: "
272
+ + ", ".join(blocking)
273
+ + " did not complete successfully",
274
+ source="load_execution",
275
+ ),
276
+ ),
277
+ )
278
+
279
+
280
+ def _invalid_report(resolved: ResolvedLoadNode) -> LoadNodeReport:
281
+ return _report(resolved, FAILED, resolved.validation_messages)
282
+
283
+
284
+ def _skipped_report(resolved: ResolvedLoadNode) -> LoadNodeReport:
285
+ return _report(resolved, SKIPPED, resolved.validation_messages)
286
+
287
+
288
+ def _pending_report(resolved: ResolvedLoadNode) -> LoadNodeReport:
289
+ return _report(
290
+ resolved,
291
+ PENDING,
292
+ (
293
+ info(
294
+ DEPENDENCY_BLOCKED,
295
+ f"{resolved.node_id} was not scheduled: an earlier node failed "
296
+ "and this run is not fault tolerant",
297
+ source="load_execution",
298
+ ),
299
+ ),
300
+ )
301
+
302
+
303
+ def _report(resolved: ResolvedLoadNode, status: str, messages) -> LoadNodeReport:
304
+ node = resolved.node
305
+ return LoadNodeReport(
306
+ node_id=node.node_id,
307
+ logical_id=str(node.logical_id) if node.logical_id else None,
308
+ physical_target=str(node.physical_target),
309
+ primitive_kind=node.primitive_kind,
310
+ dispatch_location=resolved.dispatch_location,
311
+ status=status,
312
+ executed=False,
313
+ messages=tuple(messages),
314
+ )
315
+
316
+
317
+ def _now() -> str:
318
+ return datetime.now(timezone.utc).isoformat()
319
+
320
+
321
+ # --- the dispatch boundary ----------------------------------------------------
322
+
323
+
324
+ def dispatch_load_node(
325
+ resolved: ResolvedLoadNode,
326
+ *,
327
+ fault_tolerant: bool = False,
328
+ environment: LoadEnvironment,
329
+ ) -> LoadResult:
330
+ """Run one resolved node's installed primitive and return what it reported."""
331
+
332
+ kind = resolved.node.primitive_kind
333
+ if kind == WAREHOUSE_PROCEDURE:
334
+ return _dispatch_warehouse_procedure(resolved, fault_tolerant, environment)
335
+ if kind == SPARK_SQL_FILE:
336
+ return _dispatch_spark_sql_file(resolved, fault_tolerant, environment)
337
+ if kind in (PYTHON_TABLE, PYTHON_FOLDER):
338
+ return _dispatch_python(resolved, fault_tolerant, environment)
339
+ if kind == ENDPOINT_REFRESH:
340
+ return _dispatch_endpoint_refresh(resolved, environment)
341
+ raise LoadError(f"{resolved.node_id} names unknown primitive kind {kind!r}")
342
+
343
+
344
+ def _dispatch_warehouse_procedure(
345
+ resolved, fault_tolerant: bool, environment: LoadEnvironment
346
+ ) -> LoadResult:
347
+ target = resolved.node.physical_target
348
+ sql = environment.sql_for(target)
349
+ if sql is None:
350
+ raise LoadError(
351
+ f"{resolved.node_id} needs a SQL capability for {target}, and this "
352
+ "run has none"
353
+ )
354
+ procedure = load_procedure_name(resolved.node.logical_id.object_id)
355
+ rows = sql.query(
356
+ f"exec {procedure} @fault_tolerant = {1 if fault_tolerant else 0}"
357
+ )
358
+ if not rows:
359
+ raise LoadError(
360
+ f"{procedure} returned no row — a load procedure projects its result"
361
+ )
362
+ return LoadResult.from_row(rows[0])
363
+
364
+
365
+ def _dispatch_spark_sql_file(
366
+ resolved, fault_tolerant: bool, environment: LoadEnvironment
367
+ ) -> LoadResult:
368
+ from .runtime.spark_load import run_load_program
369
+
370
+ if environment.store is None or environment.spark is None:
371
+ raise LoadError(
372
+ f"{resolved.node_id} needs a store and a Spark session, and this run "
373
+ "has neither"
374
+ )
375
+ program = environment.store.read(Location(resolved.dispatch_location))
376
+ return run_load_program(
377
+ environment.spark,
378
+ program.decode("utf-8"),
379
+ fault_tolerant=fault_tolerant,
380
+ )
381
+
382
+
383
+ def _dispatch_python(
384
+ resolved, fault_tolerant: bool, environment: LoadEnvironment
385
+ ) -> LoadResult:
386
+ """Import the deployed module, construct its object, and load it.
387
+
388
+ The destination is resolved *here* and handed in, never inferred: an authored
389
+ object with no Lakehouse falls back to the session's attachment, which in an
390
+ orchestrated run is the Weaver control plane. Orchestration runs detached
391
+ from every destination it writes to, so it must always say which one it
392
+ means.
393
+ """
394
+
395
+ from .lakehouse import lakehouse_for
396
+
397
+ if environment.spark is None:
398
+ raise LoadError(f"{resolved.node_id} needs a Spark session, and this run has none")
399
+ lakehouse = lakehouse_for(environment.resolver, ItemRef(resolved.node.physical_target.name))
400
+ runtime_root = _join(lakehouse.files_root(), *LOAD_ROOT.split("/"))
401
+ relative = (
402
+ f"{resolved.node.primitive_object.schema}/"
403
+ f"{resolved.node.primitive_object.object}"
404
+ )
405
+ within = relative[len(LOAD_ROOT) + 1 :] if relative.startswith(LOAD_ROOT) else relative
406
+ module = _import_deployed(
407
+ runtime_root, within, expected=resolved.expected_class, node_id=resolved.node_id
408
+ )
409
+ cls = getattr(module, resolved.expected_class)
410
+ return cls(environment.spark, lakehouse=lakehouse).load(
411
+ fault_tolerant=fault_tolerant
412
+ )
413
+
414
+
415
+ def _import_deployed(runtime_root: str, relative: str, *, expected: str, node_id: str):
416
+ """One deployed module, imported from the runtime tree it was deployed into.
417
+
418
+ Loaded from its exact file, so the module a node dispatches is unambiguously
419
+ the one at the location the node resolved — but *named* by its position in
420
+ the tree, ``Files.Sales__Seed`` rather than ``Sales__Seed``, because that is
421
+ what other deployed modules import it as. Naming it otherwise would leave two
422
+ module objects for one file, one of them the object nobody imports.
423
+
424
+ The tree's root goes on ``sys.path`` first, so a module's own imports resolve
425
+ exactly as they did when it was authored: ``from lib.dates import parse``
426
+ finds ``lib`` where it was written, and ``from Files.Sales__Seed import …``
427
+ finds the folder module through the ordinary machinery.
428
+ """
429
+
430
+ import importlib.util
431
+ import sys
432
+
433
+ path = _join(runtime_root, *relative.split("/"))
434
+ # Ahead of whatever is already there: a process may hold more than one
435
+ # estate's runtime tree, and the one being dispatched is the one that wins.
436
+ if runtime_root in sys.path:
437
+ sys.path.remove(runtime_root)
438
+ sys.path.insert(0, runtime_root)
439
+ importlib.invalidate_caches()
440
+ name = relative[: -len(".py")].replace("/", ".")
441
+ specification = importlib.util.spec_from_file_location(name, path)
442
+ if specification is None or specification.loader is None:
443
+ raise LoadError(f"{node_id}: no deployed module at {path}")
444
+ module = importlib.util.module_from_spec(specification)
445
+ # Registered before execution so a module that imports itself by name — and a
446
+ # dataclass or pickle that later looks it up — finds the one being run.
447
+ sys.modules[name] = module
448
+ try:
449
+ specification.loader.exec_module(module)
450
+ except FileNotFoundError as exc:
451
+ raise LoadError(f"{node_id}: no deployed module at {path}") from exc
452
+ except Exception as exc: # noqa: BLE001 - authored code, any failure is data
453
+ raise LoadError(
454
+ f"{node_id}: importing {path} raised {type(exc).__name__}: {exc}"
455
+ ) from exc
456
+ if not hasattr(module, expected):
457
+ raise LoadError(
458
+ f"{node_id}: {path} defines no class {expected!r} — a deployed object "
459
+ "module names its class for its file"
460
+ )
461
+ return module
462
+
463
+
464
+ def _dispatch_endpoint_refresh(resolved, environment: LoadEnvironment) -> LoadResult:
465
+ """Refresh one Lakehouse's SQL analytics endpoint. No rows, so no counts."""
466
+
467
+ refresh = getattr(environment.resolver, "refresh_sql_endpoint", None)
468
+ if refresh is None:
469
+ raise LoadError(
470
+ f"{resolved.node_id}: this environment cannot refresh a SQL endpoint"
471
+ )
472
+ refresh(ItemRef(resolved.node.physical_target.name))
473
+ return LoadResult(succeeded=True)
474
+
475
+
476
+ def _join(root: str, *parts: str) -> str:
477
+ return "/".join([root.rstrip("/"), *parts])
478
+
479
+
480
+ __all__ = [
481
+ "dispatch_load_node",
482
+ "execute_load_plan",
483
+ ]