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/fabric/store.py
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"""Session-native storage for Weaver running inside Microsoft Fabric.
|
|
2
|
+
|
|
3
|
+
This is the within-workspace counterpart to :class:`OneLakeDfsClient`. It uses the
|
|
4
|
+
``notebookutils.fs`` object already present in a Fabric Spark session and never
|
|
5
|
+
authenticates back across the workspace boundary.
|
|
6
|
+
|
|
7
|
+
Directory operations came first, for wipe. Byte reads and writes came next, for
|
|
8
|
+
installing a build bundle in-session — the installer reads ``plan.yml`` and the
|
|
9
|
+
generated payloads from OneLake and writes an install report back. They go
|
|
10
|
+
through ``notebookutils.fs.head``/``put``, which exchange UTF-8 text: a build
|
|
11
|
+
bundle's manifest and payloads are UTF-8, so this is exact for them. Arbitrary
|
|
12
|
+
binary artefacts do not pass through those methods: recursive repository
|
|
13
|
+
materialisation and bundle-archive persistence use ``notebookutils.fs.cp``
|
|
14
|
+
between OneLake and the driver's ``file:/tmp`` filesystem.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
from ..errors import CommandError
|
|
23
|
+
from ..locations import Location
|
|
24
|
+
from ..store import Entry, StoreError
|
|
25
|
+
|
|
26
|
+
#: notebookutils.fs.head reads up to this many bytes. Bundle files are tiny; this
|
|
27
|
+
#: ceiling is only a guard against an unexpectedly large one.
|
|
28
|
+
_MAX_READ_BYTES = 256 * 1024 * 1024
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class FabricStore:
|
|
32
|
+
"""Within-workspace Fabric storage, backed by ``notebookutils.fs``."""
|
|
33
|
+
|
|
34
|
+
def __init__(self, fs: Any | None = None) -> None:
|
|
35
|
+
if fs is None:
|
|
36
|
+
try:
|
|
37
|
+
from notebookutils import fs as notebook_fs
|
|
38
|
+
except ImportError as exc:
|
|
39
|
+
raise CommandError(
|
|
40
|
+
"FabricStore is available only inside a Fabric session; "
|
|
41
|
+
"desktop access uses OneLakeDfsClient explicitly"
|
|
42
|
+
) from exc
|
|
43
|
+
fs = notebook_fs
|
|
44
|
+
self.fs = fs
|
|
45
|
+
|
|
46
|
+
@staticmethod
|
|
47
|
+
def _path(location: Location) -> str:
|
|
48
|
+
if not isinstance(location, Location):
|
|
49
|
+
raise CommandError(
|
|
50
|
+
f"store operations take a Location, got {type(location).__name__}"
|
|
51
|
+
)
|
|
52
|
+
if not location.value.startswith("abfss://"):
|
|
53
|
+
raise CommandError(
|
|
54
|
+
f"FabricStore needs an abfss location, got {location.value!r}"
|
|
55
|
+
)
|
|
56
|
+
return location.value
|
|
57
|
+
|
|
58
|
+
def exists(self, location: Location) -> bool:
|
|
59
|
+
return bool(self.fs.exists(self._path(location)))
|
|
60
|
+
|
|
61
|
+
def is_directory(self, location: Location) -> bool:
|
|
62
|
+
path = self._path(location)
|
|
63
|
+
if not self.fs.exists(path):
|
|
64
|
+
return False
|
|
65
|
+
parent, _, name = path.rstrip("/").rpartition("/")
|
|
66
|
+
if not parent:
|
|
67
|
+
return True
|
|
68
|
+
for info in self.fs.ls(parent):
|
|
69
|
+
info_path = str(getattr(info, "path", "")).rstrip("/")
|
|
70
|
+
info_name = str(getattr(info, "name", "")).rstrip("/")
|
|
71
|
+
if info_path == path.rstrip("/") or info_name == name:
|
|
72
|
+
return bool(getattr(info, "isDir", False))
|
|
73
|
+
return False
|
|
74
|
+
|
|
75
|
+
def list(self, location: Location, *, recursive: bool = False) -> list[Entry]:
|
|
76
|
+
path = self._path(location)
|
|
77
|
+
if not self.fs.exists(path):
|
|
78
|
+
raise StoreError(f"cannot list a location that does not exist: {path}")
|
|
79
|
+
|
|
80
|
+
entries = self._list_once(location)
|
|
81
|
+
if not recursive:
|
|
82
|
+
return entries
|
|
83
|
+
|
|
84
|
+
found = list(entries)
|
|
85
|
+
pending = [entry.location for entry in entries if entry.is_directory]
|
|
86
|
+
while pending:
|
|
87
|
+
directory = pending.pop()
|
|
88
|
+
children = self._list_once(directory)
|
|
89
|
+
found.extend(children)
|
|
90
|
+
pending.extend(
|
|
91
|
+
child.location for child in children if child.is_directory
|
|
92
|
+
)
|
|
93
|
+
return found
|
|
94
|
+
|
|
95
|
+
def _list_once(self, location: Location) -> list[Entry]:
|
|
96
|
+
entries = []
|
|
97
|
+
for info in self.fs.ls(self._path(location)):
|
|
98
|
+
name = str(getattr(info, "name", "")).rstrip("/")
|
|
99
|
+
raw_path = str(getattr(info, "path", "")).rstrip("/")
|
|
100
|
+
child = (
|
|
101
|
+
Location(raw_path)
|
|
102
|
+
if raw_path.startswith("abfss://")
|
|
103
|
+
else location / name
|
|
104
|
+
)
|
|
105
|
+
is_directory = bool(getattr(info, "isDir", False))
|
|
106
|
+
entries.append(
|
|
107
|
+
Entry(
|
|
108
|
+
location=child,
|
|
109
|
+
is_directory=is_directory,
|
|
110
|
+
size=None if is_directory else int(getattr(info, "size", 0)),
|
|
111
|
+
)
|
|
112
|
+
)
|
|
113
|
+
return entries
|
|
114
|
+
|
|
115
|
+
def read(self, location: Location) -> bytes:
|
|
116
|
+
"""The file's bytes, decoded as the UTF-8 text a bundle is made of."""
|
|
117
|
+
|
|
118
|
+
path = self._path(location)
|
|
119
|
+
if not self.fs.exists(path):
|
|
120
|
+
raise StoreError(f"cannot read a location that does not exist: {path}")
|
|
121
|
+
try:
|
|
122
|
+
text = self.fs.head(path, _MAX_READ_BYTES)
|
|
123
|
+
except Exception as exc: # notebookutils raises a bare Py4J error
|
|
124
|
+
raise StoreError(f"cannot read {location.value}: {exc}") from exc
|
|
125
|
+
return text.encode("utf-8")
|
|
126
|
+
|
|
127
|
+
def write(self, location: Location, data: bytes) -> None:
|
|
128
|
+
"""Write UTF-8 text (a bundle manifest, a payload, an install report)."""
|
|
129
|
+
|
|
130
|
+
path = self._path(location)
|
|
131
|
+
try:
|
|
132
|
+
self.fs.put(path, data.decode("utf-8"), True)
|
|
133
|
+
except Exception as exc:
|
|
134
|
+
raise StoreError(f"cannot write {location.value}: {exc}") from exc
|
|
135
|
+
|
|
136
|
+
def delete(self, location: Location, *, recursive: bool = False) -> None:
|
|
137
|
+
if not self.fs.rm(self._path(location), recurse=recursive):
|
|
138
|
+
raise StoreError(f"could not delete {location.value}")
|
|
139
|
+
|
|
140
|
+
def make_directory(self, location: Location) -> None:
|
|
141
|
+
if not self.fs.mkdirs(self._path(location)):
|
|
142
|
+
raise StoreError(f"could not create directory {location.value}")
|
|
143
|
+
|
|
144
|
+
def copy_to_local(self, source: Location, destination: Path) -> None:
|
|
145
|
+
"""Copy OneLake content to the driver and remove Hadoop checksum debris.
|
|
146
|
+
|
|
147
|
+
``notebookutils.fs.cp`` writes ``.filename.crc`` sidecars through
|
|
148
|
+
Hadoop's checked local filesystem. They are transport metadata, not
|
|
149
|
+
repository content. A same-shaped file that really exists in OneLake is
|
|
150
|
+
retained, so strict discovery still sees authored input exactly.
|
|
151
|
+
"""
|
|
152
|
+
|
|
153
|
+
source_is_directory = self.is_directory(source)
|
|
154
|
+
retained_checksums: set[str] = set()
|
|
155
|
+
if source_is_directory:
|
|
156
|
+
prefix = source.value.rstrip("/") + "/"
|
|
157
|
+
retained_checksums = {
|
|
158
|
+
entry.location.value[len(prefix) :]
|
|
159
|
+
for entry in self.list(source, recursive=True)
|
|
160
|
+
if not entry.is_directory and _is_checksum_sidecar(entry.name)
|
|
161
|
+
}
|
|
162
|
+
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
163
|
+
target = f"file:{destination.as_posix()}"
|
|
164
|
+
try:
|
|
165
|
+
copied = self.fs.cp(
|
|
166
|
+
self._path(source), target, source_is_directory
|
|
167
|
+
)
|
|
168
|
+
except Exception as exc:
|
|
169
|
+
raise StoreError(
|
|
170
|
+
f"cannot materialise {source.value} at {destination}: {exc}"
|
|
171
|
+
) from exc
|
|
172
|
+
if copied is False:
|
|
173
|
+
raise StoreError(f"could not materialise {source.value} at {destination}")
|
|
174
|
+
if source_is_directory:
|
|
175
|
+
for sidecar in destination.rglob(".*.crc"):
|
|
176
|
+
relative = sidecar.relative_to(destination).as_posix()
|
|
177
|
+
if relative not in retained_checksums:
|
|
178
|
+
sidecar.unlink()
|
|
179
|
+
else:
|
|
180
|
+
generated = destination.parent / f".{destination.name}.crc"
|
|
181
|
+
generated.unlink(missing_ok=True)
|
|
182
|
+
|
|
183
|
+
def copy_from_local(self, source: Path, destination: Location) -> None:
|
|
184
|
+
"""Copy one driver-local file or tree into OneLake without text decoding."""
|
|
185
|
+
|
|
186
|
+
origin = f"file:{source.as_posix()}"
|
|
187
|
+
try:
|
|
188
|
+
copied = self.fs.cp(origin, self._path(destination), source.is_dir())
|
|
189
|
+
except Exception as exc:
|
|
190
|
+
raise StoreError(
|
|
191
|
+
f"cannot persist {source} at {destination.value}: {exc}"
|
|
192
|
+
) from exc
|
|
193
|
+
if copied is False:
|
|
194
|
+
raise StoreError(f"could not persist {source} at {destination.value}")
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _is_checksum_sidecar(name: str) -> bool:
|
|
198
|
+
return name.startswith(".") and name.endswith(".crc")
|
weaver/initialise.py
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"""Bootstrapping the Weaver Lakehouse — Weaver installing its own control plane.
|
|
2
|
+
|
|
3
|
+
Initialisation composes the package-owned catalogue item into the parsed
|
|
4
|
+
repository in memory and builds it through the *ordinary* planner and installer. There is
|
|
5
|
+
deliberately no second "create the control tables" path: if the catalogue needed
|
|
6
|
+
privileged machinery to exist, the claim that a catalogue table is an ordinary
|
|
7
|
+
Weaver object would be false, and every later assumption resting on that claim
|
|
8
|
+
would be resting on nothing.
|
|
9
|
+
|
|
10
|
+
The bootstrap looks circular and is not. One bundle does the whole of it, because
|
|
11
|
+
the barriers already order it correctly:
|
|
12
|
+
|
|
13
|
+
.. code-block:: text
|
|
14
|
+
|
|
15
|
+
create schema `_` and the catalogue tables
|
|
16
|
+
publish dictionaries and Installation as one batch
|
|
17
|
+
certify them in Registry last
|
|
18
|
+
|
|
19
|
+
The catalogue's own DML runs after the tables it writes to exist, so no special
|
|
20
|
+
first-run mode is needed and generation reads nothing — the statements are
|
|
21
|
+
rendered from the projection and are correct against an absent catalogue as much
|
|
22
|
+
as a populated one.
|
|
23
|
+
|
|
24
|
+
The built-in ``_weaver`` item's inventory is scoped to the reserved ``_`` schema,
|
|
25
|
+
so the ordinary authoritative prune cannot touch application schemas that happen
|
|
26
|
+
to share the control Lakehouse.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
from dataclasses import dataclass
|
|
32
|
+
from pathlib import Path
|
|
33
|
+
import tempfile
|
|
34
|
+
from typing import Any
|
|
35
|
+
|
|
36
|
+
from .build_bundle.models import BuildPlan
|
|
37
|
+
from .build_bundle.installer import InstallationEnvironment, install_bundle
|
|
38
|
+
from .build_bundle.planner import generate_item_build_bundle
|
|
39
|
+
from .build_bundle.report import InstallationReport
|
|
40
|
+
from .build_bundle.targets import ItemBinding, ItemBindings, LakehouseBinding
|
|
41
|
+
from .build_bundle.workflow import read_reconciled_catalogue, read_target_inventories
|
|
42
|
+
from .catalogue.tables import CATALOGUE_TABLES
|
|
43
|
+
from .declaration import parse_item_repository
|
|
44
|
+
from .declaration.model import WeaverItemId
|
|
45
|
+
from .errors import CommandError
|
|
46
|
+
from .locations import Location
|
|
47
|
+
from .resolution import resolver_for
|
|
48
|
+
from .store import LocalStore, Store
|
|
49
|
+
from .targets import ItemRef
|
|
50
|
+
from .workspaces import FabricWorkspace, LocalWorkspace
|
|
51
|
+
|
|
52
|
+
#: The driver-local bundle directory used while initialisation runs. A fixed
|
|
53
|
+
#: name is enough because the containing temporary directory is per invocation.
|
|
54
|
+
INITIALISE_BUNDLE_NAME = "weaver-initialise"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass(frozen=True)
|
|
58
|
+
class InitialiseResult:
|
|
59
|
+
"""What initialisation did, in terms a caller can print or assert on."""
|
|
60
|
+
|
|
61
|
+
item: str
|
|
62
|
+
weaver_lakehouse: str
|
|
63
|
+
plan: BuildPlan
|
|
64
|
+
report: InstallationReport
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def succeeded(self) -> bool:
|
|
68
|
+
return self.report.status == "succeeded"
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def tables(self) -> tuple[str, ...]:
|
|
72
|
+
return tuple(table.qualified for table in CATALOGUE_TABLES)
|
|
73
|
+
|
|
74
|
+
def to_mapping(self) -> dict[str, Any]:
|
|
75
|
+
"""A plain structure, for a CLI to serialise. The CLI owns no semantics."""
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
"item": self.item,
|
|
79
|
+
"weaver_lakehouse": self.weaver_lakehouse,
|
|
80
|
+
"bundle_id": self.plan.bundle_id,
|
|
81
|
+
"status": self.report.status,
|
|
82
|
+
"tables": list(self.tables),
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@dataclass(frozen=True)
|
|
87
|
+
class PreparedWeaverLakehouse:
|
|
88
|
+
workspace: str
|
|
89
|
+
weaver_lakehouse: str
|
|
90
|
+
created: bool
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def prepare_weaver_lakehouse(
|
|
94
|
+
workspace,
|
|
95
|
+
*,
|
|
96
|
+
exists_ok: bool = False,
|
|
97
|
+
store: Store | None = None,
|
|
98
|
+
client=None,
|
|
99
|
+
) -> PreparedWeaverLakehouse:
|
|
100
|
+
"""Create the configured Weaver Lakehouse and its required Files areas."""
|
|
101
|
+
|
|
102
|
+
if not workspace.weaver_lakehouse:
|
|
103
|
+
raise CommandError("initialise requires a configured Weaver Lakehouse")
|
|
104
|
+
name = workspace.weaver_lakehouse
|
|
105
|
+
if isinstance(workspace, LocalWorkspace):
|
|
106
|
+
from .store import LocalStore
|
|
107
|
+
|
|
108
|
+
store = store or LocalStore()
|
|
109
|
+
resolver = resolver_for(workspace)
|
|
110
|
+
existed = store.exists(resolver.weaver_lakehouse)
|
|
111
|
+
if existed and not exists_ok:
|
|
112
|
+
raise CommandError(
|
|
113
|
+
f"Weaver Lakehouse {name!r} already exists; pass --exists-ok"
|
|
114
|
+
)
|
|
115
|
+
store.make_directory(resolver.files_root(ItemRef(name)))
|
|
116
|
+
store.make_directory(resolver.tables_root(ItemRef(name)))
|
|
117
|
+
return PreparedWeaverLakehouse(str(workspace.workspace), name, not existed)
|
|
118
|
+
|
|
119
|
+
if isinstance(workspace, FabricWorkspace):
|
|
120
|
+
from .fabric.resources import (
|
|
121
|
+
LAKEHOUSE,
|
|
122
|
+
ItemNotFoundError,
|
|
123
|
+
create_lakehouse,
|
|
124
|
+
find_item,
|
|
125
|
+
find_workspace,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
physical_workspace = find_workspace(workspace.workspace, client=client)
|
|
129
|
+
try:
|
|
130
|
+
find_item(physical_workspace, name, item_type=LAKEHOUSE, client=client)
|
|
131
|
+
except ItemNotFoundError:
|
|
132
|
+
create_lakehouse(physical_workspace, name, client=client)
|
|
133
|
+
created = True
|
|
134
|
+
else:
|
|
135
|
+
if not exists_ok:
|
|
136
|
+
raise CommandError(
|
|
137
|
+
f"Weaver Lakehouse {name!r} already exists; pass --exists-ok"
|
|
138
|
+
)
|
|
139
|
+
created = False
|
|
140
|
+
return PreparedWeaverLakehouse(workspace.workspace, name, created)
|
|
141
|
+
|
|
142
|
+
raise CommandError(f"unsupported Workspace type: {type(workspace).__name__}")
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def initialise_weaver_lakehouse(
|
|
146
|
+
*,
|
|
147
|
+
weaver_lakehouse: ItemRef,
|
|
148
|
+
workspace,
|
|
149
|
+
store: Store,
|
|
150
|
+
spark: Any = None,
|
|
151
|
+
output: Location | None = None,
|
|
152
|
+
) -> InitialiseResult:
|
|
153
|
+
"""Install Weaver's catalogue into the Weaver Lakehouse, through the normal build.
|
|
154
|
+
|
|
155
|
+
Idempotent to re-run in *shape*: the same package produces the same bundle, and
|
|
156
|
+
the catalogue's own reconciliation is a no-op when nothing changed.
|
|
157
|
+
|
|
158
|
+
An unchanged incremental plan emits no physical table work, so re-running
|
|
159
|
+
initialisation preserves existing catalogue rows while its catalogue tail
|
|
160
|
+
reconciles the built-in item.
|
|
161
|
+
"""
|
|
162
|
+
|
|
163
|
+
resolver = resolver_for(workspace)
|
|
164
|
+
control = LakehouseBinding(lakehouse=weaver_lakehouse)
|
|
165
|
+
bindings = ItemBindings(
|
|
166
|
+
(
|
|
167
|
+
ItemBinding(
|
|
168
|
+
WeaverItemId.parse("Lakehouse/_weaver"),
|
|
169
|
+
control,
|
|
170
|
+
),
|
|
171
|
+
)
|
|
172
|
+
)
|
|
173
|
+
environment = InstallationEnvironment(
|
|
174
|
+
store=store, resolver=resolver, spark=spark, workspace=workspace
|
|
175
|
+
)
|
|
176
|
+
with tempfile.TemporaryDirectory(prefix="weaver-initialise-") as temporary:
|
|
177
|
+
local_store = LocalStore()
|
|
178
|
+
repository_root = Path(temporary) / "repository"
|
|
179
|
+
repository_root.mkdir()
|
|
180
|
+
repository = parse_item_repository(
|
|
181
|
+
Location(repository_root.as_posix()), store=local_store
|
|
182
|
+
)
|
|
183
|
+
inventories = read_target_inventories(bindings, environment=environment)
|
|
184
|
+
reconciled = read_reconciled_catalogue(
|
|
185
|
+
bindings,
|
|
186
|
+
inventories=inventories,
|
|
187
|
+
environment=environment,
|
|
188
|
+
repository=repository,
|
|
189
|
+
)
|
|
190
|
+
bundle = generate_item_build_bundle(
|
|
191
|
+
repository,
|
|
192
|
+
bindings=bindings,
|
|
193
|
+
output=output
|
|
194
|
+
or Location((Path(temporary) / INITIALISE_BUNDLE_NAME).as_posix()),
|
|
195
|
+
store=local_store,
|
|
196
|
+
control_lakehouse=control,
|
|
197
|
+
target_inventories=inventories,
|
|
198
|
+
catalogue=reconciled.catalogue,
|
|
199
|
+
stale_claims=reconciled.stale_claims,
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
report = install_bundle(bundle, environment=environment)
|
|
203
|
+
|
|
204
|
+
return InitialiseResult(
|
|
205
|
+
item="Lakehouse/_weaver",
|
|
206
|
+
weaver_lakehouse=weaver_lakehouse.name,
|
|
207
|
+
plan=bundle.plan,
|
|
208
|
+
report=report,
|
|
209
|
+
)
|