weaverstack 0.1.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. weaver/__init__.py +59 -0
  2. weaver/build_bundle/__init__.py +109 -0
  3. weaver/build_bundle/aliases.py +325 -0
  4. weaver/build_bundle/bundle.py +359 -0
  5. weaver/build_bundle/catalogue_actions.py +275 -0
  6. weaver/build_bundle/changes.py +186 -0
  7. weaver/build_bundle/endpoints.py +83 -0
  8. weaver/build_bundle/executors/__init__.py +69 -0
  9. weaver/build_bundle/executors/alias.py +202 -0
  10. weaver/build_bundle/executors/base.py +132 -0
  11. weaver/build_bundle/executors/folder.py +71 -0
  12. weaver/build_bundle/executors/load_file.py +205 -0
  13. weaver/build_bundle/executors/spark_case.py +26 -0
  14. weaver/build_bundle/executors/spark_schema.py +60 -0
  15. weaver/build_bundle/executors/spark_sql.py +59 -0
  16. weaver/build_bundle/executors/spark_sql_batch.py +57 -0
  17. weaver/build_bundle/executors/spark_table.py +213 -0
  18. weaver/build_bundle/executors/sql_endpoint_refresh.py +34 -0
  19. weaver/build_bundle/executors/tsql.py +81 -0
  20. weaver/build_bundle/incremental.py +288 -0
  21. weaver/build_bundle/installer.py +384 -0
  22. weaver/build_bundle/models.py +288 -0
  23. weaver/build_bundle/payloads.py +34 -0
  24. weaver/build_bundle/physical.py +625 -0
  25. weaver/build_bundle/planner.py +389 -0
  26. weaver/build_bundle/prune.py +620 -0
  27. weaver/build_bundle/report.py +108 -0
  28. weaver/build_bundle/stages.py +196 -0
  29. weaver/build_bundle/targets.py +272 -0
  30. weaver/build_bundle/workflow.py +585 -0
  31. weaver/catalogue/__init__.py +73 -0
  32. weaver/catalogue/builtin.py +238 -0
  33. weaver/catalogue/claims.py +121 -0
  34. weaver/catalogue/projection.py +437 -0
  35. weaver/catalogue/reader.py +152 -0
  36. weaver/catalogue/reconcile.py +231 -0
  37. weaver/catalogue/render.py +410 -0
  38. weaver/catalogue/state.py +660 -0
  39. weaver/catalogue/tables.py +648 -0
  40. weaver/config.py +178 -0
  41. weaver/declaration/__init__.py +171 -0
  42. weaver/declaration/columns.py +223 -0
  43. weaver/declaration/ddl.py +266 -0
  44. weaver/declaration/dependencies.py +544 -0
  45. weaver/declaration/graph.py +240 -0
  46. weaver/declaration/item_dependencies.py +292 -0
  47. weaver/declaration/load.py +191 -0
  48. weaver/declaration/metadata.py +1405 -0
  49. weaver/declaration/model.py +448 -0
  50. weaver/declaration/references.py +294 -0
  51. weaver/declaration/repository.py +959 -0
  52. weaver/declaration/schemas.py +135 -0
  53. weaver/declaration/source.py +674 -0
  54. weaver/declaration/spark_load.py +759 -0
  55. weaver/declaration/sql_shaping.py +591 -0
  56. weaver/declaration/templates/ddl/declared_create_table.sql +64 -0
  57. weaver/declaration/templates/ddl/infer_create_table.sql +97 -0
  58. weaver/declaration/templates/ddl/metadata_column_validation.sql +30 -0
  59. weaver/declaration/templates/load/column_metadata.sql +40 -0
  60. weaver/declaration/templates/load/full_replace_body.sql +21 -0
  61. weaver/declaration/templates/load/install_load_procedure.sql +27 -0
  62. weaver/declaration/templates/load/load_procedure.sql +48 -0
  63. weaver/declaration/templates/load/primary_key_body.sql +113 -0
  64. weaver/declaration/tsql_ddl.py +468 -0
  65. weaver/declaration/tsql_load.py +417 -0
  66. weaver/declaration/warehouse_type_mapping.yml +93 -0
  67. weaver/diagnostics.py +247 -0
  68. weaver/errors.py +61 -0
  69. weaver/etl.py +469 -0
  70. weaver/fabric/__init__.py +107 -0
  71. weaver/fabric/auth.py +137 -0
  72. weaver/fabric/capacity.py +143 -0
  73. weaver/fabric/client.py +147 -0
  74. weaver/fabric/environment.py +460 -0
  75. weaver/fabric/livy.py +478 -0
  76. weaver/fabric/notebooks.py +201 -0
  77. weaver/fabric/onelake.py +263 -0
  78. weaver/fabric/resolution.py +344 -0
  79. weaver/fabric/resources.py +245 -0
  80. weaver/fabric/session.py +148 -0
  81. weaver/fabric/shortcuts.py +120 -0
  82. weaver/fabric/sql.py +118 -0
  83. weaver/fabric/store.py +198 -0
  84. weaver/initialise.py +209 -0
  85. weaver/lakehouse.py +386 -0
  86. weaver/load.py +474 -0
  87. weaver/load_execution.py +483 -0
  88. weaver/load_plan.py +912 -0
  89. weaver/load_report.py +330 -0
  90. weaver/load_resolution.py +386 -0
  91. weaver/locations.py +164 -0
  92. weaver/objects.py +392 -0
  93. weaver/operations.py +757 -0
  94. weaver/physical_wipe.py +369 -0
  95. weaver/push.py +76 -0
  96. weaver/resolution.py +292 -0
  97. weaver/runtime/__init__.py +30 -0
  98. weaver/runtime/folder_load.py +402 -0
  99. weaver/runtime/load_contract.py +245 -0
  100. weaver/runtime/load_result.py +104 -0
  101. weaver/runtime/spark_load.py +152 -0
  102. weaver/runtime/table_load.py +497 -0
  103. weaver/spark/__init__.py +49 -0
  104. weaver/spark/catalogue.py +245 -0
  105. weaver/spark/destination.py +195 -0
  106. weaver/spark/session.py +84 -0
  107. weaver/spark/tokens.py +138 -0
  108. weaver/sql/__init__.py +40 -0
  109. weaver/sql/authentication.py +38 -0
  110. weaver/sql/connection.py +90 -0
  111. weaver/sql/errors.py +25 -0
  112. weaver/sql/execution.py +123 -0
  113. weaver/sql/pool.py +174 -0
  114. weaver/sql/wipe.py +156 -0
  115. weaver/store.py +209 -0
  116. weaver/targets.py +257 -0
  117. weaver/task_logging.py +215 -0
  118. weaver/unbind.py +74 -0
  119. weaver/workspaces.py +175 -0
  120. weaver_cli/__init__.py +12 -0
  121. weaver_cli/__main__.py +7 -0
  122. weaver_cli/main.py +626 -0
  123. weaverstack-0.1.1.dist-info/METADATA +113 -0
  124. weaverstack-0.1.1.dist-info/RECORD +127 -0
  125. weaverstack-0.1.1.dist-info/WHEEL +4 -0
  126. weaverstack-0.1.1.dist-info/entry_points.txt +2 -0
  127. weaverstack-0.1.1.dist-info/licenses/LICENSE +201 -0
weaver/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
+ ]