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/etl.py
ADDED
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
"""What a repository's load layer owns, derived from the source alone.
|
|
2
|
+
|
|
3
|
+
A load artefact is not a side effect of building an object. It is a target in its
|
|
4
|
+
own right — claimed during interpretation, registered in the catalogue, signed,
|
|
5
|
+
selected incrementally, built by a physical action and pruned when its source
|
|
6
|
+
stops declaring it. This module answers the one question the rest of the build
|
|
7
|
+
asks about them: *given this repository, which load artefacts exist, where do
|
|
8
|
+
they go, and what is each one's signature.*
|
|
9
|
+
|
|
10
|
+
Three artefacts, from three kinds of source:
|
|
11
|
+
|
|
12
|
+
.. code-block:: text
|
|
13
|
+
|
|
14
|
+
Warehouse/Reporting/Sales__Customer.sql -> _.[Load Sales.Customer]
|
|
15
|
+
Lakehouse/Sales/lib/dates.py -> Files/_/Load/lib/dates.py
|
|
16
|
+
Lakehouse/Sales/Sales__Customer.sql -> Files/_/Load/Sales__Customer.sql
|
|
17
|
+
|
|
18
|
+
Views produce nothing on either side. A view's definition *is* its query, so
|
|
19
|
+
there is no work to schedule and nothing for a load to do.
|
|
20
|
+
|
|
21
|
+
**Nothing here inspects a target.** The current repository contents determine the
|
|
22
|
+
complete set of claims, which is what lets a deleted or renamed source produce an
|
|
23
|
+
ordinary prune through catalogue reconciliation rather than needing a scan to
|
|
24
|
+
notice its artefact has been orphaned.
|
|
25
|
+
|
|
26
|
+
**Nothing here generates a payload.** A T-SQL or Spark SQL load's bytes come
|
|
27
|
+
from :meth:`weaver.declaration.source.SourceDocument.create_load`, and deployed
|
|
28
|
+
Python is its own authored source. This module asks for a payload and carries
|
|
29
|
+
what it gets, so the question of *what a load does* stays with the generator
|
|
30
|
+
that knows the language, and the question of *which loads exist and how they are
|
|
31
|
+
signed* stays here. Each generated payload arrives with its generator's version,
|
|
32
|
+
which salts the signature — so a change to load generation invalidates exactly
|
|
33
|
+
the artefacts it changed, and leaves deployed Python untouched.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
from __future__ import annotations
|
|
37
|
+
|
|
38
|
+
import hashlib
|
|
39
|
+
from dataclasses import dataclass
|
|
40
|
+
from typing import Iterable, Mapping
|
|
41
|
+
|
|
42
|
+
from .declaration.metadata import PYTHON, SPARK_SQL, TABLE, ObjectId
|
|
43
|
+
from .declaration.model import (
|
|
44
|
+
FILE_SHAPE,
|
|
45
|
+
PROCEDURE_SHAPE,
|
|
46
|
+
WAREHOUSE,
|
|
47
|
+
WeaverDocumentId,
|
|
48
|
+
WeaverItemId,
|
|
49
|
+
WeaverRepository,
|
|
50
|
+
)
|
|
51
|
+
from .declaration.source import content_hash
|
|
52
|
+
|
|
53
|
+
#: Where generated infrastructure lives, in both physical forms. The Warehouse
|
|
54
|
+
#: gets a schema named ``_`` holding the load procedures; the Lakehouse gets a
|
|
55
|
+
#: managed folder ``Files/_/Load`` holding the deployed runtime tree.
|
|
56
|
+
#:
|
|
57
|
+
#: Same name, same principle, different physical object — and neither is a
|
|
58
|
+
#: reserved word. Both are projected as ordinary managed objects while the item
|
|
59
|
+
#: has load artefacts, so the ordinary inventory, keep-set and prune machinery
|
|
60
|
+
#: gives them their whole lifecycle, including removal once the last artefact
|
|
61
|
+
#: goes.
|
|
62
|
+
ETL_SCHEMA = "_"
|
|
63
|
+
LOAD_FOLDER = "Load"
|
|
64
|
+
|
|
65
|
+
#: The deployed runtime tree, relative to a Lakehouse's ``Files`` area. It is
|
|
66
|
+
#: also the Python import root the orchestrator will execute with, which is why
|
|
67
|
+
#: the authored tree is reproduced beneath it verbatim: ``from lib.dates import
|
|
68
|
+
#: parse_date`` keeps working because ``lib`` sits exactly where it was authored.
|
|
69
|
+
LOAD_ROOT = f"{ETL_SCHEMA}/{LOAD_FOLDER}"
|
|
70
|
+
|
|
71
|
+
#: What a generated load procedure is called: the object it loads, spelled out.
|
|
72
|
+
#: ``Load Sales.Customer`` is a real name in a real schema, not an encoding, so
|
|
73
|
+
#: the catalogue stores it exactly as the Warehouse holds it.
|
|
74
|
+
LOAD_PROCEDURE_PREFIX = "Load "
|
|
75
|
+
|
|
76
|
+
FILE_TYPE = "file"
|
|
77
|
+
PROCEDURE_TYPE = "stored_procedure"
|
|
78
|
+
|
|
79
|
+
PYTHON_SUFFIX = ".py"
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@dataclass(frozen=True)
|
|
83
|
+
class LoadArtefact:
|
|
84
|
+
"""One load target, complete: what it is, where it goes, what it contains.
|
|
85
|
+
|
|
86
|
+
Everything a build needs about a load artefact and nothing about how it was
|
|
87
|
+
reached. The identity is the catalogue key; the signature is what incremental
|
|
88
|
+
selection compares; the payload is the frozen bytes the installer is *given*
|
|
89
|
+
— deployed source for a Python object, and for a generated load an installer
|
|
90
|
+
script or an instruction the executor completes against the built target. An
|
|
91
|
+
artefact carries its own content either way, so the installer is never sent
|
|
92
|
+
back to a repository it must never reopen.
|
|
93
|
+
|
|
94
|
+
``origin`` is the declaration this artefact was derived from, where there was
|
|
95
|
+
one. A deployed helper module under ``lib/`` has none: it is authored source
|
|
96
|
+
that no Weaver document declares, which is exactly why it needs a claim of
|
|
97
|
+
its own or nothing would ever notice it had been deleted.
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
identity: WeaverDocumentId
|
|
101
|
+
object_type: str
|
|
102
|
+
signature: str
|
|
103
|
+
payload: bytes
|
|
104
|
+
origin: WeaverDocumentId | None = None
|
|
105
|
+
|
|
106
|
+
@property
|
|
107
|
+
def is_file(self) -> bool:
|
|
108
|
+
return self.object_type == FILE_TYPE
|
|
109
|
+
|
|
110
|
+
@property
|
|
111
|
+
def target_path(self) -> str:
|
|
112
|
+
"""Where a file artefact lands, relative to the Lakehouse ``Files`` area."""
|
|
113
|
+
|
|
114
|
+
if not self.is_file:
|
|
115
|
+
raise ValueError(f"{self.identity} is not a file and has no path")
|
|
116
|
+
return f"{self.identity.object_id.schema}/{self.identity.object_id.object}"
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def load_artefacts(repository: WeaverRepository) -> tuple[LoadArtefact, ...]:
|
|
120
|
+
"""Every load artefact the whole repository claims, in identity order."""
|
|
121
|
+
|
|
122
|
+
artefacts: list[LoadArtefact] = []
|
|
123
|
+
for model in repository.items:
|
|
124
|
+
artefacts.extend(item_load_artefacts(repository, item=model.identity))
|
|
125
|
+
return tuple(sorted(artefacts, key=lambda artefact: str(artefact.identity)))
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def item_load_artefacts(
|
|
129
|
+
repository: WeaverRepository, *, item: WeaverItemId
|
|
130
|
+
) -> tuple[LoadArtefact, ...]:
|
|
131
|
+
"""One item's load artefacts, derived from what it declares.
|
|
132
|
+
|
|
133
|
+
The built-in ``Lakehouse/_weaver`` owns none, and is excluded here rather
|
|
134
|
+
than filtered out downstream. It is Weaver's own control plane — the tables
|
|
135
|
+
that record what was installed — not a user ETL package, and letting its
|
|
136
|
+
claims through planning only to suppress them later would leave the question
|
|
137
|
+
"does the catalogue have a load layer?" answered in two places.
|
|
138
|
+
"""
|
|
139
|
+
|
|
140
|
+
if _is_builtin(item):
|
|
141
|
+
return ()
|
|
142
|
+
if item.item_type == WAREHOUSE:
|
|
143
|
+
return _warehouse_artefacts(repository, item=item)
|
|
144
|
+
return _lakehouse_artefacts(repository, item=item)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _warehouse_artefacts(
|
|
148
|
+
repository: WeaverRepository, *, item: WeaverItemId
|
|
149
|
+
) -> tuple[LoadArtefact, ...]:
|
|
150
|
+
"""One generated load procedure per Warehouse table."""
|
|
151
|
+
|
|
152
|
+
artefacts = []
|
|
153
|
+
for identity, source in sorted(repository.source_documents.items(), key=_by_text):
|
|
154
|
+
if identity.item != item or source.kind != TABLE:
|
|
155
|
+
continue
|
|
156
|
+
generated = source.create_load()
|
|
157
|
+
artefacts.append(
|
|
158
|
+
LoadArtefact(
|
|
159
|
+
identity=load_procedure_id(item, identity.object_id),
|
|
160
|
+
object_type=generated.object_type,
|
|
161
|
+
signature=_salted(
|
|
162
|
+
source.effective_signature, generated.template_version
|
|
163
|
+
),
|
|
164
|
+
payload=generated.payload,
|
|
165
|
+
origin=identity,
|
|
166
|
+
)
|
|
167
|
+
)
|
|
168
|
+
return tuple(artefacts)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _lakehouse_artefacts(
|
|
172
|
+
repository: WeaverRepository, *, item: WeaverItemId
|
|
173
|
+
) -> tuple[LoadArtefact, ...]:
|
|
174
|
+
"""The deployed Python tree, plus one generated file per Spark SQL table."""
|
|
175
|
+
|
|
176
|
+
artefacts = []
|
|
177
|
+
for identity, source in sorted(repository.source_documents.items(), key=_by_text):
|
|
178
|
+
if identity.item != item or source.relative_path in repository.generated_files:
|
|
179
|
+
continue
|
|
180
|
+
relative = _within_item(source.relative_path, item)
|
|
181
|
+
if source.language == PYTHON:
|
|
182
|
+
# A Python document authors a structural object *and* is runtime
|
|
183
|
+
# source. Both are true and they are separate targets: the table it
|
|
184
|
+
# declares, and the module a load will import.
|
|
185
|
+
artefacts.append(
|
|
186
|
+
_file_artefact(
|
|
187
|
+
item,
|
|
188
|
+
relative,
|
|
189
|
+
payload=source.text.encode("utf-8"),
|
|
190
|
+
signature=content_hash(source.text.encode("utf-8")),
|
|
191
|
+
origin=identity,
|
|
192
|
+
)
|
|
193
|
+
)
|
|
194
|
+
elif source.language == SPARK_SQL and source.kind == TABLE:
|
|
195
|
+
generated = source.create_load()
|
|
196
|
+
artefacts.append(
|
|
197
|
+
_file_artefact(
|
|
198
|
+
item,
|
|
199
|
+
relative,
|
|
200
|
+
payload=generated.payload,
|
|
201
|
+
signature=_salted(
|
|
202
|
+
source.effective_signature, generated.template_version
|
|
203
|
+
),
|
|
204
|
+
origin=identity,
|
|
205
|
+
)
|
|
206
|
+
)
|
|
207
|
+
for relative, content in sorted(repository.support_file_contents.items()):
|
|
208
|
+
parts = relative.split("/")
|
|
209
|
+
# Everything beneath ``lib/``, whatever it is. The tree is reproduced
|
|
210
|
+
# verbatim, so a helper module's data file travels with the module that
|
|
211
|
+
# reads it — an ``alias.yml`` beside it is a declaration rather than
|
|
212
|
+
# runtime source and stays behind.
|
|
213
|
+
if len(parts) < 4 or parts[2] != "lib":
|
|
214
|
+
continue
|
|
215
|
+
if WeaverItemId(parts[0], parts[1]) != item:
|
|
216
|
+
continue
|
|
217
|
+
artefacts.append(
|
|
218
|
+
_file_artefact(
|
|
219
|
+
item,
|
|
220
|
+
_within_item(relative, item),
|
|
221
|
+
payload=content,
|
|
222
|
+
signature=content_hash(content),
|
|
223
|
+
)
|
|
224
|
+
)
|
|
225
|
+
return tuple(artefacts)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _file_artefact(
|
|
229
|
+
item: WeaverItemId,
|
|
230
|
+
relative: str,
|
|
231
|
+
*,
|
|
232
|
+
payload: bytes,
|
|
233
|
+
signature: str,
|
|
234
|
+
origin: WeaverDocumentId | None = None,
|
|
235
|
+
) -> LoadArtefact:
|
|
236
|
+
"""One deployed file, at the item-relative path reproduced under the root.
|
|
237
|
+
|
|
238
|
+
The authored path is preserved whole, ``Files/`` segment included. That
|
|
239
|
+
segment is not noise: ``Sales__Customer.py`` at the item root and
|
|
240
|
+
``Files/Sales__Customer.py`` are legitimately different documents, and
|
|
241
|
+
flattening them would deploy two files to one path.
|
|
242
|
+
"""
|
|
243
|
+
|
|
244
|
+
path = f"{LOAD_ROOT}/{relative}"
|
|
245
|
+
directory, _, name = path.rpartition("/")
|
|
246
|
+
return LoadArtefact(
|
|
247
|
+
identity=WeaverDocumentId(
|
|
248
|
+
item, ObjectId(schema=directory, object=name), shape=FILE_SHAPE
|
|
249
|
+
),
|
|
250
|
+
object_type=FILE_TYPE,
|
|
251
|
+
signature=signature,
|
|
252
|
+
payload=payload,
|
|
253
|
+
origin=origin,
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def load_procedure_id(item: WeaverItemId, source: ObjectId) -> WeaverDocumentId:
|
|
258
|
+
"""The identity of the procedure that loads one Warehouse table.
|
|
259
|
+
|
|
260
|
+
``Load Sales.Customer`` is a real name in a real schema, not an encoding, so
|
|
261
|
+
the catalogue stores it exactly as the Warehouse holds it.
|
|
262
|
+
"""
|
|
263
|
+
|
|
264
|
+
return WeaverDocumentId(
|
|
265
|
+
item,
|
|
266
|
+
ObjectId(
|
|
267
|
+
schema=ETL_SCHEMA,
|
|
268
|
+
object=f"{LOAD_PROCEDURE_PREFIX}{source.qualified}",
|
|
269
|
+
),
|
|
270
|
+
shape=PROCEDURE_SHAPE,
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def load_procedure_name(source: ObjectId) -> str:
|
|
275
|
+
"""How the generated procedure spells its own name in T-SQL.
|
|
276
|
+
|
|
277
|
+
Derived from the same parts as :func:`load_procedure_id`, so the identity
|
|
278
|
+
the catalogue registers and the name the script creates cannot drift.
|
|
279
|
+
"""
|
|
280
|
+
|
|
281
|
+
schema = _tsql_ident(ETL_SCHEMA)
|
|
282
|
+
procedure = _tsql_ident(f"{LOAD_PROCEDURE_PREFIX}{source.qualified}")
|
|
283
|
+
return f"{schema}.{procedure}"
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _salted(signature: str, version: int) -> str:
|
|
287
|
+
"""A generated artefact's signature: what it is rendered from, and by what.
|
|
288
|
+
|
|
289
|
+
Both halves have to be in it. The document alone would leave every generated
|
|
290
|
+
body stale after the generator changed, with nothing to say so; the version
|
|
291
|
+
alone would rebuild the estate whenever anything at all was edited.
|
|
292
|
+
"""
|
|
293
|
+
|
|
294
|
+
digest = hashlib.sha256()
|
|
295
|
+
digest.update(signature.encode("ascii"))
|
|
296
|
+
digest.update(b"\0")
|
|
297
|
+
digest.update(str(version).encode("ascii"))
|
|
298
|
+
return digest.hexdigest()
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def _within_item(relative: str, item: WeaverItemId) -> str:
|
|
302
|
+
"""``Lakehouse/Sales/lib/dates.py`` -> ``lib/dates.py``."""
|
|
303
|
+
|
|
304
|
+
prefix = f"{item}/"
|
|
305
|
+
if not relative.startswith(prefix):
|
|
306
|
+
raise ValueError(f"{relative} does not belong to item {item}")
|
|
307
|
+
return relative[len(prefix) :]
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def _by_text(entry):
|
|
311
|
+
return str(entry[0])
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def _is_builtin(item: WeaverItemId) -> bool:
|
|
315
|
+
from .catalogue.builtin import ITEM_ROOT
|
|
316
|
+
|
|
317
|
+
return str(item) == ITEM_ROOT
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def _tsql_ident(name: str) -> str:
|
|
321
|
+
return "[" + name.replace("]", "]]") + "]"
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
#: The generated folder document that owns the deployed tree, and the schema
|
|
325
|
+
#: declaration it needs. ``_`` + ``__`` + ``Load`` spells ``___Load``, which the
|
|
326
|
+
#: parser reads as ``_.Load`` — see
|
|
327
|
+
#: :func:`weaver.declaration.source.python_id_parts`.
|
|
328
|
+
FOLDER_DOCUMENT = f"Files/{ETL_SCHEMA}{'__'}{LOAD_FOLDER}.py"
|
|
329
|
+
SCHEMA_DOCUMENT = f"schemas/{ETL_SCHEMA}.yml"
|
|
330
|
+
|
|
331
|
+
_FOLDER_CLASS = f"{ETL_SCHEMA}{'__'}{LOAD_FOLDER}"
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def render_folder_document() -> str:
|
|
335
|
+
"""The declaration for ``Files/_/Load``, generated per item that needs one.
|
|
336
|
+
|
|
337
|
+
An ordinary Folder document, deliberately. The deployed tree could have been
|
|
338
|
+
a reserved path the prune was taught to skip, but then its removal would need
|
|
339
|
+
a rule of its own; declared as a folder it is claimed, registered, inventoried
|
|
340
|
+
and pruned by the machinery that already exists, and when the last load
|
|
341
|
+
artefact goes the folder stops being projected and the whole subtree is
|
|
342
|
+
removed by ordinary folder prune.
|
|
343
|
+
|
|
344
|
+
``Static: true`` because nothing loads *into* it — a build writes the runtime
|
|
345
|
+
tree, and the files inside it are separately claimed objects of their own.
|
|
346
|
+
``Incremental: false`` for the same reason: a Folder accumulates by default,
|
|
347
|
+
and nothing here accumulates, since each deployed file is replaced whole by
|
|
348
|
+
the claim that owns it.
|
|
349
|
+
"""
|
|
350
|
+
|
|
351
|
+
return f'''\
|
|
352
|
+
"""
|
|
353
|
+
Folder ID: {ETL_SCHEMA}.{LOAD_FOLDER}
|
|
354
|
+
|
|
355
|
+
Description: The runtime tree Weaver deploys this item's load code into.
|
|
356
|
+
|
|
357
|
+
Lineage: Generated by Weaver from the item's own authored source.
|
|
358
|
+
|
|
359
|
+
File key: "**/*"
|
|
360
|
+
|
|
361
|
+
Incremental: false
|
|
362
|
+
|
|
363
|
+
Static: true
|
|
364
|
+
"""
|
|
365
|
+
from weaver import Folder
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
class {_FOLDER_CLASS}(Folder):
|
|
369
|
+
def read(self):
|
|
370
|
+
return self.staging_folder(), []
|
|
371
|
+
'''
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def render_schema_document() -> str:
|
|
375
|
+
"""The ``_`` schema declaration the generated folder document needs."""
|
|
376
|
+
|
|
377
|
+
return (
|
|
378
|
+
f"Schema ID: {ETL_SCHEMA}\n"
|
|
379
|
+
"\n"
|
|
380
|
+
"Description: Generated Weaver infrastructure — the runtime tree a "
|
|
381
|
+
"Lakehouse item's load code is deployed into, and the schema a "
|
|
382
|
+
"Warehouse item's generated load procedures live in.\n"
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def generated_item_files(
|
|
387
|
+
item: WeaverItemId, *, documents: Iterable, support_paths: Iterable[str]
|
|
388
|
+
) -> dict[str, bytes]:
|
|
389
|
+
"""The generated declarations one item needs, or nothing if it needs none.
|
|
390
|
+
|
|
391
|
+
Keyed by repository-relative path, so they compose with the built-in
|
|
392
|
+
catalogue item's generated files and are read by the same static readers as
|
|
393
|
+
authored content — no second parsing path, and no source mutated on disk.
|
|
394
|
+
|
|
395
|
+
Takes the item's documents rather than a repository because it runs *during*
|
|
396
|
+
interpretation: the folder document has to exist before the artefacts landing
|
|
397
|
+
inside it can be claimed, since it is what owns the directory they land in.
|
|
398
|
+
"""
|
|
399
|
+
|
|
400
|
+
if _is_builtin(item):
|
|
401
|
+
return {}
|
|
402
|
+
schema = {f"{item}/{SCHEMA_DOCUMENT}": render_schema_document().encode("utf-8")}
|
|
403
|
+
if item.item_type == WAREHOUSE:
|
|
404
|
+
# A Warehouse needs the schema its generated load procedures live in and
|
|
405
|
+
# nothing else — there is no Files area, so no runtime tree and no folder
|
|
406
|
+
# to own one.
|
|
407
|
+
documents = tuple(documents)
|
|
408
|
+
return schema if any(source.kind == TABLE for source in documents) else {}
|
|
409
|
+
if not has_deployable_source(item, documents=documents, support_paths=support_paths):
|
|
410
|
+
return {}
|
|
411
|
+
return {
|
|
412
|
+
**schema,
|
|
413
|
+
f"{item}/{FOLDER_DOCUMENT}": render_folder_document().encode("utf-8"),
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def has_deployable_source(
|
|
418
|
+
item: WeaverItemId, *, documents: Iterable, support_paths: Iterable[str]
|
|
419
|
+
) -> bool:
|
|
420
|
+
"""Whether this Lakehouse item has anything for a load layer to deploy."""
|
|
421
|
+
|
|
422
|
+
for source in documents:
|
|
423
|
+
if source.language == PYTHON:
|
|
424
|
+
return True
|
|
425
|
+
if source.language == SPARK_SQL and source.kind == TABLE:
|
|
426
|
+
return True
|
|
427
|
+
prefix = f"{item}/lib/"
|
|
428
|
+
return any(relative.startswith(prefix) for relative in support_paths)
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def load_schemas(artefacts: Iterable[LoadArtefact]) -> tuple[str, ...]:
|
|
432
|
+
"""The Warehouse schemas these artefacts need, which is ``_`` or nothing.
|
|
433
|
+
|
|
434
|
+
Derived rather than assumed, so an item with no procedures asks for no schema
|
|
435
|
+
and the ordinary schema prune can then remove one left behind.
|
|
436
|
+
"""
|
|
437
|
+
|
|
438
|
+
return tuple(
|
|
439
|
+
sorted(
|
|
440
|
+
{
|
|
441
|
+
artefact.identity.object_id.schema
|
|
442
|
+
for artefact in artefacts
|
|
443
|
+
if artefact.object_type == PROCEDURE_TYPE
|
|
444
|
+
}
|
|
445
|
+
)
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def load_artefacts_by_identity(
|
|
450
|
+
artefacts: Iterable[LoadArtefact],
|
|
451
|
+
) -> Mapping[WeaverDocumentId, LoadArtefact]:
|
|
452
|
+
return {artefact.identity: artefact for artefact in artefacts}
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
__all__ = [
|
|
456
|
+
"ETL_SCHEMA",
|
|
457
|
+
"FILE_TYPE",
|
|
458
|
+
"LOAD_FOLDER",
|
|
459
|
+
"LOAD_PROCEDURE_PREFIX",
|
|
460
|
+
"LOAD_ROOT",
|
|
461
|
+
"LoadArtefact",
|
|
462
|
+
"PROCEDURE_TYPE",
|
|
463
|
+
"item_load_artefacts",
|
|
464
|
+
"load_artefacts",
|
|
465
|
+
"load_artefacts_by_identity",
|
|
466
|
+
"load_schemas",
|
|
467
|
+
"load_procedure_id",
|
|
468
|
+
"load_procedure_name",
|
|
469
|
+
]
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""The Fabric substrate: authentication, capacity, and workspace resources.
|
|
2
|
+
|
|
3
|
+
Everything here is optional. The core imports without it, and a local workspace
|
|
4
|
+
never reaches it. Install with the ``fabric`` extra.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from .capacity import CapacityAction, CapacityError, capacity_command, run_capacity_action
|
|
10
|
+
from .client import FabricClient, FabricError
|
|
11
|
+
from .livy import (
|
|
12
|
+
LivyError,
|
|
13
|
+
LivySession,
|
|
14
|
+
LivySessionInfo,
|
|
15
|
+
StatementResult,
|
|
16
|
+
WorkspaceLivySession,
|
|
17
|
+
emit_source,
|
|
18
|
+
list_livy_sessions,
|
|
19
|
+
list_workspace_livy_sessions,
|
|
20
|
+
)
|
|
21
|
+
from .environment import (
|
|
22
|
+
InstallResult,
|
|
23
|
+
build_wheel,
|
|
24
|
+
find_or_create_environment,
|
|
25
|
+
install,
|
|
26
|
+
missing_from_environment,
|
|
27
|
+
)
|
|
28
|
+
from .resolution import FabricResolver
|
|
29
|
+
from .session import FabricSessionResolver
|
|
30
|
+
from .store import FabricStore
|
|
31
|
+
from .onelake import (
|
|
32
|
+
OneLakeDfsClient,
|
|
33
|
+
abfss_root,
|
|
34
|
+
lakehouse_artifact_segment,
|
|
35
|
+
onelake_url,
|
|
36
|
+
parse_onelake,
|
|
37
|
+
)
|
|
38
|
+
from .resources import (
|
|
39
|
+
LAKEHOUSE,
|
|
40
|
+
WAREHOUSE,
|
|
41
|
+
SQL_ENDPOINT,
|
|
42
|
+
Item,
|
|
43
|
+
ItemNotFoundError,
|
|
44
|
+
Workspace,
|
|
45
|
+
create_lakehouse,
|
|
46
|
+
create_warehouse,
|
|
47
|
+
delete_item,
|
|
48
|
+
find_item,
|
|
49
|
+
find_workspace,
|
|
50
|
+
list_items,
|
|
51
|
+
refresh_sql_endpoint_metadata,
|
|
52
|
+
)
|
|
53
|
+
from .sql import (
|
|
54
|
+
FABRIC_SQL_AUDIENCE,
|
|
55
|
+
desktop_sql_executor,
|
|
56
|
+
desktop_sql_pool,
|
|
57
|
+
fabric_sql_executor,
|
|
58
|
+
fabric_sql_pool,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
__all__ = [
|
|
62
|
+
"CapacityAction",
|
|
63
|
+
"CapacityError",
|
|
64
|
+
"capacity_command",
|
|
65
|
+
"run_capacity_action",
|
|
66
|
+
"FabricResolver",
|
|
67
|
+
"FabricSessionResolver",
|
|
68
|
+
"FabricStore",
|
|
69
|
+
"install",
|
|
70
|
+
"InstallResult",
|
|
71
|
+
"build_wheel",
|
|
72
|
+
"find_or_create_environment",
|
|
73
|
+
"missing_from_environment",
|
|
74
|
+
"LivySession",
|
|
75
|
+
"LivySessionInfo",
|
|
76
|
+
"WorkspaceLivySession",
|
|
77
|
+
"LivyError",
|
|
78
|
+
"StatementResult",
|
|
79
|
+
"emit_source",
|
|
80
|
+
"list_livy_sessions",
|
|
81
|
+
"list_workspace_livy_sessions",
|
|
82
|
+
"OneLakeDfsClient",
|
|
83
|
+
"abfss_root",
|
|
84
|
+
"onelake_url",
|
|
85
|
+
"lakehouse_artifact_segment",
|
|
86
|
+
"parse_onelake",
|
|
87
|
+
"FabricClient",
|
|
88
|
+
"FabricError",
|
|
89
|
+
"Workspace",
|
|
90
|
+
"Item",
|
|
91
|
+
"ItemNotFoundError",
|
|
92
|
+
"LAKEHOUSE",
|
|
93
|
+
"WAREHOUSE",
|
|
94
|
+
"SQL_ENDPOINT",
|
|
95
|
+
"find_workspace",
|
|
96
|
+
"find_item",
|
|
97
|
+
"list_items",
|
|
98
|
+
"refresh_sql_endpoint_metadata",
|
|
99
|
+
"create_lakehouse",
|
|
100
|
+
"create_warehouse",
|
|
101
|
+
"delete_item",
|
|
102
|
+
"desktop_sql_executor",
|
|
103
|
+
"desktop_sql_pool",
|
|
104
|
+
"fabric_sql_executor",
|
|
105
|
+
"fabric_sql_pool",
|
|
106
|
+
"FABRIC_SQL_AUDIENCE",
|
|
107
|
+
]
|