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.
- weaver/__init__.py +59 -0
- weaver/build_bundle/__init__.py +109 -0
- weaver/build_bundle/aliases.py +325 -0
- weaver/build_bundle/bundle.py +359 -0
- weaver/build_bundle/catalogue_actions.py +275 -0
- weaver/build_bundle/changes.py +186 -0
- weaver/build_bundle/endpoints.py +83 -0
- weaver/build_bundle/executors/__init__.py +69 -0
- weaver/build_bundle/executors/alias.py +202 -0
- weaver/build_bundle/executors/base.py +132 -0
- weaver/build_bundle/executors/folder.py +71 -0
- weaver/build_bundle/executors/load_file.py +205 -0
- weaver/build_bundle/executors/spark_case.py +26 -0
- weaver/build_bundle/executors/spark_schema.py +60 -0
- weaver/build_bundle/executors/spark_sql.py +59 -0
- weaver/build_bundle/executors/spark_sql_batch.py +57 -0
- weaver/build_bundle/executors/spark_table.py +213 -0
- weaver/build_bundle/executors/sql_endpoint_refresh.py +34 -0
- weaver/build_bundle/executors/tsql.py +81 -0
- weaver/build_bundle/incremental.py +288 -0
- weaver/build_bundle/installer.py +384 -0
- weaver/build_bundle/models.py +288 -0
- weaver/build_bundle/payloads.py +34 -0
- weaver/build_bundle/physical.py +625 -0
- weaver/build_bundle/planner.py +389 -0
- weaver/build_bundle/prune.py +620 -0
- weaver/build_bundle/report.py +108 -0
- weaver/build_bundle/stages.py +196 -0
- weaver/build_bundle/targets.py +272 -0
- weaver/build_bundle/workflow.py +585 -0
- weaver/catalogue/__init__.py +73 -0
- weaver/catalogue/builtin.py +238 -0
- weaver/catalogue/claims.py +121 -0
- weaver/catalogue/projection.py +437 -0
- weaver/catalogue/reader.py +152 -0
- weaver/catalogue/reconcile.py +231 -0
- weaver/catalogue/render.py +410 -0
- weaver/catalogue/state.py +660 -0
- weaver/catalogue/tables.py +648 -0
- weaver/config.py +178 -0
- weaver/declaration/__init__.py +171 -0
- weaver/declaration/columns.py +223 -0
- weaver/declaration/ddl.py +266 -0
- weaver/declaration/dependencies.py +544 -0
- weaver/declaration/graph.py +240 -0
- weaver/declaration/item_dependencies.py +292 -0
- weaver/declaration/load.py +191 -0
- weaver/declaration/metadata.py +1405 -0
- weaver/declaration/model.py +448 -0
- weaver/declaration/references.py +294 -0
- weaver/declaration/repository.py +959 -0
- weaver/declaration/schemas.py +135 -0
- weaver/declaration/source.py +674 -0
- weaver/declaration/spark_load.py +759 -0
- weaver/declaration/sql_shaping.py +591 -0
- weaver/declaration/templates/ddl/declared_create_table.sql +64 -0
- weaver/declaration/templates/ddl/infer_create_table.sql +97 -0
- weaver/declaration/templates/ddl/metadata_column_validation.sql +30 -0
- weaver/declaration/templates/load/column_metadata.sql +40 -0
- weaver/declaration/templates/load/full_replace_body.sql +21 -0
- weaver/declaration/templates/load/install_load_procedure.sql +27 -0
- weaver/declaration/templates/load/load_procedure.sql +48 -0
- weaver/declaration/templates/load/primary_key_body.sql +113 -0
- weaver/declaration/tsql_ddl.py +468 -0
- weaver/declaration/tsql_load.py +417 -0
- weaver/declaration/warehouse_type_mapping.yml +93 -0
- weaver/diagnostics.py +247 -0
- weaver/errors.py +61 -0
- weaver/etl.py +469 -0
- weaver/fabric/__init__.py +107 -0
- weaver/fabric/auth.py +137 -0
- weaver/fabric/capacity.py +143 -0
- weaver/fabric/client.py +147 -0
- weaver/fabric/environment.py +460 -0
- weaver/fabric/livy.py +478 -0
- weaver/fabric/notebooks.py +201 -0
- weaver/fabric/onelake.py +263 -0
- weaver/fabric/resolution.py +344 -0
- weaver/fabric/resources.py +245 -0
- weaver/fabric/session.py +148 -0
- weaver/fabric/shortcuts.py +120 -0
- weaver/fabric/sql.py +118 -0
- weaver/fabric/store.py +198 -0
- weaver/initialise.py +209 -0
- weaver/lakehouse.py +386 -0
- weaver/load.py +474 -0
- weaver/load_execution.py +483 -0
- weaver/load_plan.py +912 -0
- weaver/load_report.py +330 -0
- weaver/load_resolution.py +386 -0
- weaver/locations.py +164 -0
- weaver/objects.py +392 -0
- weaver/operations.py +757 -0
- weaver/physical_wipe.py +369 -0
- weaver/push.py +76 -0
- weaver/resolution.py +292 -0
- weaver/runtime/__init__.py +30 -0
- weaver/runtime/folder_load.py +402 -0
- weaver/runtime/load_contract.py +245 -0
- weaver/runtime/load_result.py +104 -0
- weaver/runtime/spark_load.py +152 -0
- weaver/runtime/table_load.py +497 -0
- weaver/spark/__init__.py +49 -0
- weaver/spark/catalogue.py +245 -0
- weaver/spark/destination.py +195 -0
- weaver/spark/session.py +84 -0
- weaver/spark/tokens.py +138 -0
- weaver/sql/__init__.py +40 -0
- weaver/sql/authentication.py +38 -0
- weaver/sql/connection.py +90 -0
- weaver/sql/errors.py +25 -0
- weaver/sql/execution.py +123 -0
- weaver/sql/pool.py +174 -0
- weaver/sql/wipe.py +156 -0
- weaver/store.py +209 -0
- weaver/targets.py +257 -0
- weaver/task_logging.py +215 -0
- weaver/unbind.py +74 -0
- weaver/workspaces.py +175 -0
- weaver_cli/__init__.py +12 -0
- weaver_cli/__main__.py +7 -0
- weaver_cli/main.py +626 -0
- weaverstack-0.1.1.dist-info/METADATA +113 -0
- weaverstack-0.1.1.dist-info/RECORD +127 -0
- weaverstack-0.1.1.dist-info/WHEEL +4 -0
- weaverstack-0.1.1.dist-info/entry_points.txt +2 -0
- 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"]
|