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_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())
|