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
@@ -0,0 +1,585 @@
1
+ """Source preparation, state handover, bundle generation, and installation.
2
+
3
+ A repository source is independent of the target estate. Remote sources are
4
+ materialised once onto the current process's local filesystem; parsing and
5
+ request validation finish before target state is read. Native builds keep all
6
+ four stages in-environment. A desktop Fabric build serialises only ``BuildState``
7
+ out and hands only a completed archive back for execution.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import stat
13
+ import tempfile
14
+ import zipfile
15
+ from contextlib import contextmanager
16
+ from dataclasses import dataclass
17
+ from datetime import datetime, timezone
18
+ from pathlib import Path, PurePosixPath
19
+ from typing import Iterator, Mapping
20
+
21
+ from ..errors import BuildError
22
+ from ..locations import Location
23
+ from ..declaration.model import WeaverItemId, WeaverRepository
24
+ from ..declaration.repository import parse_item_repository
25
+ from ..store import LocalStore, Store
26
+ from .bundle import BuildBundle, load_bundle
27
+ from .installer import InstallationEnvironment, install_bundle
28
+ from .planner import generate_item_build_bundle
29
+ from .models import BuildPlan
30
+ from .report import InstallationReport
31
+ from .targets import ItemBindings, LakehouseBinding
32
+ from .targets import WAREHOUSE_TARGET
33
+ from .prune import (
34
+ TargetInventory,
35
+ read_lakehouse_inventory,
36
+ read_warehouse_inventory,
37
+ )
38
+ from ..catalogue.state import (
39
+ Catalogue,
40
+ Reconciliation,
41
+ read_catalogue_state,
42
+ reconcile_catalogue_state,
43
+ )
44
+
45
+ ARCHIVE_SUFFIX = ".weaver.zip"
46
+
47
+
48
+ @dataclass(frozen=True)
49
+ class MaterialisedTree:
50
+ """A source tree copied onto the current process's local filesystem."""
51
+
52
+ location: Location
53
+ store: LocalStore
54
+
55
+
56
+ @dataclass(frozen=True)
57
+ class PreparedRepository:
58
+ """A parsed repository and the process-local store that owns its files."""
59
+
60
+ repository: WeaverRepository
61
+ store: LocalStore
62
+
63
+
64
+ @dataclass(frozen=True)
65
+ class ItemBuildResult:
66
+ """Durable in-memory result of a temporary in-environment build."""
67
+
68
+ plan: BuildPlan
69
+ report: InstallationReport
70
+ repository_signature: str
71
+ item_signatures: Mapping[WeaverItemId, str]
72
+ archive: Location | None = None
73
+
74
+ @property
75
+ def bundle_id(self) -> str:
76
+ return self.plan.bundle_id
77
+
78
+
79
+ @dataclass(frozen=True)
80
+ class BuildState:
81
+ """Authoritative target state handed from Fabric to a local planner."""
82
+
83
+ catalogue: Catalogue
84
+ target_inventories: Mapping[WeaverItemId, TargetInventory]
85
+
86
+ def to_mapping(self) -> dict[str, object]:
87
+ return {
88
+ "format_version": 1,
89
+ "catalogue": self.catalogue.to_mapping(),
90
+ "target_inventories": [
91
+ {
92
+ "item": str(item),
93
+ "inventory": inventory.to_mapping(),
94
+ }
95
+ for item, inventory in sorted(
96
+ self.target_inventories.items(), key=lambda pair: str(pair[0])
97
+ )
98
+ ],
99
+ }
100
+
101
+ @classmethod
102
+ def from_mapping(cls, mapping) -> "BuildState":
103
+ version = mapping.get("format_version")
104
+ if version != 1:
105
+ raise BuildError(
106
+ f"unsupported build state format_version {version!r}; expected 1"
107
+ )
108
+ return cls(
109
+ catalogue=Catalogue.from_mapping(mapping["catalogue"]),
110
+ target_inventories={
111
+ WeaverItemId.parse(entry["item"]): TargetInventory.from_mapping(
112
+ entry["inventory"]
113
+ )
114
+ for entry in mapping.get("target_inventories", ())
115
+ },
116
+ )
117
+
118
+
119
+ def catalogue_items_for_build(
120
+ repository: WeaverRepository, bindings: ItemBindings
121
+ ) -> tuple[WeaverItemId, ...]:
122
+ """Catalogue scope needed for selection and cross-item alias freshness."""
123
+
124
+ bound = set(bindings.by_item)
125
+ items = bound | {
126
+ alias.source.item
127
+ for alias in repository.aliases
128
+ if alias.destination.item in bound and alias.source.item not in bound
129
+ }
130
+ return tuple(sorted(items, key=str))
131
+
132
+
133
+ def validate_build_request(
134
+ repository: WeaverRepository,
135
+ bindings: ItemBindings,
136
+ *,
137
+ control_lakehouse: LakehouseBinding,
138
+ ) -> tuple[WeaverItemId, ...]:
139
+ """Validate repository-dependent input before any target is contacted."""
140
+
141
+ if control_lakehouse is None:
142
+ raise BuildError("every build needs an explicit control-plane Lakehouse")
143
+ if not bindings.entries:
144
+ raise BuildError("at least one Weaver item must be bound")
145
+ known = {item.identity for item in repository.items}
146
+ unknown = set(bindings.by_item) - known
147
+ if unknown:
148
+ raise BuildError(
149
+ "binding names item(s) absent from the repository: "
150
+ + ", ".join(sorted(map(str, unknown)))
151
+ )
152
+ placed = {item for layer in repository.item_layers for item in layer}
153
+ missing = set(bindings.by_item) - placed
154
+ if missing:
155
+ raise BuildError(
156
+ "bound item(s) absent from the repository item graph: "
157
+ + ", ".join(sorted(map(str, missing)))
158
+ )
159
+ builtin = WeaverItemId.parse("Lakehouse/_weaver")
160
+ binding = bindings.by_item.get(builtin)
161
+ if binding is not None and not isinstance(binding.target, LakehouseBinding):
162
+ raise BuildError("Lakehouse/_weaver requires a Lakehouse binding")
163
+ if (
164
+ binding is not None
165
+ and binding.target.lakehouse.name != control_lakehouse.lakehouse.name
166
+ ):
167
+ raise BuildError(
168
+ "Lakehouse/_weaver must be bound to the explicit control-plane Lakehouse"
169
+ )
170
+ return catalogue_items_for_build(repository, bindings)
171
+
172
+
173
+ def read_build_state(
174
+ bindings: ItemBindings,
175
+ *,
176
+ required_catalogue_items,
177
+ environment: InstallationEnvironment,
178
+ sql_by_item=None,
179
+ ) -> BuildState:
180
+ """Read only the authoritative state a source-independent planner needs."""
181
+
182
+ inventories = read_target_inventories(
183
+ bindings, environment=environment, sql_by_item=sql_by_item
184
+ )
185
+ if environment.spark is None:
186
+ raise BuildError("every build needs Spark to read and publish the catalogue")
187
+ workspace = environment.workspace
188
+ if workspace is None or not workspace.weaver_lakehouse:
189
+ raise BuildError("every build needs a Workspace with a Weaver Lakehouse")
190
+ from ..spark import SparkCatalogue
191
+ from ..targets import ItemRef
192
+
193
+ catalogue = SparkCatalogue(
194
+ environment.spark,
195
+ environment.resolver.spark_destination(ItemRef(workspace.weaver_lakehouse)),
196
+ )
197
+ return BuildState(
198
+ catalogue=read_catalogue_state(catalogue, required_catalogue_items),
199
+ target_inventories=inventories,
200
+ )
201
+
202
+
203
+ @contextmanager
204
+ def materialise_tree(
205
+ source: Location,
206
+ *,
207
+ store: Store,
208
+ prefix: str = "weaver-source-",
209
+ ) -> Iterator[MaterialisedTree]:
210
+ """Copy a store tree once to a temporary local directory.
211
+
212
+ FabricStore uses one recursive ``notebookutils.fs.cp`` operation. A generic
213
+ Store falls back to one listing and exactly one read per file, which makes the
214
+ same contract testable without Fabric.
215
+ """
216
+
217
+ if not store.exists(source):
218
+ raise BuildError(f"source does not exist: {source.value}")
219
+ if not store.is_directory(source):
220
+ raise BuildError(f"source is not a directory: {source.value}")
221
+
222
+ with tempfile.TemporaryDirectory(prefix=prefix) as temporary:
223
+ destination = Path(temporary) / source.name
224
+ copier = getattr(store, "copy_to_local", None)
225
+ if callable(copier):
226
+ copier(source, destination)
227
+ else:
228
+ _copy_tree_through_store(source, store, destination)
229
+ if not destination.is_dir():
230
+ raise BuildError(
231
+ f"materialising {source.value} did not create {destination}"
232
+ )
233
+ yield MaterialisedTree(Location(destination.as_posix()), LocalStore())
234
+
235
+
236
+ @contextmanager
237
+ def prepare_repository(
238
+ source: Location,
239
+ *,
240
+ source_store: Store,
241
+ ) -> Iterator[PreparedRepository]:
242
+ """Make a repository process-local when needed, then parse it completely."""
243
+
244
+ with _local_tree(source, source_store, prefix="weaver-repository-") as root:
245
+ store = LocalStore()
246
+ repository = parse_item_repository(Location(root.as_posix()), store=store)
247
+ yield PreparedRepository(repository=repository, store=store)
248
+
249
+
250
+ def _copy_tree_through_store(source: Location, store: Store, destination: Path) -> None:
251
+ destination.mkdir(parents=True)
252
+ prefix = source.value.rstrip("/") + "/"
253
+ entries = store.list(source, recursive=True)
254
+ for entry in entries:
255
+ relative = entry.location.value[len(prefix) :]
256
+ target = destination.joinpath(*relative.split("/"))
257
+ if entry.is_directory:
258
+ target.mkdir(parents=True, exist_ok=True)
259
+ for entry in entries:
260
+ if entry.is_directory:
261
+ continue
262
+ relative = entry.location.value[len(prefix) :]
263
+ target = destination.joinpath(*relative.split("/"))
264
+ target.parent.mkdir(parents=True, exist_ok=True)
265
+ target.write_bytes(store.read(entry.location))
266
+
267
+
268
+ def timestamped_archive_name(at: datetime | None = None) -> str:
269
+ """A sortable, collision-resistant physical name for an optional record."""
270
+
271
+ at = at or datetime.now(timezone.utc)
272
+ if at.tzinfo is None:
273
+ at = at.replace(tzinfo=timezone.utc)
274
+ stamp = at.astimezone(timezone.utc).strftime("%Y%m%dT%H%M%S%fZ")
275
+ return f"{stamp}{ARCHIVE_SUFFIX}"
276
+
277
+
278
+ def persist_bundle_archive(
279
+ bundle: BuildBundle,
280
+ destination: Location,
281
+ *,
282
+ store: Store,
283
+ ) -> Location:
284
+ """Persist a complete bundle as one deterministic ZIP file."""
285
+
286
+ if not destination.name.endswith(ARCHIVE_SUFFIX):
287
+ raise BuildError(
288
+ f"bundle archive must end with {ARCHIVE_SUFFIX!r}: {destination.value}"
289
+ )
290
+ bundle_store = bundle.store or store
291
+ with _local_tree(bundle.location, bundle_store, prefix="weaver-bundle-source-") as root:
292
+ with tempfile.TemporaryDirectory(prefix="weaver-bundle-archive-") as temporary:
293
+ archive = Path(temporary) / destination.name
294
+ _write_archive(root, archive)
295
+ parent_value, separator, _ = destination.value.rpartition("/")
296
+ if separator:
297
+ parent = Location(parent_value)
298
+ if not store.exists(parent):
299
+ store.make_directory(parent)
300
+ copier = getattr(store, "copy_from_local", None)
301
+ if callable(copier):
302
+ copier(archive, destination)
303
+ else:
304
+ store.write(destination, archive.read_bytes())
305
+ return destination
306
+
307
+
308
+ @contextmanager
309
+ def materialise_bundle_archive(
310
+ archive: Location,
311
+ *,
312
+ store: Store,
313
+ ) -> Iterator[BuildBundle]:
314
+ """Copy one archive locally, extract safely, and load its validated bundle."""
315
+
316
+ if not archive.name.endswith(ARCHIVE_SUFFIX):
317
+ raise BuildError(f"not a Weaver bundle archive: {archive.value}")
318
+ with tempfile.TemporaryDirectory(prefix="weaver-bundle-install-") as temporary:
319
+ temporary_path = Path(temporary)
320
+ local_archive = temporary_path / archive.name
321
+ copier = getattr(store, "copy_to_local", None)
322
+ if callable(copier):
323
+ copier(archive, local_archive)
324
+ else:
325
+ local_archive.write_bytes(store.read(archive))
326
+ root = temporary_path / "bundle"
327
+ root.mkdir()
328
+ _extract_archive(local_archive, root)
329
+ local_store = LocalStore()
330
+ yield load_bundle(Location(root.as_posix()), store=local_store)
331
+
332
+
333
+ def install_bundle_archive(
334
+ archive: Location,
335
+ *,
336
+ archive_store: Store,
337
+ environment: InstallationEnvironment,
338
+ ) -> InstallationReport:
339
+ """Install a handover archive entirely from its temporary local extraction."""
340
+
341
+ with materialise_bundle_archive(archive, store=archive_store) as bundle:
342
+ return install_bundle(bundle, environment=environment)
343
+
344
+
345
+ def build_item_repository(
346
+ repository: WeaverRepository,
347
+ *,
348
+ bindings: ItemBindings,
349
+ target_inventories: Mapping[WeaverItemId, TargetInventory],
350
+ reconciliation: Reconciliation,
351
+ environment: InstallationEnvironment,
352
+ source_store: Store,
353
+ control_lakehouse: LakehouseBinding,
354
+ archive: Location | None = None,
355
+ archive_store: Store | None = None,
356
+ ) -> ItemBuildResult:
357
+ """Generate and install from already parsed source and already read state.
358
+
359
+ This is the planner/executor seam. It deliberately cannot materialise or
360
+ parse authored files, inspect a Workspace, or discover target state.
361
+ """
362
+
363
+ validate_build_request(
364
+ repository, bindings, control_lakehouse=control_lakehouse
365
+ )
366
+
367
+ with tempfile.TemporaryDirectory(prefix="weaver-build-") as temporary:
368
+ bundle = generate_item_build_bundle(
369
+ repository,
370
+ bindings=bindings,
371
+ output=Location((Path(temporary) / "bundle").as_posix()),
372
+ store=source_store,
373
+ target_inventories=target_inventories,
374
+ catalogue=reconciliation.catalogue,
375
+ stale_claims=reconciliation.stale_claims,
376
+ control_lakehouse=control_lakehouse,
377
+ )
378
+ report = install_bundle(bundle, environment=environment)
379
+ persisted = None
380
+ if archive is not None:
381
+ persisted = persist_bundle_archive(
382
+ bundle,
383
+ archive,
384
+ store=archive_store or environment.store,
385
+ )
386
+ return ItemBuildResult(
387
+ plan=bundle.plan,
388
+ report=report,
389
+ repository_signature=repository.signature,
390
+ item_signatures={item.identity: item.signature for item in repository.items},
391
+ archive=persisted,
392
+ )
393
+
394
+
395
+ def build_uploaded_item_repository(
396
+ repository_root: Location,
397
+ *,
398
+ bindings: ItemBindings,
399
+ environment: InstallationEnvironment,
400
+ control_lakehouse: LakehouseBinding,
401
+ archive: Location | None = None,
402
+ archive_store: Store | None = None,
403
+ sql_by_item=None,
404
+ ) -> ItemBuildResult:
405
+ """Compatibility wrapper for a repository stored with the target estate."""
406
+
407
+ return build_item_repository_source(
408
+ repository_root,
409
+ source_store=environment.store,
410
+ bindings=bindings,
411
+ environment=environment,
412
+ control_lakehouse=control_lakehouse,
413
+ archive=archive,
414
+ archive_store=archive_store,
415
+ sql_by_item=sql_by_item,
416
+ )
417
+
418
+
419
+ def build_item_repository_source(
420
+ source: Location,
421
+ *,
422
+ source_store: Store,
423
+ bindings: ItemBindings,
424
+ environment: InstallationEnvironment,
425
+ control_lakehouse: LakehouseBinding,
426
+ archive: Location | None = None,
427
+ archive_store: Store | None = None,
428
+ sql_by_item=None,
429
+ ) -> ItemBuildResult:
430
+ """Prepare an explicit source independently from the target, then build it."""
431
+
432
+ with prepare_repository(source, source_store=source_store) as prepared:
433
+ repository = prepared.repository
434
+ validate_build_request(
435
+ repository, bindings, control_lakehouse=control_lakehouse
436
+ )
437
+ inventories = read_target_inventories(
438
+ bindings, environment=environment, sql_by_item=sql_by_item
439
+ )
440
+ reconciled = read_reconciled_catalogue(
441
+ bindings,
442
+ inventories=inventories,
443
+ environment=environment,
444
+ repository=repository,
445
+ )
446
+ return build_item_repository(
447
+ repository,
448
+ bindings=bindings,
449
+ target_inventories=inventories,
450
+ reconciliation=reconciled,
451
+ environment=environment,
452
+ source_store=prepared.store,
453
+ control_lakehouse=control_lakehouse,
454
+ archive=archive,
455
+ archive_store=archive_store,
456
+ )
457
+
458
+
459
+ def read_reconciled_catalogue(
460
+ bindings: ItemBindings,
461
+ *,
462
+ inventories,
463
+ environment: InstallationEnvironment,
464
+ repository=None,
465
+ ) -> Reconciliation:
466
+ """Read the Weaver Lakehouse catalogue and prove selected claims physically.
467
+
468
+ The read covers the bound items and, when a ``repository`` is given, the
469
+ items that *produce* what those items alias. Those producers are not being
470
+ built and nothing about them will be written — but their Registry rows carry
471
+ the build that published them, and comparing that against the alias's own row
472
+ is the only way to learn that a producer moved on while this consumer was not
473
+ looking (see
474
+ :func:`~weaver.build_bundle.incremental.stale_alias_destinations`).
475
+
476
+ They are read without an inventory, so nothing about them is reconciled away:
477
+ a build has no business proving claims about a target it was not pointed at.
478
+ """
479
+
480
+ items = {binding.item for binding in bindings.entries}
481
+ if repository is not None:
482
+ items |= {
483
+ alias.source.item
484
+ for alias in repository.aliases
485
+ if alias.destination.item in items and alias.source.item not in items
486
+ }
487
+
488
+ if environment.spark is None:
489
+ raise BuildError("every build needs Spark to read and publish the catalogue")
490
+ workspace = environment.workspace
491
+ if workspace is None or not workspace.weaver_lakehouse:
492
+ raise BuildError("every build needs a Workspace with a Weaver Lakehouse")
493
+ from ..spark import SparkCatalogue
494
+ from ..targets import ItemRef
495
+
496
+ catalogue = SparkCatalogue(
497
+ environment.spark,
498
+ environment.resolver.spark_destination(ItemRef(workspace.weaver_lakehouse)),
499
+ )
500
+ state = read_catalogue_state(catalogue, sorted(items, key=str))
501
+ return reconcile_catalogue_state(state, inventories=inventories)
502
+
503
+
504
+ def read_target_inventories(
505
+ bindings: ItemBindings,
506
+ *,
507
+ environment: InstallationEnvironment,
508
+ sql_by_item=None,
509
+ ) -> dict:
510
+ """Read every selected physical target before planning begins."""
511
+
512
+ supplied_sql = sql_by_item or {}
513
+ inventories = {}
514
+ owned = []
515
+ try:
516
+ for binding in bindings.entries:
517
+ target = binding.to_bound_target()
518
+ if target.kind == WAREHOUSE_TARGET:
519
+ sql = supplied_sql.get(binding.item)
520
+ if sql is None:
521
+ if environment.workspace is None:
522
+ raise BuildError(
523
+ f"reading Warehouse inventory for {binding.item} needs a Workspace"
524
+ )
525
+ from ..fabric.sql import fabric_sql_executor
526
+ from ..targets import WarehouseTarget
527
+
528
+ sql = fabric_sql_executor(
529
+ WarehouseTarget.parse(target.item_id), environment.workspace
530
+ )
531
+ owned.append(sql)
532
+ inventories[binding.item] = read_warehouse_inventory(target, sql=sql)
533
+ else:
534
+ inventories[binding.item] = read_lakehouse_inventory(
535
+ target,
536
+ resolver=environment.resolver,
537
+ store=environment.store,
538
+ spark=environment.spark,
539
+ )
540
+ return inventories
541
+ finally:
542
+ for sql in owned:
543
+ if hasattr(sql, "close"):
544
+ sql.close()
545
+
546
+
547
+ @contextmanager
548
+ def _local_tree(
549
+ source: Location,
550
+ store: Store,
551
+ *,
552
+ prefix: str,
553
+ ) -> Iterator[Path]:
554
+ if isinstance(store, LocalStore) and not source.is_url:
555
+ yield source.path.resolve()
556
+ return
557
+ with materialise_tree(source, store=store, prefix=prefix) as tree:
558
+ yield tree.location.path
559
+
560
+
561
+ def _write_archive(root: Path, destination: Path) -> None:
562
+ with zipfile.ZipFile(
563
+ destination, mode="w", compression=zipfile.ZIP_DEFLATED, compresslevel=9
564
+ ) as zipped:
565
+ for path in sorted(candidate for candidate in root.rglob("*") if candidate.is_file()):
566
+ relative = path.relative_to(root).as_posix()
567
+ info = zipfile.ZipInfo(relative, date_time=(1980, 1, 1, 0, 0, 0))
568
+ info.compress_type = zipfile.ZIP_DEFLATED
569
+ info.external_attr = (stat.S_IFREG | 0o644) << 16
570
+ zipped.writestr(info, path.read_bytes(), compresslevel=9)
571
+
572
+
573
+ def _extract_archive(archive: Path, destination: Path) -> None:
574
+ with zipfile.ZipFile(archive) as zipped:
575
+ for info in zipped.infolist():
576
+ path = PurePosixPath(info.filename)
577
+ mode = info.external_attr >> 16
578
+ if (
579
+ path.is_absolute()
580
+ or not path.parts
581
+ or any(part in ("", ".", "..") for part in path.parts)
582
+ or stat.S_ISLNK(mode)
583
+ ):
584
+ raise BuildError(f"unsafe path in bundle archive: {info.filename!r}")
585
+ zipped.extractall(destination)
@@ -0,0 +1,73 @@
1
+ """Weaver's central catalogue, scoped by logical item.
2
+
3
+ An installation is identified by ``(item_type, item_name)`` and an object by
4
+ that item identity plus ``(schema_name, object_name)``. Files objects use
5
+ ``Files/<schema>`` as their catalogue schema, so the same four-part identity
6
+ covers them without a namespace column.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from .render import (
12
+ InstallationScope,
13
+ Row,
14
+ column_set,
15
+ identifier,
16
+ literal,
17
+ qualified_name,
18
+ render_delete_obsolete,
19
+ render_delete_scope,
20
+ render_merge,
21
+ sorted_rows,
22
+ typed_literal,
23
+ )
24
+ from .tables import (
25
+ ALIAS,
26
+ AUDIT_COLUMN_NAMES,
27
+ CATALOGUE_SCHEMA,
28
+ CATALOGUE_TABLES,
29
+ COLUMN_DICTIONARY,
30
+ DEPENDENCY,
31
+ DICTIONARY_TABLES,
32
+ FOLDER_DICTIONARY,
33
+ FOREIGN_KEY_DICTIONARY,
34
+ INDEX_DICTIONARY,
35
+ INSTALLATION,
36
+ ITEM_SCOPE_COLUMNS,
37
+ KEY_PRIMARY,
38
+ KEY_UNIQUE,
39
+ OBJECT_ROLES,
40
+ OBJECT_TYPES,
41
+ REGISTRY,
42
+ ROLE_DATA,
43
+ ROLE_LOAD,
44
+ SCHEMA_DICTIONARY,
45
+ SIGNATURE,
46
+ TABLE_DICTIONARY,
47
+ CatalogueColumn,
48
+ CatalogueTable,
49
+ table,
50
+ )
51
+ from .state import (
52
+ RegisteredDocument,
53
+ Catalogue,
54
+ Reconciliation,
55
+ for_targets,
56
+ retaining,
57
+ read_catalogue_state,
58
+ reconcile_catalogue_state,
59
+ )
60
+
61
+ __all__ = [
62
+ "ALIAS", "AUDIT_COLUMN_NAMES", "CATALOGUE_SCHEMA", "CATALOGUE_TABLES",
63
+ "COLUMN_DICTIONARY", "CatalogueColumn", "CatalogueTable", "DEPENDENCY",
64
+ "DICTIONARY_TABLES", "FOLDER_DICTIONARY", "FOREIGN_KEY_DICTIONARY",
65
+ "INDEX_DICTIONARY", "INSTALLATION", "ITEM_SCOPE_COLUMNS",
66
+ "InstallationScope", "KEY_PRIMARY", "KEY_UNIQUE",
67
+ "OBJECT_ROLES", "OBJECT_TYPES", "REGISTRY", "ROLE_DATA", "ROLE_LOAD", "Row",
68
+ "SCHEMA_DICTIONARY", "SIGNATURE", "TABLE_DICTIONARY", "column_set",
69
+ "identifier", "literal", "qualified_name", "render_delete_obsolete",
70
+ "render_delete_scope", "render_merge", "sorted_rows", "table", "typed_literal",
71
+ "RegisteredDocument", "Catalogue", "Reconciliation", "retaining", "for_targets",
72
+ "read_catalogue_state", "reconcile_catalogue_state",
73
+ ]