weaverstack 0.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- weaver/__init__.py +59 -0
- weaver/build_bundle/__init__.py +109 -0
- weaver/build_bundle/aliases.py +325 -0
- weaver/build_bundle/bundle.py +359 -0
- weaver/build_bundle/catalogue_actions.py +275 -0
- weaver/build_bundle/changes.py +186 -0
- weaver/build_bundle/endpoints.py +83 -0
- weaver/build_bundle/executors/__init__.py +69 -0
- weaver/build_bundle/executors/alias.py +202 -0
- weaver/build_bundle/executors/base.py +132 -0
- weaver/build_bundle/executors/folder.py +71 -0
- weaver/build_bundle/executors/load_file.py +205 -0
- weaver/build_bundle/executors/spark_case.py +26 -0
- weaver/build_bundle/executors/spark_schema.py +60 -0
- weaver/build_bundle/executors/spark_sql.py +59 -0
- weaver/build_bundle/executors/spark_sql_batch.py +57 -0
- weaver/build_bundle/executors/spark_table.py +213 -0
- weaver/build_bundle/executors/sql_endpoint_refresh.py +34 -0
- weaver/build_bundle/executors/tsql.py +81 -0
- weaver/build_bundle/incremental.py +288 -0
- weaver/build_bundle/installer.py +384 -0
- weaver/build_bundle/models.py +288 -0
- weaver/build_bundle/payloads.py +34 -0
- weaver/build_bundle/physical.py +625 -0
- weaver/build_bundle/planner.py +389 -0
- weaver/build_bundle/prune.py +620 -0
- weaver/build_bundle/report.py +108 -0
- weaver/build_bundle/stages.py +196 -0
- weaver/build_bundle/targets.py +272 -0
- weaver/build_bundle/workflow.py +585 -0
- weaver/catalogue/__init__.py +73 -0
- weaver/catalogue/builtin.py +238 -0
- weaver/catalogue/claims.py +121 -0
- weaver/catalogue/projection.py +437 -0
- weaver/catalogue/reader.py +152 -0
- weaver/catalogue/reconcile.py +231 -0
- weaver/catalogue/render.py +410 -0
- weaver/catalogue/state.py +660 -0
- weaver/catalogue/tables.py +648 -0
- weaver/config.py +178 -0
- weaver/declaration/__init__.py +171 -0
- weaver/declaration/columns.py +223 -0
- weaver/declaration/ddl.py +266 -0
- weaver/declaration/dependencies.py +544 -0
- weaver/declaration/graph.py +240 -0
- weaver/declaration/item_dependencies.py +292 -0
- weaver/declaration/load.py +191 -0
- weaver/declaration/metadata.py +1405 -0
- weaver/declaration/model.py +448 -0
- weaver/declaration/references.py +294 -0
- weaver/declaration/repository.py +959 -0
- weaver/declaration/schemas.py +135 -0
- weaver/declaration/source.py +674 -0
- weaver/declaration/spark_load.py +759 -0
- weaver/declaration/sql_shaping.py +591 -0
- weaver/declaration/templates/ddl/declared_create_table.sql +64 -0
- weaver/declaration/templates/ddl/infer_create_table.sql +97 -0
- weaver/declaration/templates/ddl/metadata_column_validation.sql +30 -0
- weaver/declaration/templates/load/column_metadata.sql +40 -0
- weaver/declaration/templates/load/full_replace_body.sql +21 -0
- weaver/declaration/templates/load/install_load_procedure.sql +27 -0
- weaver/declaration/templates/load/load_procedure.sql +48 -0
- weaver/declaration/templates/load/primary_key_body.sql +113 -0
- weaver/declaration/tsql_ddl.py +468 -0
- weaver/declaration/tsql_load.py +417 -0
- weaver/declaration/warehouse_type_mapping.yml +93 -0
- weaver/diagnostics.py +247 -0
- weaver/errors.py +61 -0
- weaver/etl.py +469 -0
- weaver/fabric/__init__.py +107 -0
- weaver/fabric/auth.py +137 -0
- weaver/fabric/capacity.py +143 -0
- weaver/fabric/client.py +147 -0
- weaver/fabric/environment.py +460 -0
- weaver/fabric/livy.py +478 -0
- weaver/fabric/notebooks.py +201 -0
- weaver/fabric/onelake.py +263 -0
- weaver/fabric/resolution.py +344 -0
- weaver/fabric/resources.py +245 -0
- weaver/fabric/session.py +148 -0
- weaver/fabric/shortcuts.py +120 -0
- weaver/fabric/sql.py +118 -0
- weaver/fabric/store.py +198 -0
- weaver/initialise.py +209 -0
- weaver/lakehouse.py +386 -0
- weaver/load.py +474 -0
- weaver/load_execution.py +483 -0
- weaver/load_plan.py +912 -0
- weaver/load_report.py +330 -0
- weaver/load_resolution.py +386 -0
- weaver/locations.py +164 -0
- weaver/objects.py +392 -0
- weaver/operations.py +757 -0
- weaver/physical_wipe.py +369 -0
- weaver/push.py +76 -0
- weaver/resolution.py +292 -0
- weaver/runtime/__init__.py +30 -0
- weaver/runtime/folder_load.py +402 -0
- weaver/runtime/load_contract.py +245 -0
- weaver/runtime/load_result.py +104 -0
- weaver/runtime/spark_load.py +152 -0
- weaver/runtime/table_load.py +497 -0
- weaver/spark/__init__.py +49 -0
- weaver/spark/catalogue.py +245 -0
- weaver/spark/destination.py +195 -0
- weaver/spark/session.py +84 -0
- weaver/spark/tokens.py +138 -0
- weaver/sql/__init__.py +40 -0
- weaver/sql/authentication.py +38 -0
- weaver/sql/connection.py +90 -0
- weaver/sql/errors.py +25 -0
- weaver/sql/execution.py +123 -0
- weaver/sql/pool.py +174 -0
- weaver/sql/wipe.py +156 -0
- weaver/store.py +209 -0
- weaver/targets.py +257 -0
- weaver/task_logging.py +215 -0
- weaver/unbind.py +74 -0
- weaver/workspaces.py +175 -0
- weaver_cli/__init__.py +12 -0
- weaver_cli/__main__.py +7 -0
- weaver_cli/main.py +626 -0
- weaverstack-0.1.1.dist-info/METADATA +113 -0
- weaverstack-0.1.1.dist-info/RECORD +127 -0
- weaverstack-0.1.1.dist-info/WHEEL +4 -0
- weaverstack-0.1.1.dist-info/entry_points.txt +2 -0
- weaverstack-0.1.1.dist-info/licenses/LICENSE +201 -0
|
@@ -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)
|