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_cli/main.py ADDED
@@ -0,0 +1,626 @@
1
+ """Top-level CLI routing.
2
+
3
+ Commands are registered here as the core APIs they wrap become available. The
4
+ handler contract is the one convention worth keeping from the old repository:
5
+ a command function returns a plain serialisable structure and the CLI prints
6
+ it.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import argparse
12
+ import sys
13
+ import time
14
+
15
+ import weaver
16
+ from weaver.errors import WeaverError
17
+
18
+ #: The capacity verbs, kept here so the parser needs no Fabric import — a
19
+ #: CLI-only install without the [fabric] extra must still build its parser.
20
+ CAPACITY_ACTIONS = ("status", "resume", "suspend")
21
+
22
+
23
+ def build_parser() -> argparse.ArgumentParser:
24
+ parser = argparse.ArgumentParser(
25
+ prog="weaver",
26
+ description="Weaver — build and load Fabric Lakehouse and Warehouse objects.",
27
+ )
28
+ parser.add_argument(
29
+ "--version",
30
+ action="version",
31
+ version=f"weaverstack {weaver.__version__}",
32
+ )
33
+ subcommands = parser.add_subparsers(dest="command", metavar="command")
34
+
35
+ doctor = subcommands.add_parser(
36
+ "doctor", help="check whether this machine can run local Spark and Delta"
37
+ )
38
+ doctor.add_argument("--json", action="store_true", help="emit the report as JSON")
39
+ doctor.set_defaults(handler=handle_doctor)
40
+
41
+ build = subcommands.add_parser(
42
+ "build", help="build bound logical items from an explicit repository"
43
+ )
44
+ build.add_argument(
45
+ "repository",
46
+ nargs="?",
47
+ help="authored repository folder; defaults to the current directory/Notebook Resources",
48
+ )
49
+ build.add_argument(
50
+ "--bind",
51
+ dest="item_bindings",
52
+ action="append",
53
+ metavar="PHYSICAL[=LOGICAL]",
54
+ help="typed physical target with configured default or logical override",
55
+ )
56
+ build.add_argument(
57
+ "--bundle",
58
+ nargs="?",
59
+ const="",
60
+ metavar="NAME",
61
+ help=(
62
+ "optionally retain a .weaver.zip build record; omit NAME for a UTC "
63
+ "timestamp"
64
+ ),
65
+ )
66
+ build.add_argument("--json", action="store_true", help="emit the result as JSON")
67
+ _add_workspace_args(build)
68
+ build.set_defaults(handler=handle_build)
69
+
70
+ push = subcommands.add_parser(
71
+ "push", help="compatibility utility: validate and upload an authored repository"
72
+ )
73
+ push.add_argument("repository", help="local authored repository folder")
74
+ push.add_argument("--json", action="store_true", help="emit the result as JSON")
75
+ _add_workspace_args(push)
76
+ push.set_defaults(handler=handle_push)
77
+
78
+ unbind = subcommands.add_parser(
79
+ "unbind", help="remove catalogue state for named physical targets"
80
+ )
81
+ unbind.add_argument(
82
+ "targets",
83
+ nargs="+",
84
+ metavar="TARGET",
85
+ help="Lakehouse/Name or Warehouse/Name",
86
+ )
87
+ unbind.add_argument("--json", action="store_true", help="emit the result as JSON")
88
+ _add_workspace_args(unbind)
89
+ unbind.set_defaults(handler=handle_unbind)
90
+
91
+ wipe = subcommands.add_parser(
92
+ "wipe", help="clear a physical Lakehouse or Warehouse"
93
+ )
94
+ wipe.add_argument(
95
+ "targets",
96
+ nargs="+",
97
+ metavar="TARGET",
98
+ help="Lakehouse/Name[/Files|/Tables] or Warehouse/Name",
99
+ )
100
+ _add_workspace_args(wipe)
101
+ wipe.add_argument(
102
+ "--unbind-from",
103
+ metavar="LAKEHOUSE",
104
+ help="immediately remove wiped target claims from this Weaver catalogue",
105
+ )
106
+ wipe.add_argument("--dry-run", action="store_true", help="report without removing")
107
+ wipe.add_argument("--yes", action="store_true", help="do not ask for confirmation")
108
+ wipe.add_argument("--json", action="store_true", help="emit the result as JSON")
109
+ wipe.set_defaults(handler=handle_wipe)
110
+
111
+ install = subcommands.add_parser(
112
+ "install",
113
+ help="build Weaver and install it into a Fabric Environment",
114
+ )
115
+ _add_workspace_args(install, include_weaver_lakehouse=False)
116
+ install.add_argument(
117
+ "--no-publish",
118
+ action="store_true",
119
+ help="stage the wheel and dependencies but do not publish (development only)",
120
+ )
121
+ install.add_argument("--json", action="store_true", help="emit the result as JSON")
122
+ install.set_defaults(handler=handle_install)
123
+
124
+ notebook = subcommands.add_parser(
125
+ "notebook", help="deploy or execute a Fabric notebook"
126
+ )
127
+ notebook_commands = notebook.add_subparsers(
128
+ dest="notebook_command", metavar="command"
129
+ )
130
+
131
+ notebook_push = notebook_commands.add_parser(
132
+ "push", help="create or update a notebook definition"
133
+ )
134
+ notebook_push.add_argument("source", help="local .py or .ipynb notebook source")
135
+ notebook_push.add_argument("--name", help="Fabric display name; defaults to filename")
136
+ notebook_push.add_argument("--description")
137
+ notebook_push.add_argument("--json", action="store_true")
138
+ _add_workspace_args(notebook_push, include_weaver_lakehouse=False)
139
+ notebook_push.set_defaults(handler=handle_notebook_push)
140
+
141
+ notebook_run = notebook_commands.add_parser(
142
+ "run", help="execute a deployed notebook in Fabric"
143
+ )
144
+ notebook_run.add_argument("name", help="Fabric Notebook display name")
145
+ notebook_run.add_argument(
146
+ "--lakehouse",
147
+ help="default Lakehouse attached to the notebook session",
148
+ )
149
+ notebook_run.add_argument("--no-wait", action="store_true")
150
+ notebook_run.add_argument("--timeout", type=float, default=7200.0)
151
+ notebook_run.add_argument("--poll-interval", type=float, default=10.0)
152
+ notebook_run.add_argument("--json", action="store_true")
153
+ _add_workspace_args(notebook_run)
154
+ notebook_run.set_defaults(handler=handle_notebook_run)
155
+
156
+ capacity = subcommands.add_parser(
157
+ "capacity", help="turn a Fabric capacity on or off, or report its state"
158
+ )
159
+ capacity.add_argument("action", choices=CAPACITY_ACTIONS)
160
+ capacity.add_argument("--resource-group", required=True)
161
+ capacity.add_argument("--capacity-name", required=True)
162
+ capacity.add_argument(
163
+ "--subscription-id",
164
+ help="only needed when az has more than one subscription",
165
+ )
166
+ capacity.set_defaults(handler=handle_capacity)
167
+
168
+ return parser
169
+
170
+
171
+ def _fabric_cli_workspace(args: argparse.Namespace):
172
+ """Resolve the Fabric-only values shared by notebook CLI utilities."""
173
+
174
+ from weaver.errors import CommandError
175
+ from weaver.workspaces import FabricWorkspace
176
+
177
+ workspace = _resolve_workspace(args)
178
+ if not isinstance(workspace, FabricWorkspace):
179
+ raise CommandError("weaver notebook requires a Fabric Workspace")
180
+ return workspace
181
+
182
+
183
+ def handle_notebook_push(args: argparse.Namespace) -> int:
184
+ """Deploy a notebook definition without adding notebook APIs to core."""
185
+
186
+ import json
187
+
188
+ from weaver.fabric.notebooks import push_notebook
189
+
190
+ workspace = _fabric_cli_workspace(args)
191
+ result = push_notebook(
192
+ args.source,
193
+ workspace=workspace.workspace,
194
+ name=args.name,
195
+ description=args.description,
196
+ )
197
+ if args.json:
198
+ print(json.dumps(result.to_mapping(), indent=2))
199
+ else:
200
+ print(f"{result.action} notebook {result.notebook!r} in {result.workspace!r}")
201
+ print(f" id: {result.notebook_id}")
202
+ print(f" source: {result.source}")
203
+ return 0
204
+
205
+
206
+ def handle_notebook_run(args: argparse.Namespace) -> int:
207
+ """Run a notebook with explicit session attachments."""
208
+
209
+ import json
210
+
211
+ from weaver.errors import CommandError
212
+ from weaver.fabric.notebooks import run_notebook
213
+
214
+ workspace = _fabric_cli_workspace(args)
215
+ lakehouse = args.lakehouse or workspace.weaver_lakehouse
216
+ if not lakehouse:
217
+ raise CommandError(
218
+ "notebook run requires --lakehouse or a configured Weaver Lakehouse"
219
+ )
220
+ if not workspace.environment:
221
+ raise CommandError(
222
+ "notebook run requires --environment or a configured Environment"
223
+ )
224
+ result = run_notebook(
225
+ args.name,
226
+ workspace=workspace.workspace,
227
+ lakehouse=lakehouse,
228
+ environment=workspace.environment,
229
+ wait=not args.no_wait,
230
+ timeout=args.timeout,
231
+ poll_interval=args.poll_interval,
232
+ )
233
+ if args.json:
234
+ print(json.dumps(result.to_mapping(), indent=2))
235
+ else:
236
+ print(f"notebook {result.notebook!r}: {result.status}")
237
+ print(f" job: {result.job_url}")
238
+ if result.exit_value is not None:
239
+ print(f" result: {result.exit_value}")
240
+ return 0 if result.succeeded or args.no_wait else 1
241
+
242
+
243
+ def handle_install(args: argparse.Namespace) -> int:
244
+ """Build Weaver from this checkout and install it into a Fabric Environment.
245
+
246
+ The authoritative deployment path. Afterwards a notebook, Livy session or
247
+ Fabric pytest run attached to the Environment can ``import weaver`` with no
248
+ source shipped into a Lakehouse.
249
+ """
250
+
251
+ from weaver.workspaces import FabricWorkspace
252
+ from weaver.errors import CommandError
253
+
254
+ workspace = _resolve_workspace(args)
255
+ if not isinstance(workspace, FabricWorkspace):
256
+ raise CommandError("weaver install requires a Fabric Workspace")
257
+ if not workspace.environment:
258
+ raise CommandError("weaver install requires --environment or a configured environment")
259
+ _prefer_desktop_credential()
260
+ from weaver.fabric import install as run_install
261
+
262
+ started = time.perf_counter()
263
+ result = run_install(
264
+ workspace.workspace,
265
+ workspace.environment,
266
+ publish=not args.no_publish,
267
+ )
268
+ total = time.perf_counter() - started
269
+
270
+ if args.json:
271
+ import json
272
+
273
+ payload = result.as_dict()
274
+ payload["timings"]["total"] = round(total, 2)
275
+ print(json.dumps(payload, indent=2))
276
+ return 0
277
+
278
+ _print_install(result, total)
279
+ return 0
280
+
281
+
282
+ def _print_install(result, total: float) -> None:
283
+ print("Installed Weaver into Microsoft Fabric\n")
284
+ print("Workspace")
285
+ print(f" Name: {result.workspace_name}")
286
+ print(f" ID: {result.workspace_id}\n")
287
+ print("Environment")
288
+ print(f" Name: {result.environment_name}")
289
+ print(f" ID: {result.environment_id}\n")
290
+ print("Package")
291
+ print(f" Distribution: {result.package_name}")
292
+ print(f" Version: {result.package_version}")
293
+ print(f" Wheel: {result.wheel_filename}\n")
294
+ print("Changes")
295
+ print(f" Environment created: {'yes' if result.created_environment else 'no'}")
296
+ print(f" Dependencies changed: {'yes' if result.dependencies_changed else 'no'}")
297
+ print(f" Wheel changed: {'yes' if result.wheel_changed else 'no'}")
298
+ print(f" Published: {result.publish_status}\n")
299
+ parts = ", ".join(f"{name} {secs:.1f}s" for name, secs in result.timings.items())
300
+ print(f"Timing {parts + ', ' if parts else ''}total {total:.1f}s\n")
301
+ print("Notebook use")
302
+ print(f' 1. Attach the "{result.environment_name}" Environment.')
303
+ print(" 2. Start a new session.")
304
+ print(" 3. Run: import weaver")
305
+
306
+
307
+ def handle_capacity(args: argparse.Namespace) -> int:
308
+ """Report or change a capacity's state.
309
+
310
+ Capacity is billed while it runs, so this is the first and last thing a
311
+ Fabric session touches.
312
+ """
313
+
314
+ _prefer_desktop_credential()
315
+ from weaver.fabric import run_capacity_action
316
+
317
+ result = run_capacity_action(
318
+ args.action,
319
+ resource_group=args.resource_group,
320
+ capacity_name=args.capacity_name,
321
+ subscription_id=args.subscription_id,
322
+ )
323
+ print(result)
324
+ if args.action == "resume" and not result.running:
325
+ print(" (resuming takes a moment; run `capacity status` to confirm)")
326
+ return 0
327
+
328
+
329
+ def _add_workspace_args(
330
+ parser: argparse.ArgumentParser, *, include_weaver_lakehouse: bool = True
331
+ ) -> None:
332
+ """Add the explicit values that a Workspace configuration can abbreviate."""
333
+
334
+ parser.add_argument("--workspace", help="Fabric Workspace name or local folder path")
335
+ parser.add_argument("--workspace-config", help="one Workspace configuration file")
336
+ parser.add_argument(
337
+ "--workspace-type",
338
+ choices=("fabric", "local"),
339
+ help="resource environment; defaults to fabric",
340
+ )
341
+ parser.add_argument("--environment", help="Fabric Environment name")
342
+ if include_weaver_lakehouse:
343
+ parser.add_argument("--weaver-lakehouse", help="control Lakehouse name")
344
+
345
+
346
+ def _prefer_desktop_credential() -> None:
347
+ """Pin the Azure CLI credential for desktop commands.
348
+
349
+ Credential choice is the CLI's policy, not the core's. Best-effort — if the
350
+ Fabric extra is not installed there is nothing to pin, and a local command
351
+ never needs it.
352
+ """
353
+
354
+ try:
355
+ from weaver.fabric.auth import prefer_cli_credential
356
+ except ImportError:
357
+ return
358
+ prefer_cli_credential()
359
+
360
+
361
+ def _desktop_store(workspace):
362
+ """The store a desktop command uses to reach a workspace.
363
+
364
+ Local is within-workspace; Fabric is cross-boundary, so the CLI constructs the
365
+ OneLakeDfsClient here — core never turns a FabricWorkspace into a DFS client.
366
+ """
367
+
368
+ from weaver.workspaces import LocalWorkspace
369
+ from weaver.store import LocalStore
370
+
371
+ if isinstance(workspace, LocalWorkspace):
372
+ return LocalStore()
373
+ from weaver.fabric import OneLakeDfsClient
374
+
375
+ return OneLakeDfsClient()
376
+
377
+
378
+ def _resolve_workspace(args: argparse.Namespace):
379
+ from weaver.config import resolve_workspace
380
+ from weaver.workspaces import LocalWorkspace
381
+
382
+ workspace = resolve_workspace(
383
+ workspace=args.workspace,
384
+ workspace_type=args.workspace_type,
385
+ environment=args.environment,
386
+ weaver_lakehouse=getattr(args, "weaver_lakehouse", None),
387
+ workspace_config=args.workspace_config,
388
+ )
389
+
390
+ if not isinstance(workspace, LocalWorkspace):
391
+ _prefer_desktop_credential()
392
+ return workspace
393
+
394
+
395
+ def handle_push(args: argparse.Namespace) -> int:
396
+ """Validate locally, then replace ``Files/weaver_items`` as one unit."""
397
+
398
+ import json
399
+
400
+ from weaver.locations import Location
401
+ from weaver.push import push_item_repository
402
+ from weaver.errors import CommandError
403
+ from weaver.resolution import resolver_for
404
+
405
+ workspace = _resolve_workspace(args)
406
+ if not workspace.weaver_lakehouse:
407
+ raise CommandError("push requires --weaver-lakehouse or a configured value")
408
+ resolver = resolver_for(workspace)
409
+ result = push_item_repository(
410
+ Location(args.repository),
411
+ resolver.weaver_items_root,
412
+ destination_store=_desktop_store(workspace),
413
+ )
414
+ payload = result.to_mapping()
415
+ if args.json:
416
+ print(json.dumps(payload, indent=2))
417
+ else:
418
+ print(f"pushed {len(result.files)} file(s)")
419
+ print(f" from: {result.source}")
420
+ print(f" to: {result.destination}")
421
+ print(f" signature: {result.repository_signature}")
422
+ return 0
423
+
424
+
425
+ def handle_unbind(args: argparse.Namespace) -> int:
426
+ import json
427
+
428
+ from weaver.operations import _unbind_target_names
429
+ from weaver.errors import CommandError
430
+
431
+ lakehouses, warehouses = _unbind_target_names(args.targets)
432
+ workspace = _resolve_workspace(args)
433
+ if not workspace.weaver_lakehouse:
434
+ raise CommandError("unbind requires a configured Weaver Lakehouse")
435
+ result = _run_unbind(
436
+ workspace, lakehouses=lakehouses, warehouses=warehouses
437
+ )
438
+ if args.json:
439
+ print(json.dumps(result, indent=2))
440
+ else:
441
+ print(f"unbound {len(result['logical_items'])} logical installation(s)")
442
+ for target in result["targets"]:
443
+ print(f" {target}")
444
+ return 0
445
+
446
+
447
+ def _run_unbind(workspace, *, lakehouses, warehouses) -> dict:
448
+ from weaver.workspaces import LocalWorkspace
449
+
450
+ if isinstance(workspace, LocalWorkspace):
451
+ from weaver.targets import ItemRef
452
+ from weaver.unbind import unbind_targets
453
+ from weaver.resolution import resolver_for
454
+ from weaver.spark import SparkCatalogue, local_delta_session
455
+
456
+ resolver = resolver_for(workspace)
457
+ with local_delta_session(workspace) as session:
458
+ catalogue = SparkCatalogue(
459
+ session,
460
+ resolver.spark_destination(ItemRef(workspace.weaver_lakehouse)),
461
+ )
462
+ return unbind_targets(
463
+ catalogue, lakehouses=lakehouses, warehouses=warehouses
464
+ ).to_mapping()
465
+
466
+ from weaver.fabric import LivySession
467
+
468
+ body = (
469
+ "from weaver.workspaces import FabricWorkspace\n"
470
+ "from weaver.targets import ItemRef\n"
471
+ "from weaver.unbind import unbind_targets\n"
472
+ "from weaver.resolution import resolver_for\n"
473
+ "from weaver.spark import SparkCatalogue\n"
474
+ f"workspace = FabricWorkspace(workspace={workspace.workspace!r}, "
475
+ f"environment={workspace.environment!r}, "
476
+ f"weaver_lakehouse={workspace.weaver_lakehouse!r})\n"
477
+ "resolver = resolver_for(workspace)\n"
478
+ "catalogue = SparkCatalogue(spark, resolver.spark_destination("
479
+ "ItemRef(workspace.weaver_lakehouse)))\n"
480
+ f"result = unbind_targets(catalogue, lakehouses={tuple(lakehouses)!r}, "
481
+ f"warehouses={tuple(warehouses)!r})\n"
482
+ "emit(result.to_mapping())\n"
483
+ )
484
+ with LivySession.for_workspace(workspace) as session:
485
+ return session.run(body).payload
486
+
487
+
488
+ def handle_wipe(args: argparse.Namespace) -> int:
489
+ """Preview, confirm, then invoke the same public wipe operation."""
490
+
491
+ import json
492
+
493
+ workspace = _resolve_workspace(args)
494
+ planned = weaver.wipe(
495
+ args.targets,
496
+ workspace=workspace,
497
+ unbind_from=args.unbind_from,
498
+ dry_run=True,
499
+ )
500
+ print(f"wipe on {workspace.workspace}\n")
501
+ for report in planned.reports:
502
+ print(f" {report.target}")
503
+ print(f" {report.location}")
504
+ for name in report.removed:
505
+ print(f" - {name}")
506
+ if not report.removed:
507
+ print(" (already empty)")
508
+ total = planned.count
509
+ print()
510
+
511
+ if args.dry_run:
512
+ if args.json:
513
+ print(json.dumps(planned.to_mapping(), indent=2))
514
+ else:
515
+ print(f"{total} item(s) would be removed. Nothing was changed.")
516
+ return 0
517
+
518
+ if total and not args.yes:
519
+ if not sys.stdin.isatty():
520
+ print(
521
+ f"Refusing to remove {total} item(s) without confirmation. "
522
+ "Pass --yes, or --dry-run to preview.",
523
+ file=sys.stderr,
524
+ )
525
+ return 1
526
+ answer = input(f"Remove {total} item(s)? This cannot be undone [y/N] ")
527
+ if answer.strip().lower() not in {"y", "yes"}:
528
+ print("Cancelled.")
529
+ return 1
530
+
531
+ result = weaver.wipe(
532
+ args.targets,
533
+ workspace=workspace,
534
+ unbind_from=args.unbind_from,
535
+ )
536
+ if args.json:
537
+ print(json.dumps(result.to_mapping(), indent=2))
538
+ elif result.count:
539
+ for report in result.reports:
540
+ print(f" {report.target}: removed {report.count}")
541
+ else:
542
+ print("Nothing to remove.")
543
+ return 0
544
+
545
+
546
+ def handle_build(args: argparse.Namespace) -> int:
547
+ """Adapt command-line values to :func:`weaver.build`."""
548
+
549
+ import json
550
+
551
+ workspace = _resolve_workspace(args)
552
+ result = weaver.build(
553
+ args.repository,
554
+ bind=args.item_bindings,
555
+ workspace=workspace,
556
+ bundle=args.bundle,
557
+ )
558
+ payload = result.to_mapping()
559
+ if args.json:
560
+ print(json.dumps(payload, indent=2))
561
+ else:
562
+ print(f"build {result.status}: workspace declaration")
563
+ print(f" bundle: {result.bundle_id}")
564
+ if result.archive:
565
+ print(f" record: {result.archive}")
566
+ print(f" items: {', '.join(result.items)}")
567
+ if result.errors:
568
+ for error in payload["errors"]:
569
+ print(f" failed: {error['id']}: {error['type']}: {error['message']}")
570
+ return 0 if result.succeeded else 1
571
+
572
+
573
+ def handle_doctor(args: argparse.Namespace) -> int:
574
+ """Report what a local build and load needs, and what is missing.
575
+
576
+ None of it is required to use Weaver on Fabric. It matters for local
577
+ development, where a missing JDK otherwise surfaces as a Java stack trace.
578
+ """
579
+
580
+ from weaver.diagnostics import check_local_spark, platform_summary
581
+
582
+ report = check_local_spark()
583
+
584
+ if args.json:
585
+ import json
586
+
587
+ print(json.dumps(report.as_dict(), indent=2))
588
+ return 0 if report.ok else 1
589
+
590
+ print(f"local Spark and Delta on {platform_summary()}\n")
591
+ for check in report.checks:
592
+ print(f" {check}")
593
+ if report.ok:
594
+ print("\nReady. Run the local tests with: pytest -m spark")
595
+ return 0
596
+ print()
597
+ for hint in report.hints:
598
+ print(f" → {hint}")
599
+ # A non-zero status keeps this usable as a gate in a script, but on its own
600
+ # it reads as "your installation is broken" to someone who never wanted
601
+ # local Spark. Say plainly that it is optional.
602
+ print(
603
+ "\nThis reports local Spark only. Weaver on Fabric needs none of it —\n"
604
+ "wipe, install and capacity work without a JVM."
605
+ )
606
+ return 1
607
+
608
+
609
+ def main(argv: list[str] | None = None) -> int:
610
+ parser = build_parser()
611
+ args = parser.parse_args(argv)
612
+
613
+ handler = getattr(args, "handler", None)
614
+ if handler is None:
615
+ parser.print_help()
616
+ return 0
617
+
618
+ try:
619
+ return int(handler(args))
620
+ except WeaverError as exc:
621
+ print(f"error: {exc}", file=sys.stderr)
622
+ return 1
623
+
624
+
625
+ if __name__ == "__main__": # pragma: no cover
626
+ raise SystemExit(main())