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/physical_wipe.py
ADDED
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
"""Internal mechanics for clearing a physical target.
|
|
2
|
+
|
|
3
|
+
Wipe is per target, because the three are different places with different
|
|
4
|
+
mechanics. It is also the bluntest thing Weaver does, so it reports what it
|
|
5
|
+
would remove before it removes anything.
|
|
6
|
+
|
|
7
|
+
**Delta.** Weaver addresses Delta tables by explicit path and never registers
|
|
8
|
+
them in a metastore, so there is no catalogue to consult and none to leave
|
|
9
|
+
dangling — a table is a directory, and wiping is removing it. That is the same
|
|
10
|
+
property that lets a Fabric notebook write to a Lakehouse it is not attached
|
|
11
|
+
to, showing up again as a simplification. On Fabric the Lakehouse auto-discovers
|
|
12
|
+
what appears under ``Tables/``, so removing the directory is expected to
|
|
13
|
+
de-register it; that is worth confirming against a real workspace before relying
|
|
14
|
+
on it.
|
|
15
|
+
|
|
16
|
+
**Shortcuts, first.** A shortcut is the one thing in a Lakehouse that is not the
|
|
17
|
+
Lakehouse's own data: it is a name this item holds for data another item owns. So
|
|
18
|
+
it cannot be removed by deleting a directory, and *must not be* — a recursive
|
|
19
|
+
delete of a directory holding one would be reaching through the pointer at the
|
|
20
|
+
producer's data, which is the one outcome a wipe of *this* Lakehouse must never
|
|
21
|
+
have. Shortcuts are therefore removed through the workspace, and removed before
|
|
22
|
+
any storage is swept, so the sweep can never meet one.
|
|
23
|
+
|
|
24
|
+
The local emulator materialises an alias as a symbolic link, and
|
|
25
|
+
:meth:`weaver.store.LocalStore.delete` unlinks a link rather than following it —
|
|
26
|
+
so the emulator reaches the same guarantee by a different mechanism, and neither
|
|
27
|
+
needs the other's.
|
|
28
|
+
|
|
29
|
+
**Folders.** The Files area is kept and its contents removed, so the target
|
|
30
|
+
survives and only what it held goes. Shortcuts under ``Files/`` go first, for the
|
|
31
|
+
same reason.
|
|
32
|
+
|
|
33
|
+
**Warehouse.** One dynamic statement enumerates and removes user objects in
|
|
34
|
+
dependency-safe order while preserving the Warehouse item and system schemas.
|
|
35
|
+
|
|
36
|
+
Nothing here is scoped to Weaver-managed objects: a wipe clears the target. That
|
|
37
|
+
suits a development loop, and makes the function something a CLI must gate
|
|
38
|
+
rather than something safe by default.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
from __future__ import annotations
|
|
42
|
+
|
|
43
|
+
from dataclasses import dataclass
|
|
44
|
+
from typing import Iterable
|
|
45
|
+
|
|
46
|
+
from .errors import CommandError
|
|
47
|
+
from .workspaces import Workspace, LocalWorkspace
|
|
48
|
+
from .locations import Location
|
|
49
|
+
from .resolution import TABLES_AREA, LocalResolver, resolver_for, store_for
|
|
50
|
+
from .store import LocalStore, Store
|
|
51
|
+
from .targets import FILES_AREA, DeltaTarget, FolderTarget, ItemRef, WarehouseTarget
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass(frozen=True)
|
|
55
|
+
class WipeReport:
|
|
56
|
+
"""What a wipe removed, or would remove."""
|
|
57
|
+
|
|
58
|
+
target: str
|
|
59
|
+
location: Location
|
|
60
|
+
removed: tuple[str, ...]
|
|
61
|
+
dry_run: bool = False
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def count(self) -> int:
|
|
65
|
+
return len(self.removed)
|
|
66
|
+
|
|
67
|
+
def __str__(self) -> str:
|
|
68
|
+
verb = "would remove" if self.dry_run else "removed"
|
|
69
|
+
return f"{self.target}: {verb} {self.count} from {self.location}"
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
#: Schemas a wipe empties but does not remove. A schema-enabled Fabric Lakehouse
|
|
73
|
+
#: is created holding ``dbo``; Fabric owns it, Weaver never manages it, and
|
|
74
|
+
#: nothing recreates it once its directory is gone — the Lakehouse is simply left
|
|
75
|
+
#: unable to resolve a schema it is supposed to have. Deleting it is therefore
|
|
76
|
+
#: not "clearing the target" but damaging it, which is a different act and not
|
|
77
|
+
#: one a wipe is asking for.
|
|
78
|
+
#:
|
|
79
|
+
#: This is the same judgement :data:`weaver.build_bundle.prune._RESERVED_SCHEMAS`
|
|
80
|
+
#: already makes for prune. The two agreeing is the point: an operator should not
|
|
81
|
+
#: have to know which of Weaver's destructive paths respects the default schema.
|
|
82
|
+
_KEPT_SCHEMAS = ("dbo",)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _guard(location: Location, root: Location) -> None:
|
|
86
|
+
"""Never remove anything outside the workspace root.
|
|
87
|
+
|
|
88
|
+
Locations are derived rather than supplied, so this should be unreachable —
|
|
89
|
+
which is exactly why it is worth having, since the failure it prevents is
|
|
90
|
+
unrecoverable.
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
inside = location.value == root.value or location.value.startswith(
|
|
94
|
+
root.value.rstrip("/") + "/"
|
|
95
|
+
)
|
|
96
|
+
if not inside:
|
|
97
|
+
raise CommandError(
|
|
98
|
+
f"refusing to wipe {location.value!r}: outside the workspace root {root.value!r}"
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _clear(
|
|
103
|
+
store: Store, location: Location, root: Location, *, dry_run: bool, keep=()
|
|
104
|
+
) -> tuple[str, ...]:
|
|
105
|
+
"""Remove the contents of a location, keeping the location itself.
|
|
106
|
+
|
|
107
|
+
``keep`` names entries the wipe passes over. It is not a scoping of what a
|
|
108
|
+
wipe is *for* — a wipe still clears the target — but a recognition that not
|
|
109
|
+
everything under an area belongs to the target. See :data:`_KEPT_SCHEMAS`.
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
_guard(location, root)
|
|
113
|
+
if not store.exists(location):
|
|
114
|
+
return ()
|
|
115
|
+
kept = {name.casefold() for name in keep}
|
|
116
|
+
entries = [
|
|
117
|
+
entry for entry in store.list(location) if entry.location.name.casefold() not in kept
|
|
118
|
+
]
|
|
119
|
+
removed = tuple(sorted(entry.location.name for entry in entries))
|
|
120
|
+
if not dry_run:
|
|
121
|
+
for entry in entries:
|
|
122
|
+
_guard(entry.location, root)
|
|
123
|
+
store.delete(entry.location, recursive=entry.is_directory)
|
|
124
|
+
return removed
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _remove_shortcuts(
|
|
128
|
+
resolver, lakehouse: ItemRef, *, prefix: str, dry_run: bool
|
|
129
|
+
) -> tuple[str, ...]:
|
|
130
|
+
"""Take away this Lakehouse's shortcuts beneath ``prefix``, before storage is swept.
|
|
131
|
+
|
|
132
|
+
Scoped by path prefix, so a wipe of one area leaves the other's pointers
|
|
133
|
+
alone — a Files wipe must not take away a shortcut under ``Tables/``.
|
|
134
|
+
|
|
135
|
+
Reported as ``shortcut:<path>/<name>`` so a dry run distinguishes a pointer
|
|
136
|
+
being taken away from a directory being deleted — they are not the same act,
|
|
137
|
+
and only one of them destroys data.
|
|
138
|
+
|
|
139
|
+
An environment with no shortcuts to remove offers no capability and this
|
|
140
|
+
answers nothing: the emulator's links are removed by the storage sweep, which
|
|
141
|
+
unlinks rather than follows.
|
|
142
|
+
"""
|
|
143
|
+
|
|
144
|
+
enumerate_shortcuts = getattr(resolver, "onelake_shortcuts", None)
|
|
145
|
+
remove = getattr(resolver, "remove_onelake_shortcut", None)
|
|
146
|
+
if enumerate_shortcuts is None or remove is None:
|
|
147
|
+
return ()
|
|
148
|
+
|
|
149
|
+
within = prefix.strip("/").casefold()
|
|
150
|
+
shortcuts = tuple(
|
|
151
|
+
shortcut
|
|
152
|
+
for shortcut in enumerate_shortcuts(lakehouse)
|
|
153
|
+
if shortcut.path.casefold() == within
|
|
154
|
+
or shortcut.path.casefold().startswith(within + "/")
|
|
155
|
+
)
|
|
156
|
+
if not dry_run:
|
|
157
|
+
for shortcut in shortcuts:
|
|
158
|
+
remove(lakehouse, path=shortcut.path, name=shortcut.name)
|
|
159
|
+
return tuple(f"shortcut:{shortcut.qualified}" for shortcut in shortcuts)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def wipe_folder_target(
|
|
163
|
+
target: FolderTarget,
|
|
164
|
+
workspace: Workspace,
|
|
165
|
+
*,
|
|
166
|
+
store: Store | None = None,
|
|
167
|
+
dry_run: bool = False,
|
|
168
|
+
) -> WipeReport:
|
|
169
|
+
"""Empty a folder target, keeping the Files area itself."""
|
|
170
|
+
|
|
171
|
+
store = store or store_for(workspace)
|
|
172
|
+
resolver = resolver_for(workspace)
|
|
173
|
+
location = resolver.folder_root(target)
|
|
174
|
+
shortcuts = _remove_shortcuts(
|
|
175
|
+
resolver, target.lakehouse, prefix=FILES_AREA, dry_run=dry_run
|
|
176
|
+
)
|
|
177
|
+
return WipeReport(
|
|
178
|
+
target=f"folder:{target}",
|
|
179
|
+
location=location,
|
|
180
|
+
removed=shortcuts + _clear(store, location, resolver.root, dry_run=dry_run),
|
|
181
|
+
dry_run=dry_run,
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def wipe_delta_target(
|
|
186
|
+
target: DeltaTarget,
|
|
187
|
+
workspace: Workspace,
|
|
188
|
+
*,
|
|
189
|
+
store: Store | None = None,
|
|
190
|
+
dry_run: bool = False,
|
|
191
|
+
) -> WipeReport:
|
|
192
|
+
"""Remove every Delta table in a Lakehouse, keeping the Tables area.
|
|
193
|
+
|
|
194
|
+
A table is a directory. There is no catalogue to enumerate from and none to
|
|
195
|
+
leave behind, because Weaver never registered one — with one exception, and it
|
|
196
|
+
is the reason shortcuts go first: a table shortcut is a directory whose bytes
|
|
197
|
+
belong to another item.
|
|
198
|
+
"""
|
|
199
|
+
|
|
200
|
+
store = store or store_for(workspace)
|
|
201
|
+
resolver = resolver_for(workspace)
|
|
202
|
+
location = resolver.tables_root(target.lakehouse)
|
|
203
|
+
shortcuts = _remove_shortcuts(
|
|
204
|
+
resolver, target.lakehouse, prefix=TABLES_AREA, dry_run=dry_run
|
|
205
|
+
)
|
|
206
|
+
return WipeReport(
|
|
207
|
+
target=f"delta:{target}",
|
|
208
|
+
location=location,
|
|
209
|
+
removed=shortcuts
|
|
210
|
+
+ _clear(
|
|
211
|
+
store, location, resolver.root, dry_run=dry_run, keep=_KEPT_SCHEMAS
|
|
212
|
+
),
|
|
213
|
+
dry_run=dry_run,
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def wipe_sql_target(
|
|
218
|
+
target: WarehouseTarget,
|
|
219
|
+
workspace: Workspace,
|
|
220
|
+
*,
|
|
221
|
+
sql=None,
|
|
222
|
+
) -> None:
|
|
223
|
+
"""Clear a Warehouse through the common SQL capability.
|
|
224
|
+
|
|
225
|
+
The default is deliberately Fabric-native. A desktop caller crossing into
|
|
226
|
+
Fabric constructs and injects ``desktop_sql_executor`` explicitly.
|
|
227
|
+
"""
|
|
228
|
+
|
|
229
|
+
from .sql import SqlError, SqlExecutionError, generate_warehouse_wipe_sql
|
|
230
|
+
|
|
231
|
+
owns_sql = sql is None
|
|
232
|
+
if sql is None:
|
|
233
|
+
from .fabric.sql import fabric_sql_executor
|
|
234
|
+
|
|
235
|
+
sql = fabric_sql_executor(target, workspace)
|
|
236
|
+
try:
|
|
237
|
+
sql.execute_script(generate_warehouse_wipe_sql())
|
|
238
|
+
except SqlError as exc:
|
|
239
|
+
raise SqlExecutionError(
|
|
240
|
+
f"failed to wipe Warehouse {target.warehouse.name!r}: {exc}"
|
|
241
|
+
) from exc
|
|
242
|
+
except Exception as exc:
|
|
243
|
+
raise SqlExecutionError(
|
|
244
|
+
f"failed to wipe Warehouse {target.warehouse.name!r}: {exc}"
|
|
245
|
+
) from exc
|
|
246
|
+
finally:
|
|
247
|
+
if owns_sql and hasattr(sql, "close"):
|
|
248
|
+
sql.close()
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def wipe(
|
|
252
|
+
workspace: Workspace,
|
|
253
|
+
*,
|
|
254
|
+
folder_target: FolderTarget | None = None,
|
|
255
|
+
delta_target: DeltaTarget | None = None,
|
|
256
|
+
sql_target: WarehouseTarget | None = None,
|
|
257
|
+
store: Store | None = None,
|
|
258
|
+
sql=None,
|
|
259
|
+
dry_run: bool = False,
|
|
260
|
+
) -> tuple[WipeReport, ...]:
|
|
261
|
+
"""Wipe each supplied target. At least one is required.
|
|
262
|
+
|
|
263
|
+
Targets are independently optional, so a development loop can clear the
|
|
264
|
+
Delta tables while leaving downloaded source files alone.
|
|
265
|
+
"""
|
|
266
|
+
|
|
267
|
+
if not any((folder_target, delta_target, sql_target)):
|
|
268
|
+
raise CommandError("wipe needs at least one target")
|
|
269
|
+
|
|
270
|
+
reports: list[WipeReport] = []
|
|
271
|
+
storage = store
|
|
272
|
+
if folder_target is not None:
|
|
273
|
+
storage = storage or store_for(workspace)
|
|
274
|
+
reports.append(
|
|
275
|
+
wipe_folder_target(folder_target, workspace, store=storage, dry_run=dry_run)
|
|
276
|
+
)
|
|
277
|
+
if delta_target is not None:
|
|
278
|
+
storage = storage or store_for(workspace)
|
|
279
|
+
reports.append(
|
|
280
|
+
wipe_delta_target(delta_target, workspace, store=storage, dry_run=dry_run)
|
|
281
|
+
)
|
|
282
|
+
if sql_target is not None:
|
|
283
|
+
if dry_run:
|
|
284
|
+
raise CommandError("Warehouse wipe does not support dry_run")
|
|
285
|
+
wipe_sql_target(sql_target, workspace, sql=sql)
|
|
286
|
+
return tuple(reports)
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def wipe_lakehouse(
|
|
290
|
+
lakehouse: ItemRef,
|
|
291
|
+
workspace: Workspace,
|
|
292
|
+
*,
|
|
293
|
+
store: Store | None = None,
|
|
294
|
+
dry_run: bool = False,
|
|
295
|
+
) -> tuple[WipeReport, ...]:
|
|
296
|
+
"""Clear both areas of a Lakehouse — its Files and its Tables.
|
|
297
|
+
|
|
298
|
+
The item is resolved *as a Lakehouse*, so there is no untyped "what is this
|
|
299
|
+
name?" discovery: a Warehouse of the same name resolves elsewhere and is not
|
|
300
|
+
reached here. A destructive operation must not depend on name inference.
|
|
301
|
+
"""
|
|
302
|
+
|
|
303
|
+
store = store or store_for(workspace)
|
|
304
|
+
resolver = resolver_for(workspace)
|
|
305
|
+
if not _lakehouse_exists(resolver, lakehouse):
|
|
306
|
+
raise CommandError(
|
|
307
|
+
f"no Lakehouse named {lakehouse.name!r} on this workspace — nothing to wipe"
|
|
308
|
+
)
|
|
309
|
+
return (
|
|
310
|
+
wipe_folder_target(
|
|
311
|
+
FolderTarget(lakehouse=lakehouse), workspace, store=store, dry_run=dry_run
|
|
312
|
+
),
|
|
313
|
+
wipe_delta_target(
|
|
314
|
+
DeltaTarget(lakehouse=lakehouse), workspace, store=store, dry_run=dry_run
|
|
315
|
+
),
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def _lakehouse_exists(resolver, lakehouse: ItemRef) -> bool:
|
|
320
|
+
"""Whether the Lakehouse is there, resolved as a Lakehouse.
|
|
321
|
+
|
|
322
|
+
Locally that is a directory check; on Fabric, resolving it as a Lakehouse
|
|
323
|
+
both proves it exists and refuses a same-named Warehouse.
|
|
324
|
+
"""
|
|
325
|
+
|
|
326
|
+
if hasattr(resolver, "lakehouse_exists"):
|
|
327
|
+
return resolver.lakehouse_exists(lakehouse)
|
|
328
|
+
from .errors import CommandError as _CommandError
|
|
329
|
+
|
|
330
|
+
try:
|
|
331
|
+
resolver.lakehouse(lakehouse)
|
|
332
|
+
return True
|
|
333
|
+
except _CommandError:
|
|
334
|
+
return False
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def wipe_selection(
|
|
338
|
+
selection: Iterable[str],
|
|
339
|
+
workspace: Workspace,
|
|
340
|
+
*,
|
|
341
|
+
store: Store | None = None,
|
|
342
|
+
dry_run: bool = False,
|
|
343
|
+
) -> tuple[WipeReport, ...]:
|
|
344
|
+
"""Wipe each named target, taking its type from its shape.
|
|
345
|
+
|
|
346
|
+
``Sales_LH`` is a **Lakehouse** and clears both its areas.
|
|
347
|
+
``Sales_LH/Files`` is that Lakehouse's Files area and clears only that. A bare
|
|
348
|
+
name is always a Lakehouse — a Warehouse must be wiped through a
|
|
349
|
+
:class:`~weaver.targets.WarehouseTarget`, never inferred from a name.
|
|
350
|
+
"""
|
|
351
|
+
|
|
352
|
+
names = list(selection)
|
|
353
|
+
if not names:
|
|
354
|
+
raise CommandError("wipe needs at least one target")
|
|
355
|
+
|
|
356
|
+
store = store or store_for(workspace)
|
|
357
|
+
reports: list[WipeReport] = []
|
|
358
|
+
for name in names:
|
|
359
|
+
if "/" in name:
|
|
360
|
+
reports.append(
|
|
361
|
+
wipe_folder_target(
|
|
362
|
+
FolderTarget.parse(name), workspace, store=store, dry_run=dry_run
|
|
363
|
+
)
|
|
364
|
+
)
|
|
365
|
+
else:
|
|
366
|
+
reports.extend(
|
|
367
|
+
wipe_lakehouse(ItemRef.parse(name), workspace, store=store, dry_run=dry_run)
|
|
368
|
+
)
|
|
369
|
+
return tuple(reports)
|
weaver/push.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Validate and replace the authored repository in a Weaver Lakehouse."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
from .declaration.repository import (
|
|
8
|
+
IGNORED_DIRECTORIES,
|
|
9
|
+
IGNORED_FILENAMES,
|
|
10
|
+
IGNORED_SUFFIXES,
|
|
11
|
+
parse_item_repository,
|
|
12
|
+
)
|
|
13
|
+
from .errors import CommandError
|
|
14
|
+
from .locations import Location
|
|
15
|
+
from .store import LocalStore, Store
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True)
|
|
19
|
+
class PushResult:
|
|
20
|
+
source: str
|
|
21
|
+
destination: str
|
|
22
|
+
repository_signature: str
|
|
23
|
+
files: tuple[str, ...]
|
|
24
|
+
|
|
25
|
+
def to_mapping(self) -> dict[str, object]:
|
|
26
|
+
return {
|
|
27
|
+
"source": self.source,
|
|
28
|
+
"destination": self.destination,
|
|
29
|
+
"repository_signature": self.repository_signature,
|
|
30
|
+
"files": list(self.files),
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def push_item_repository(
|
|
35
|
+
source: Location,
|
|
36
|
+
destination: Location,
|
|
37
|
+
*,
|
|
38
|
+
destination_store: Store,
|
|
39
|
+
source_store: Store | None = None,
|
|
40
|
+
) -> PushResult:
|
|
41
|
+
"""Validate ``source`` completely, then replace ``destination`` with it."""
|
|
42
|
+
|
|
43
|
+
source_store = source_store or LocalStore()
|
|
44
|
+
repository = parse_item_repository(source, store=source_store)
|
|
45
|
+
if source_store is destination_store and source == destination:
|
|
46
|
+
raise CommandError("push source and destination are the same repository")
|
|
47
|
+
|
|
48
|
+
prefix = source.value.rstrip("/") + "/"
|
|
49
|
+
files: list[tuple[str, bytes]] = []
|
|
50
|
+
for entry in source_store.list(source, recursive=True):
|
|
51
|
+
if entry.is_directory:
|
|
52
|
+
continue
|
|
53
|
+
relative = entry.location.value[len(prefix) :]
|
|
54
|
+
parts = relative.split("/")
|
|
55
|
+
if (
|
|
56
|
+
"_ignore" in parts
|
|
57
|
+
or any(part in IGNORED_DIRECTORIES for part in parts)
|
|
58
|
+
or parts[-1] in IGNORED_FILENAMES
|
|
59
|
+
or parts[-1].endswith(IGNORED_SUFFIXES)
|
|
60
|
+
):
|
|
61
|
+
continue
|
|
62
|
+
files.append((relative, source_store.read(entry.location)))
|
|
63
|
+
|
|
64
|
+
if destination_store.exists(destination):
|
|
65
|
+
destination_store.delete(destination, recursive=True)
|
|
66
|
+
destination_store.make_directory(destination)
|
|
67
|
+
for relative, content in sorted(files):
|
|
68
|
+
destination_store.write(
|
|
69
|
+
destination.join(*relative.split("/")), content
|
|
70
|
+
)
|
|
71
|
+
return PushResult(
|
|
72
|
+
source=source.value,
|
|
73
|
+
destination=destination.value,
|
|
74
|
+
repository_signature=repository.signature,
|
|
75
|
+
files=tuple(relative for relative, _content in sorted(files)),
|
|
76
|
+
)
|