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,389 @@
1
+ """Orchestrate one item-oriented repository into a coordinated build bundle.
2
+
3
+ A build is planned as an ordered series of **item** builds. The repository owns
4
+ an acyclic item dependency graph and its topological layers; this walks those
5
+ layers and plans each item as one coherent group of stages:
6
+
7
+ .. code-block:: text
8
+
9
+ catalogue claim removal, when required
10
+
11
+ item layer 0
12
+ producer item A prune, drops, schemas, aliases, documents, refresh
13
+ independent producer B prune, drops, schemas, aliases, documents, refresh
14
+ item layer 1
15
+ consumer item C prune, drops, schemas, aliases, documents, refresh
16
+
17
+ final batched catalogue publication
18
+ Weaver Lakehouse SQL endpoint refresh
19
+
20
+ Items in the same layer share their barriers — one batch each — because nothing
21
+ orders them against each other. Items in different layers never do, which is the
22
+ one invariant multi-item build rests on: a consumer's aliases and documents
23
+ cannot begin until every item it reaches into has finished, endpoint included.
24
+
25
+ Inside an item the document dependency graph still decides everything. The item
26
+ graph is the outer boundary, not a replacement for it.
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ from dataclasses import dataclass, replace
32
+ from typing import Mapping
33
+
34
+ from ..catalogue.state import Catalogue
35
+ from ..declaration.model import WeaverItemId, WeaverRepository
36
+ from ..errors import BuildError
37
+ from ..locations import Location
38
+ from ..store import Store
39
+ from .aliases import plan_item_aliases
40
+ from .bundle import SUPPORTED_FORMAT_VERSION, BuildBundle, compute_bundle_id, write_bundle
41
+ from .catalogue_actions import (
42
+ render_catalogue_after_build,
43
+ render_catalogue_before_build,
44
+ )
45
+ from .endpoints import item_refresh_stage
46
+ from .incremental import select_build, stale_alias_destinations
47
+ from .models import OMIT_TARGET_UNBOUND, BuildPlan, OmittedNode
48
+ from ..etl import item_load_artefacts, load_artefacts, load_schemas
49
+ from .physical import (
50
+ item_build_stages,
51
+ item_load_removals,
52
+ item_load_stages,
53
+ item_drop_stages,
54
+ item_prune_stage,
55
+ item_schema_stage,
56
+ )
57
+ from .prune import TargetInventory
58
+ from .stages import PlannedStage, enumerate_stages, merge_layer_stages
59
+ from .targets import ItemBindings, LakehouseBinding
60
+
61
+
62
+ def generate_item_build_bundle(
63
+ repository: WeaverRepository,
64
+ *,
65
+ bindings: ItemBindings,
66
+ output: Location,
67
+ store: Store,
68
+ target_inventories: Mapping[WeaverItemId, TargetInventory] | None = None,
69
+ catalogue: Catalogue,
70
+ stale_claims: tuple = (),
71
+ control_lakehouse: LakehouseBinding,
72
+ ) -> BuildBundle:
73
+ """Freeze the one incremental build model into an installable bundle."""
74
+
75
+ if control_lakehouse is None:
76
+ raise BuildError("every build needs an explicit control-plane Lakehouse")
77
+ by_item = bindings.by_item
78
+ if not by_item:
79
+ raise BuildError("at least one Weaver item must be bound")
80
+ known = {item.identity for item in repository.items}
81
+ unknown = set(by_item) - known
82
+ if unknown:
83
+ raise BuildError(
84
+ "binding names item(s) absent from the repository: "
85
+ + ", ".join(sorted(map(str, unknown)))
86
+ )
87
+
88
+ # Three kinds of node are selectable, and most of what follows needs exactly
89
+ # one of them. Documents are what prune, schemas and the physical build
90
+ # pipelines are about; alias destinations are registered objects too — so
91
+ # they take part in selection and certification — but they are materialised
92
+ # by the alias executor rather than by any document stage. Load artefacts are
93
+ # the third, and are kept out of everything that assumes a selected identity
94
+ # maps to a parsed declaration: they are signed from their own content and
95
+ # installed by the item's final layer.
96
+ selected_documents = {
97
+ identity for identity in repository.source_documents if identity.item in by_item
98
+ }
99
+ selected_aliases = {
100
+ alias.destination
101
+ for alias in repository.aliases
102
+ if alias.destination.item in by_item
103
+ }
104
+ selected_loads = {
105
+ artefact.identity
106
+ for artefact in load_artefacts(repository)
107
+ if artefact.identity.item in by_item
108
+ }
109
+ selected_ids = selected_documents | selected_aliases | selected_loads
110
+
111
+ targets = tuple(
112
+ by_item[item].to_bound_target() for item in sorted(by_item, key=str)
113
+ )
114
+ target_by_item = {
115
+ item: by_item[item].to_bound_target() for item in sorted(by_item, key=str)
116
+ }
117
+ inventories = dict(target_inventories or {})
118
+ for item, target in target_by_item.items():
119
+ inventory = inventories.get(item)
120
+ if inventory is None:
121
+ raise BuildError(f"planning {item} requires a prepared target inventory")
122
+ if inventory.target_id != target.id:
123
+ raise BuildError(
124
+ f"inventory for {item} describes {inventory.target_id}, not {target.id}"
125
+ )
126
+
127
+ # Freshness is read before ``registered`` is narrowed, because the whole
128
+ # point is to compare against an item this build does *not* include.
129
+ stale_aliases = stale_alias_destinations(
130
+ repository, catalogue.registered, bound_items=by_item
131
+ )
132
+ registered = {
133
+ identity: document
134
+ for identity, document in catalogue.registered.items()
135
+ if identity.item in by_item
136
+ }
137
+ selection = select_build(
138
+ repository, registered, selected=selected_ids, stale_aliases=stale_aliases
139
+ )
140
+ selected_for_drop = set(selection.selected_for_drop)
141
+ selected_for_build = set(selection.selected_for_build)
142
+ removed = set(registered) - selected_ids
143
+
144
+ control_target = _control_target(control_lakehouse, targets)
145
+ if all(target.id != control_target.id for target in targets):
146
+ targets = targets + (control_target,)
147
+
148
+ stages: list[PlannedStage] = []
149
+ omitted: list[OmittedNode] = []
150
+
151
+ catalogue_before = render_catalogue_before_build(
152
+ catalogue,
153
+ removed | selected_for_drop,
154
+ control_target=control_target,
155
+ stale_claims=stale_claims,
156
+ )
157
+ if catalogue_before is not None:
158
+ stages.append(catalogue_before)
159
+
160
+ # Alias destinations this build wanted but could not materialise. They must
161
+ # not reach the Registry: a row there means the object's work succeeded, and
162
+ # for these no work was even planned.
163
+ uncertified: set = set()
164
+
165
+ for layer in _item_layers(repository, target_by_item):
166
+ layer_stages: list[PlannedStage] = []
167
+ for item in layer:
168
+ planned = plan_item_build(
169
+ repository,
170
+ item=item,
171
+ target=target_by_item[item],
172
+ inventory=inventories[item],
173
+ target_by_item=target_by_item,
174
+ selected_documents=selected_documents,
175
+ selected_aliases=selected_aliases,
176
+ selected_for_drop=selected_for_drop - selected_loads,
177
+ selected_for_build=selected_for_build - selected_loads,
178
+ selected_loads=selected_for_build & selected_loads,
179
+ removed=removed,
180
+ registered=registered,
181
+ )
182
+ layer_stages.extend(planned.stages)
183
+ omitted.extend(planned.omitted)
184
+ uncertified |= planned.uncertified
185
+ stages.extend(merge_layer_stages(layer_stages))
186
+
187
+ stages.extend(
188
+ render_catalogue_after_build(
189
+ repository,
190
+ selected_ids - uncertified,
191
+ target_by_item,
192
+ control_target=control_target,
193
+ # Passed for the *report* the diff can produce. The statements come
194
+ # from the desired side alone, so a bad read cannot change them.
195
+ current=catalogue,
196
+ )
197
+ )
198
+
199
+ sequences, payloads, target_changes = enumerate_stages(stages)
200
+
201
+ omitted.extend(
202
+ OmittedNode(
203
+ node_id=str(identity),
204
+ reason=OMIT_TARGET_UNBOUND,
205
+ detail=f"item {identity.item} is not bound",
206
+ )
207
+ for identity in sorted(repository.source_documents, key=str)
208
+ if identity not in selected_ids
209
+ )
210
+ plan = BuildPlan(
211
+ format_version=SUPPORTED_FORMAT_VERSION,
212
+ bundle_id="",
213
+ repository_name=repository.name,
214
+ repository_signature=repository.signature,
215
+ targets=targets,
216
+ sequences=sequences,
217
+ selection=selection,
218
+ omitted_nodes=tuple(sorted(omitted, key=lambda node: (node.node_id, node.reason))),
219
+ target_changes=target_changes,
220
+ )
221
+ plan = replace(plan, bundle_id=compute_bundle_id(plan))
222
+ return write_bundle(
223
+ output,
224
+ plan=plan,
225
+ payloads=payloads,
226
+ store=store,
227
+ )
228
+
229
+
230
+ def _item_layers(
231
+ repository: WeaverRepository,
232
+ target_by_item: Mapping[WeaverItemId, object],
233
+ ) -> tuple[tuple[WeaverItemId, ...], ...]:
234
+ """The bound items, grouped by their repository topological layer.
235
+
236
+ Selection is document-based and the bindings are sparse, so an unbound
237
+ producer simply drops out — but the items that remain keep the repository's
238
+ order rather than being re-derived here. That is the point of the repository
239
+ owning the graph: one authoritative ordering, consumed rather than rebuilt.
240
+ """
241
+
242
+ layers = repository.item_layers
243
+ if not layers:
244
+ raise BuildError(
245
+ f"repository {repository.name!r} carries no item dependency layers, so "
246
+ "the order its items must be built in is unknown"
247
+ )
248
+ placed = {item for layer in layers for item in layer}
249
+ missing = set(target_by_item) - placed
250
+ if missing:
251
+ raise BuildError(
252
+ "bound item(s) absent from the repository item graph: "
253
+ + ", ".join(sorted(map(str, missing)))
254
+ )
255
+ return tuple(
256
+ selected
257
+ for selected in (
258
+ tuple(item for item in layer if item in target_by_item) for layer in layers
259
+ )
260
+ if selected
261
+ )
262
+
263
+
264
+ @dataclass(frozen=True)
265
+ class PlannedItem:
266
+ """One item's physical plan: what to do, what was left out, what is uncertified."""
267
+
268
+ #: The item's contiguous stages, in the order they must run.
269
+ stages: tuple[PlannedStage, ...]
270
+ #: Nodes this item could not plan, each carrying why.
271
+ omitted: tuple[OmittedNode, ...]
272
+ #: Alias destinations this item could not materialise *and* was asked to
273
+ #: build. Withheld from certification: an alias whose source item is unbound
274
+ #: has no physical form under these bindings, and a Registry row for it would
275
+ #: claim an installation that never happened. One already installed from an
276
+ #: earlier build is left certified — it is still there — so only the
277
+ #: intersection with the build selection is withheld.
278
+ uncertified: frozenset
279
+
280
+
281
+ def plan_item_build(
282
+ repository: WeaverRepository,
283
+ *,
284
+ item: WeaverItemId,
285
+ target,
286
+ inventory: TargetInventory,
287
+ target_by_item,
288
+ selected_documents,
289
+ selected_aliases,
290
+ selected_for_drop,
291
+ selected_for_build,
292
+ registered,
293
+ selected_loads=(),
294
+ removed=(),
295
+ ) -> PlannedItem:
296
+ """One item's physical plan, from prepared inputs.
297
+
298
+ The seam between deciding *what* to build and arranging a whole bundle. It
299
+ takes a selection that has already been made and an inventory that has
300
+ already been read, and answers only: for this one item against this one
301
+ target, which stages run and in what order.
302
+
303
+ Everything above it stays out — item layers, catalogue publication, the
304
+ control-plane target, bundle identity, writing. So a claim about one item's
305
+ prune-then-drop-then-schema-then-build ordering can be made without
306
+ generating a bundle to see it, which is what kept such claims in the
307
+ integration suite.
308
+ """
309
+
310
+ aliases = plan_item_aliases(
311
+ repository,
312
+ item=item,
313
+ target=target,
314
+ target_by_item=target_by_item,
315
+ selected=selected_for_build & selected_aliases,
316
+ )
317
+ artefacts = item_load_artefacts(repository, item=item)
318
+ stages: list[PlannedStage] = []
319
+
320
+ # Prune is given every *declared* alias destination, never only the selected
321
+ # ones: an alias this build decided not to touch is still desired state, and
322
+ # a prune that could not see it would delete the very thing incremental
323
+ # selection just chose to keep. Load artefacts are treated the same way, and
324
+ # the stage derives them itself.
325
+ prune = item_prune_stage(
326
+ repository, selected_documents, item=item, target=target, inventory=inventory
327
+ )
328
+ if prune is not None:
329
+ stages.append(prune)
330
+ stages.extend(
331
+ item_drop_stages(
332
+ repository,
333
+ selected_for_drop - selected_aliases,
334
+ item=item,
335
+ target=target,
336
+ registered=registered,
337
+ )
338
+ )
339
+ schemas = item_schema_stage(
340
+ selected_documents,
341
+ item=item,
342
+ target=target,
343
+ inventory=inventory,
344
+ # `_` is where a Warehouse's generated load procedures live, and no
345
+ # document declares an object in it — so like an alias's namespace it
346
+ # would never be created if only documents were consulted. It is derived
347
+ # from the artefacts, so an item with no procedures asks for no schema.
348
+ extra_schemas=tuple(aliases.schemas) + load_schemas(artefacts),
349
+ )
350
+ if schemas is not None:
351
+ stages.append(schemas)
352
+ if aliases.stage is not None:
353
+ stages.append(aliases.stage)
354
+ stages.extend(
355
+ item_build_stages(
356
+ repository,
357
+ selected_for_build - selected_aliases,
358
+ item=item,
359
+ target=target,
360
+ )
361
+ )
362
+
363
+ refresh = item_refresh_stage(stages, item=item, target=target)
364
+ if refresh is not None:
365
+ stages.append(refresh)
366
+
367
+ # The load layer closes the item, after its structure is built and its
368
+ # endpoint has caught up. Removals ride in it too: they come from the
369
+ # previous Registry rows rather than from any diff against the target, so
370
+ # they need no earlier barrier to be safe.
371
+ stages.extend(
372
+ item_load_stages(artefacts, selected_loads, item=item, target=target)
373
+ )
374
+ stages.extend(
375
+ item_load_removals(removed, item=item, target=target, registered=registered)
376
+ )
377
+ return PlannedItem(
378
+ stages=tuple(stages),
379
+ omitted=aliases.omitted,
380
+ uncertified=frozenset(aliases.omitted_destinations) & frozenset(selected_for_build),
381
+ )
382
+
383
+
384
+ def _control_target(binding: LakehouseBinding, targets):
385
+ physical = binding.to_bound_target()
386
+ for target in targets:
387
+ if target.kind == physical.kind and target.item_id == physical.item_id:
388
+ return target
389
+ return replace(physical, id=f"control-{physical.id}")