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
weaver/load.py ADDED
@@ -0,0 +1,474 @@
1
+ """The public ``weaver.load(...)`` operation.
2
+
3
+ The adaptation boundary between the small notebook-facing surface and the
4
+ planning, resolution, execution and logging seams beneath it — the load
5
+ counterpart of :mod:`weaver.operations`, and deliberately shaped like it:
6
+
7
+ .. code-block:: python
8
+
9
+ weaver.load(["Warehouse/Reporting", "Lakehouse/Curated"])
10
+
11
+ Typed physical target strings at the boundary; an explicit workspace or workspace
12
+ configuration where there is one; the notebook's own context where there is not;
13
+ and typed objects retained only as internal seams. What a caller writes here is
14
+ what they already write for ``build`` and ``wipe``, because it is parsed by the
15
+ same grammar.
16
+
17
+ **The catalogue is the source, and the repository is not consulted.** By the time
18
+ anything is loadable it has been built, and what was built is recorded. Reopening
19
+ the declaration would orchestrate what somebody meant to install rather than what
20
+ is installed, and the two differ exactly when it matters most.
21
+
22
+ The order below is the order things must happen in, and each step is somebody
23
+ else's module:
24
+
25
+ .. code-block:: text
26
+
27
+ parse targets weaver.targets
28
+ resolve workspace weaver.operations
29
+ read the catalogue weaver.catalogue.state
30
+ reverse the bindings weaver.load_plan
31
+ build the physical DAG weaver.load_plan
32
+ resolve every node weaver.load_resolution
33
+ ── dry run stops here ──
34
+ execute sequentially weaver.load_execution
35
+ write task evidence weaver.task_logging
36
+
37
+ Nothing executes while catalogue state is still being discovered: the whole plan
38
+ is settled, ordered and resolved before the first primitive is dispatched.
39
+ """
40
+
41
+ from __future__ import annotations
42
+
43
+ from datetime import datetime, timezone
44
+ from pathlib import Path
45
+ from typing import Any, Mapping, Sequence
46
+
47
+ from .errors import CommandError, LoadError
48
+ from .load_execution import execute_load_plan
49
+ from .load_plan import (
50
+ ENDPOINT_REFRESH,
51
+ LAKEHOUSE_TARGET,
52
+ WAREHOUSE_TARGET,
53
+ InstalledEstate,
54
+ LoadDag,
55
+ PhysicalTargetRef,
56
+ load_dag,
57
+ )
58
+ from .load_report import (
59
+ LoadNodeReport,
60
+ LoadRunReport,
61
+ SUCCEEDED,
62
+ SUCCEEDED_WITH_REJECTS,
63
+ final_status,
64
+ )
65
+ from .load_resolution import LoadEnvironment, dry_run_reports, resolve_load_plan
66
+ from .targets import (
67
+ DeltaTarget,
68
+ ItemRef,
69
+ WarehouseTarget,
70
+ parse_physical_target,
71
+ physical_item,
72
+ )
73
+ from .workspaces import FabricWorkspace, LocalWorkspace, Workspace
74
+
75
+ #: The task type this operation writes evidence under.
76
+ TASK_TYPE = "load"
77
+
78
+
79
+ def load(
80
+ targets: str | Sequence[str],
81
+ *,
82
+ workspace: str | Path | Workspace | None = None,
83
+ workspace_config: str | Path | None = None,
84
+ fault_tolerant: bool = False,
85
+ dry_run: bool = False,
86
+ ) -> LoadRunReport:
87
+ """Load every installed loadable object in the requested physical targets.
88
+
89
+ ``targets`` are typed physical items — ``Lakehouse/Curated``,
90
+ ``Warehouse/Reporting`` — and the request means *everything loadable hosted
91
+ there, plus whatever upstream work those objects need*. Object-level
92
+ selection is not part of this phase.
93
+
94
+ ``workspace=None`` means the current Fabric session, exactly as it does for
95
+ ``build`` and ``wipe``.
96
+ """
97
+
98
+ values = (targets,) if isinstance(targets, str) else tuple(targets)
99
+ if not values:
100
+ raise CommandError("load needs at least one target")
101
+ requested = tuple(
102
+ parse_physical_target(value, what="load target", error=CommandError)
103
+ for value in values
104
+ )
105
+
106
+ from .operations import _operation_workspace, _with_inferred_control_lakehouse
107
+
108
+ resolved_workspace = _with_inferred_control_lakehouse(
109
+ _operation_workspace(workspace=workspace, workspace_config=workspace_config)
110
+ )
111
+ if not resolved_workspace.weaver_lakehouse:
112
+ raise CommandError(
113
+ "load needs a Weaver control Lakehouse in workspace configuration "
114
+ "or as the notebook's attached default Lakehouse"
115
+ )
116
+ if isinstance(resolved_workspace, LocalWorkspace) and any(
117
+ isinstance(target, WarehouseTarget) for target in requested
118
+ ):
119
+ raise CommandError(
120
+ "Warehouse targets require a Fabric Workspace; the local emulator has no SQL"
121
+ )
122
+
123
+ with _load_session(resolved_workspace, requested) as session:
124
+ return run_load(
125
+ session,
126
+ requested=tuple(_physical_ref(target) for target in requested),
127
+ fault_tolerant=fault_tolerant,
128
+ dry_run=dry_run,
129
+ )
130
+
131
+
132
+ def run_load(
133
+ session: "LoadSession",
134
+ *,
135
+ requested: Sequence[PhysicalTargetRef],
136
+ fault_tolerant: bool = False,
137
+ dry_run: bool = False,
138
+ ) -> LoadRunReport:
139
+ """The whole orchestration path, over a prepared session.
140
+
141
+ Separated from :func:`load` so the composition can be driven without the
142
+ workspace resolution and capability acquisition in front of it — which is
143
+ what the desktop, the emulator and a Fabric session each do differently and
144
+ none of them changes about the orchestration itself.
145
+ """
146
+
147
+ started = datetime.now(timezone.utc)
148
+ dag = load_dag(
149
+ InstalledEstate.from_catalogue(session.read_catalogue()), targets=requested
150
+ )
151
+ environment = session.environment(dag)
152
+ plan = resolve_load_plan(dag, environment=environment)
153
+
154
+ common = {
155
+ "requested": tuple(str(target) for target in requested),
156
+ "dry_run": dry_run,
157
+ "fault_tolerant": fault_tolerant,
158
+ "edges": dag.edges,
159
+ "order": tuple(node.node_id for node in dag.order()),
160
+ "messages": dag.messages,
161
+ "workspace": str(session.workspace.workspace),
162
+ "started_at": started.isoformat(),
163
+ }
164
+
165
+ if dry_run:
166
+ nodes = dry_run_reports(plan)
167
+ return LoadRunReport(
168
+ **common,
169
+ nodes=nodes,
170
+ status=final_status(nodes, dry_run=True),
171
+ finished_at=datetime.now(timezone.utc).isoformat(),
172
+ )
173
+
174
+ log = session.open_log()
175
+ if log is not None:
176
+ log.write_plan(_plan_document(plan, common))
177
+ nodes = execute_load_plan(
178
+ plan,
179
+ fault_tolerant=fault_tolerant,
180
+ environment=environment,
181
+ on_step=(
182
+ None
183
+ if log is None
184
+ else lambda report: log.write_step(_step_type(report), report.to_mapping())
185
+ ),
186
+ )
187
+ status = final_status(nodes, dry_run=False)
188
+ report = LoadRunReport(
189
+ **common,
190
+ nodes=nodes,
191
+ status=status,
192
+ task_id=None if log is None else log.task_id,
193
+ task_log=None if log is None else log.root.value,
194
+ finished_at=datetime.now(timezone.utc).isoformat(),
195
+ )
196
+ if log is not None:
197
+ log.write_completion(_completion_document(report))
198
+ return report
199
+
200
+
201
+ def _step_type(report: LoadNodeReport) -> str:
202
+ """The broad kind a step file's name carries: a load, or a refresh."""
203
+
204
+ return "refresh" if report.primitive_kind == ENDPOINT_REFRESH else "load"
205
+
206
+
207
+ def _plan_document(plan, common: Mapping[str, Any]) -> dict:
208
+ """The complete intended task, written once before anything runs.
209
+
210
+ Enough on its own to answer what was requested, what Weaver intended to run,
211
+ in what order, against which physical targets and through which installed
212
+ primitives — which is the whole point of writing it before rather than after.
213
+ """
214
+
215
+ from . import __version__
216
+
217
+ return {
218
+ "weaver_version": __version__,
219
+ "requested": list(common["requested"]),
220
+ "mode": "dry_run" if common["dry_run"] else "execute",
221
+ "fault_tolerant": common["fault_tolerant"],
222
+ "workspace": common["workspace"],
223
+ "started_at": common["started_at"],
224
+ "order": list(common["order"]),
225
+ "edges": [list(edge) for edge in common["edges"]],
226
+ "nodes": [
227
+ {
228
+ "node_id": resolved.node.node_id,
229
+ "logical_id": str(resolved.node.logical_id)
230
+ if resolved.node.logical_id
231
+ else None,
232
+ "physical_target": str(resolved.node.physical_target),
233
+ "physical_object": str(resolved.node.physical_object)
234
+ if resolved.node.physical_object
235
+ else None,
236
+ "primitive_kind": resolved.node.primitive_kind,
237
+ "dispatch_location": resolved.dispatch_location,
238
+ "target_exists": resolved.target_exists,
239
+ "primitive_exists": resolved.primitive_exists,
240
+ }
241
+ for resolved in plan.order
242
+ ],
243
+ "messages": [message.to_mapping() for message in common["messages"]],
244
+ }
245
+
246
+
247
+ def _completion_document(report: LoadRunReport) -> dict:
248
+ """What the run added up to, reconciled from the steps rather than tallied."""
249
+
250
+ counted = {status: 0 for status in ("executed", "succeeded", "failed", "blocked")}
251
+ rows = {
252
+ "rows_read": 0,
253
+ "rows_inserted": 0,
254
+ "rows_updated": 0,
255
+ "rows_deleted": 0,
256
+ "rows_rejected": 0,
257
+ }
258
+ for node in report.nodes:
259
+ counted["executed"] += 1 if node.executed else 0
260
+ counted["succeeded"] += 1 if node.status in (
261
+ SUCCEEDED,
262
+ SUCCEEDED_WITH_REJECTS,
263
+ ) else 0
264
+ counted["failed"] += 1 if node.status == "failed" else 0
265
+ counted["blocked"] += 1 if node.status == "blocked" else 0
266
+ if node.result is not None:
267
+ for name in rows:
268
+ rows[name] += getattr(node.result, name)
269
+ return {
270
+ "mode": "execute",
271
+ "final_status": report.status,
272
+ "planned_steps": len(report.nodes),
273
+ "executed_steps": counted["executed"],
274
+ "succeeded_steps": counted["succeeded"],
275
+ "failed_steps": counted["failed"],
276
+ "blocked_steps": counted["blocked"],
277
+ "rows": rows,
278
+ "messages": [message.to_mapping() for message in report.messages],
279
+ }
280
+
281
+
282
+ def _physical_ref(target) -> PhysicalTargetRef:
283
+ return PhysicalTargetRef(
284
+ kind=LAKEHOUSE_TARGET if isinstance(target, DeltaTarget) else WAREHOUSE_TARGET,
285
+ name=physical_item(target).name,
286
+ )
287
+
288
+
289
+ # --- acquiring capabilities ---------------------------------------------------
290
+ #
291
+ # The one part that differs between the emulator, a desktop process and a Fabric
292
+ # session. Everything above this line is the same code in all three.
293
+
294
+
295
+ class LoadSession:
296
+ """The capabilities one load run needs, acquired for its host.
297
+
298
+ Owns what it opened and closes it — a Spark session locally, a TDS connection
299
+ per Warehouse — and nothing it was given.
300
+ """
301
+
302
+ def __init__(self, workspace: Workspace, requested, *, spark=None, store=None) -> None:
303
+ self.workspace = workspace
304
+ self.requested = tuple(requested)
305
+ self.spark = spark
306
+ self.store = store
307
+ self._sql: dict[str, Any] = {}
308
+ self._opened: list[Any] = []
309
+
310
+ # --- context ------------------------------------------------------------
311
+
312
+ def __enter__(self) -> "LoadSession":
313
+ return self
314
+
315
+ def __exit__(self, *exc) -> bool:
316
+ for opened in reversed(self._opened):
317
+ close = getattr(opened, "close", None)
318
+ if close is not None:
319
+ close()
320
+ self._opened.clear()
321
+ return False
322
+
323
+ # --- what orchestration asks for ----------------------------------------
324
+
325
+ @property
326
+ def resolver(self):
327
+ from .resolution import resolver_for
328
+
329
+ return resolver_for(self.workspace)
330
+
331
+ def read_catalogue(self):
332
+ """The installed catalogue, read from the Weaver control Lakehouse."""
333
+
334
+ from .catalogue.state import read_installed_catalogue
335
+ from .spark import SparkCatalogue
336
+
337
+ if self.spark is None:
338
+ raise LoadError(
339
+ "reading the installed catalogue needs a Spark session"
340
+ )
341
+ return read_installed_catalogue(
342
+ SparkCatalogue(
343
+ self.spark,
344
+ self.resolver.spark_destination(ItemRef(self.workspace.weaver_lakehouse)),
345
+ )
346
+ )
347
+
348
+ def environment(self, dag: LoadDag) -> LoadEnvironment:
349
+ """Runtime services plus the physical state every planned target is in.
350
+
351
+ The inventory read happens once, here, and the whole of resolution then
352
+ runs against frozen state — the same discipline a build follows, and for
353
+ the same reason: a decision made against state that is still moving is a
354
+ decision nobody can reproduce.
355
+ """
356
+
357
+ targets = tuple(dict.fromkeys(node.physical_target for node in dag.nodes))
358
+ inventories = {}
359
+ for target in targets:
360
+ observed = self._inventory(target)
361
+ if observed is not None:
362
+ inventories[str(target)] = observed
363
+ return LoadEnvironment(
364
+ resolver=self.resolver,
365
+ inventories=inventories,
366
+ store=self.store,
367
+ spark=self.spark,
368
+ sql=self._sql,
369
+ workspace=self.workspace,
370
+ )
371
+
372
+ def open_log(self):
373
+ from .task_logging import log_folder, open_task_log
374
+
375
+ if self.store is None:
376
+ raise LoadError("writing a task log needs a store")
377
+ return open_task_log(
378
+ task_type=TASK_TYPE,
379
+ folder=log_folder(self.resolver, ItemRef(self.workspace.weaver_lakehouse)),
380
+ store=self.store,
381
+ )
382
+
383
+ # --- reading physical state ---------------------------------------------
384
+
385
+ def _inventory(self, target: PhysicalTargetRef):
386
+ from .build_bundle.prune import (
387
+ read_lakehouse_inventory,
388
+ read_warehouse_inventory,
389
+ )
390
+ from .build_bundle.targets import LakehouseBinding, WarehouseBinding
391
+
392
+ if target.kind == LAKEHOUSE_TARGET:
393
+ bound = LakehouseBinding(ItemRef(target.name)).to_bound_target()
394
+ try:
395
+ return read_lakehouse_inventory(
396
+ bound, resolver=self.resolver, store=self.store, spark=self.spark
397
+ )
398
+ except Exception:
399
+ # A target that cannot be read is a target that is not there, as
400
+ # far as this run is concerned. Resolution says so per node,
401
+ # naming the target, rather than the whole run failing on a read.
402
+ return None
403
+ bound = WarehouseBinding(ItemRef(target.name)).to_bound_target()
404
+ sql = self._warehouse_sql(target.name)
405
+ if sql is None:
406
+ return None
407
+ try:
408
+ return read_warehouse_inventory(bound, sql=sql)
409
+ except Exception:
410
+ return None
411
+
412
+ def _warehouse_sql(self, name: str):
413
+ if name in self._sql:
414
+ return self._sql[name]
415
+ executor = self._open_warehouse_sql(name)
416
+ if executor is not None:
417
+ self._sql[name] = executor
418
+ return executor
419
+
420
+ def _open_warehouse_sql(self, name: str):
421
+ from .operations import _inside_fabric_session
422
+
423
+ target = WarehouseTarget(ItemRef(name))
424
+ if not isinstance(self.workspace, FabricWorkspace):
425
+ return None
426
+ if _inside_fabric_session(self.workspace):
427
+ from .fabric.sql import fabric_sql_executor
428
+
429
+ executor = fabric_sql_executor(target, self.workspace)
430
+ else:
431
+ from .fabric import desktop_sql_executor
432
+
433
+ executor = desktop_sql_executor(target, self.workspace)
434
+ self._opened.append(executor)
435
+ return executor
436
+
437
+
438
+ def _load_session(workspace: Workspace, requested) -> LoadSession:
439
+ """A session with this host's Spark and store already acquired."""
440
+
441
+ from .operations import _active_spark, _inside_fabric_session, _operation_store
442
+
443
+ if isinstance(workspace, LocalWorkspace):
444
+ from .spark import local_delta_session
445
+ from .store import LocalStore
446
+
447
+ session = local_delta_session(workspace)
448
+ spark = session.__enter__()
449
+ opened = LoadSession(workspace, requested, spark=spark, store=LocalStore())
450
+ opened._opened.append(_Closing(lambda: session.__exit__(None, None, None)))
451
+ return opened
452
+ if not _inside_fabric_session(workspace):
453
+ raise CommandError(
454
+ "load runs where the data is: call it from a Fabric notebook, or "
455
+ "against a local Workspace"
456
+ )
457
+ from .resolution import store_for
458
+
459
+ return LoadSession(
460
+ workspace, requested, spark=_active_spark(), store=store_for(workspace)
461
+ )
462
+
463
+
464
+ class _Closing:
465
+ """Adapts an arbitrary teardown into the ``close()`` the session expects."""
466
+
467
+ def __init__(self, teardown) -> None:
468
+ self._teardown = teardown
469
+
470
+ def close(self) -> None:
471
+ self._teardown()
472
+
473
+
474
+ __all__ = ["LoadSession", "TASK_TYPE", "load", "run_load"]