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,294 @@
1
+ """Following a ``$Schema.Object`` documentation reference to its text.
2
+
3
+ A ``Description``, ``Lineage`` or column note is either literal prose or exactly
4
+ one reference (:class:`~weaver.ses.metadata.MetadataText`). A reference means
5
+ *the text over there is the text here* — the same sentence written once and
6
+ pointed at from everywhere it applies, so a correction lands in one place.
7
+
8
+ Resolving one is therefore a copy, and this module performs it:
9
+
10
+ .. code-block:: text
11
+
12
+ Description: $Sales.Order -> Sales.Order's description
13
+ Description: $Sales.Order[Order id] -> that column's note on Sales.Order
14
+
15
+ Pointers may chain — B points at A, C points at B — and the chain is followed to
16
+ the literal at its end. A **cycle** is an error: it can never produce text, so it
17
+ is a broken declaration rather than a missing one.
18
+
19
+ **A documentation reference is not a dependency.** A dependency binds in its
20
+ consumer's execution namespace, because that is what the SQL will bind to. A
21
+ reference is a logical pointer to *the object of that name*, and the interesting
22
+ case is precisely the cross-target one: the Warehouse ``Sales.Customer`` written
23
+ by a Lakehouse table of the same name, saying "I come from that Delta table".
24
+ Resolution therefore excludes the referring object itself and prefers the
25
+ referrer's own namespace only to break a tie.
26
+
27
+ Unresolved is **not** an error. A reference may legitimately name an object in
28
+ another repository, and refusing it would cost someone a working object for a
29
+ documentation nicety. The reference is recorded as written and the resolved text
30
+ is simply absent — the catalogue keeps both columns for exactly this reason.
31
+ """
32
+
33
+ from __future__ import annotations
34
+
35
+ from dataclasses import dataclass
36
+ from typing import Iterable, Mapping
37
+
38
+ from ..errors import DiscoveryError
39
+ from .metadata import MetadataText, Reference
40
+ from .model import RepositoryAlias, WeaverDocumentId, WeaverItemId
41
+ from .source import SourceDocument
42
+
43
+ #: The note the catalogue gives Weaver's own surrogate column, which no author
44
+ #: writes and every table with an ``Identity`` header has.
45
+ IDENTITY_COLUMN_NOTE = (
46
+ "Weaver-managed surrogate key. Created by build as a not-null bigint and "
47
+ "populated by load; not part of the declared business schema."
48
+ )
49
+
50
+
51
+ @dataclass(frozen=True)
52
+ class ResolvedText:
53
+ """One piece of metadata, split into its text and where the text came from.
54
+
55
+ ``literal`` is the prose — written here, or copied from the end of a
56
+ reference chain, or ``None`` when a reference could not be followed.
57
+ ``reference`` is the ``$Schema.Object[Column]`` as written, or ``None`` when
58
+ the prose was written here.
59
+ """
60
+
61
+ literal: str | None = None
62
+ reference: str | None = None
63
+
64
+ @property
65
+ def is_reference(self) -> bool:
66
+ return self.reference is not None
67
+
68
+
69
+ def resolve_text(
70
+ text: MetadataText | None,
71
+ *,
72
+ owner: SourceDocument,
73
+ documents: Iterable[SourceDocument],
74
+ aliases: Iterable[RepositoryAlias] = (),
75
+ ) -> ResolvedText:
76
+ """Follow one piece of metadata to its literal prose.
77
+
78
+ ``documents`` is every object in the repository — resolution needs siblings.
79
+ Raises :class:`~weaver.errors.DiscoveryError` when the chain cycles.
80
+ """
81
+
82
+ if text is None:
83
+ return ResolvedText()
84
+ if not text.is_reference:
85
+ return ResolvedText(literal=text.literal)
86
+
87
+ index = _index(documents)
88
+ alias_index = {str(alias.destination): str(alias.source) for alias in aliases}
89
+ written = str(text.reference)
90
+ literal = _follow(
91
+ text.reference,
92
+ owner,
93
+ index,
94
+ alias_index=alias_index,
95
+ seen=[(owner.node_id, written)],
96
+ )
97
+ return ResolvedText(literal=literal, reference=written)
98
+
99
+
100
+ def _index(documents: Iterable[SourceDocument]) -> Mapping[str, list[SourceDocument]]:
101
+ grouped: dict[str, list[SourceDocument]] = {}
102
+ for document in documents:
103
+ key = str(document.logical_id) if document.logical_id is not None else document.qualified.lower()
104
+ grouped.setdefault(key, []).append(document)
105
+ return grouped
106
+
107
+
108
+ def _follow(
109
+ reference: Reference,
110
+ referrer: SourceDocument,
111
+ index: Mapping[str, list[SourceDocument]],
112
+ *,
113
+ alias_index: Mapping[str, str],
114
+ seen: list[tuple[str, str]],
115
+ ) -> str | None:
116
+ """The literal at the end of a chain, or None when it cannot be followed."""
117
+
118
+ target = _target(reference, referrer, index, alias_index=alias_index)
119
+ if target is None:
120
+ if referrer.logical_id is not None:
121
+ raise DiscoveryError(
122
+ f"{referrer.node_id}: metadata reference {reference} does not resolve exactly"
123
+ )
124
+ return None
125
+
126
+ step = (target.node_id, reference.column or "")
127
+ if step in seen:
128
+ trail = " -> ".join(f"{node}[{column}]" if column else node for node, column in seen)
129
+ raise DiscoveryError(
130
+ f"metadata reference cycle: {trail} -> {target.node_id} — a reference "
131
+ "copies text from its target, so a cycle has no text to copy"
132
+ )
133
+
134
+ text = _text_of(target, reference.column)
135
+ if text is None:
136
+ if referrer.logical_id is not None:
137
+ suffix = f" column {reference.column!r}" if reference.column else " text"
138
+ raise DiscoveryError(
139
+ f"{referrer.node_id}: metadata reference {reference} names no{suffix}"
140
+ )
141
+ return None
142
+ if not text.is_reference:
143
+ return text.literal
144
+ return _follow(
145
+ text.reference,
146
+ target,
147
+ index,
148
+ alias_index=alias_index,
149
+ seen=seen + [step],
150
+ )
151
+
152
+
153
+ def _target(
154
+ reference: Reference,
155
+ referrer: SourceDocument,
156
+ index: Mapping[str, list[SourceDocument]],
157
+ *,
158
+ alias_index: Mapping[str, str],
159
+ ) -> SourceDocument | None:
160
+ """The object a documentation reference names, excluding the referrer itself.
161
+
162
+ Excluding self is what makes the cross-target case work: the Warehouse
163
+ ``Sales.Customer`` naming ``$Sales.Customer`` means the Delta table, because
164
+ it cannot sensibly mean itself. With more than one candidate left, the
165
+ referrer's own namespace wins; failing that the reference is ambiguous and is
166
+ left unresolved rather than guessed.
167
+ """
168
+
169
+ if referrer.logical_id is not None:
170
+ item = (
171
+ WeaverItemId(reference.item_type, reference.item_name)
172
+ if reference.is_item_qualified
173
+ else referrer.logical_id.item
174
+ )
175
+ identity = WeaverDocumentId(item, reference.object_id, is_files=reference.is_files)
176
+ key = str(identity)
177
+ key = alias_index.get(key, key)
178
+ candidates = index.get(key, [])
179
+ return candidates[0] if len(candidates) == 1 else None
180
+
181
+ candidates = [
182
+ candidate
183
+ for candidate in index.get(reference.object_id.qualified.lower(), [])
184
+ if candidate.node_id != referrer.node_id
185
+ ]
186
+ if len(candidates) == 1:
187
+ return candidates[0]
188
+ if not candidates:
189
+ return None
190
+ same_namespace = [
191
+ candidate for candidate in candidates if candidate.namespace == referrer.namespace
192
+ ]
193
+ return same_namespace[0] if len(same_namespace) == 1 else None
194
+
195
+
196
+ def validate_repository_metadata(
197
+ documents: Iterable[SourceDocument],
198
+ *,
199
+ aliases: Iterable[RepositoryAlias] = (),
200
+ ) -> None:
201
+ """Eagerly validate every logical metadata pointer in an item repository."""
202
+
203
+ documents = tuple(documents)
204
+ aliases = tuple(aliases)
205
+ index = _index(documents)
206
+ alias_index = {str(alias.destination): str(alias.source) for alias in aliases}
207
+ for source in documents:
208
+ resolve_text(
209
+ source.document.description,
210
+ owner=source,
211
+ documents=documents,
212
+ aliases=aliases,
213
+ )
214
+ resolve_text(
215
+ source.document.lineage,
216
+ owner=source,
217
+ documents=documents,
218
+ aliases=aliases,
219
+ )
220
+ for _column, note in declared_column_notes(source):
221
+ resolve_text(note, owner=source, documents=documents, aliases=aliases)
222
+ for foreign_key in source.document.foreign_keys:
223
+ reference = foreign_key.logical_reference or Reference(
224
+ schema=foreign_key.reference.schema,
225
+ object=foreign_key.reference.object,
226
+ )
227
+ target = _target(reference, source, index, alias_index=alias_index)
228
+ if target is None:
229
+ raise DiscoveryError(
230
+ f"{source.node_id}: foreign key target {reference.target} "
231
+ "does not resolve exactly"
232
+ )
233
+
234
+
235
+ def _text_of(document: SourceDocument, column: str | None) -> MetadataText | None:
236
+ """The referenced text on a target: its description, or one column's note."""
237
+
238
+ if column is None:
239
+ return document.document.description
240
+ return column_note(document, column)
241
+
242
+
243
+ def column_note(document: SourceDocument, column: str) -> MetadataText | None:
244
+ """One column's declared note, however the object declares its shape.
245
+
246
+ A declared schema carries notes on its columns. An inferred one has no
247
+ declared columns to carry them, so its notes stay in the raw metadata block
248
+ — the same split :func:`weaver.ses.columns.metadata_column_references` makes.
249
+ """
250
+
251
+ ses = document.document
252
+ for declared in ses.schema:
253
+ if declared.name == column and declared.note is not None:
254
+ return declared.note
255
+ if ses.identity == column:
256
+ return MetadataText(literal=IDENTITY_COLUMN_NOTE)
257
+ if not ses.has_declared_schema:
258
+ notes = ses.raw.get("Column notes") or {}
259
+ if isinstance(notes, dict):
260
+ for name, note in notes.items():
261
+ if str(name).strip() == column:
262
+ from .metadata import _parse_text_value
263
+
264
+ return _parse_text_value(str(note), f"Column notes[{column}]")
265
+ return None
266
+
267
+
268
+ def declared_column_notes(document: SourceDocument) -> tuple[tuple[str, MetadataText], ...]:
269
+ """Every column that carries a note, in declared order, plus the identity.
270
+
271
+ This is the whole of what the catalogue's column dictionary describes: the
272
+ columns an author said something about, and Weaver's own surrogate, which is
273
+ given a generic note because no author writes one. Ordinals, types and
274
+ nullability are physical and are recorded elsewhere.
275
+ """
276
+
277
+ ses = document.document
278
+ notes: list[tuple[str, MetadataText]] = []
279
+ if ses.identity is not None and ses.identity_column is not None:
280
+ notes.append((ses.identity, MetadataText(literal=IDENTITY_COLUMN_NOTE)))
281
+ if ses.has_declared_schema:
282
+ notes.extend(
283
+ (column.name, column.note) for column in ses.schema if column.note is not None
284
+ )
285
+ return tuple(notes)
286
+ raw = ses.raw.get("Column notes") or {}
287
+ if isinstance(raw, dict):
288
+ from .metadata import _parse_text_value
289
+
290
+ notes.extend(
291
+ (str(name).strip(), _parse_text_value(str(note), f"Column notes[{name}]"))
292
+ for name, note in raw.items()
293
+ )
294
+ return tuple(notes)