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
weaver/objects.py
ADDED
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
"""The authoring surface — what a developer writes.
|
|
2
|
+
|
|
3
|
+
An object is a class in a file named for its ID::
|
|
4
|
+
|
|
5
|
+
# Sales__Order.py
|
|
6
|
+
\"\"\"
|
|
7
|
+
Table ID: Sales.Order
|
|
8
|
+
|
|
9
|
+
Description: One row per confirmed customer order.
|
|
10
|
+
|
|
11
|
+
Lineage: The sales system order export.
|
|
12
|
+
|
|
13
|
+
Primary key: Order id
|
|
14
|
+
|
|
15
|
+
Schema:
|
|
16
|
+
Order id: string
|
|
17
|
+
Amount: decimal(18,2)
|
|
18
|
+
\"\"\"
|
|
19
|
+
|
|
20
|
+
from Sales__Customer import Sales__Customer
|
|
21
|
+
|
|
22
|
+
from weaver import Table
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Sales__Order(Table):
|
|
26
|
+
def read(self):
|
|
27
|
+
customers = Sales__Customer(self).dataframe()
|
|
28
|
+
existing = self.dataframe()
|
|
29
|
+
...
|
|
30
|
+
return upserts, deletes
|
|
31
|
+
|
|
32
|
+
**An object is an ordinary Python object bound to a Spark session.** The session
|
|
33
|
+
is the one mandatory argument, because authored code executes through it::
|
|
34
|
+
|
|
35
|
+
Sales__Order(spark).read()
|
|
36
|
+
|
|
37
|
+
That is the whole runtime model. There is no hidden active object, no ambient
|
|
38
|
+
resolver, and no context injected around a call — an object holds a session, a
|
|
39
|
+
destination Lakehouse and its own identity, and every physical access is an
|
|
40
|
+
ordinary instance method on it.
|
|
41
|
+
|
|
42
|
+
**The destination is a resolved Lakehouse, never a name.** In a notebook it is
|
|
43
|
+
the one attached to the session, and Weaver reads it
|
|
44
|
+
(:func:`weaver.lakehouse.default_lakehouse`); with no unique attachment,
|
|
45
|
+
construction fails rather than guessing which Lakehouse a load should land in. An
|
|
46
|
+
orchestrator, or anyone addressing more than one Lakehouse, resolves it *outside*
|
|
47
|
+
the object and passes it in::
|
|
48
|
+
|
|
49
|
+
Sales__Order(spark, lakehouse=resolved_lakehouse)
|
|
50
|
+
|
|
51
|
+
A string is refused: looking a name up needs a workspace resolver, and that is
|
|
52
|
+
not a decision authored code makes.
|
|
53
|
+
|
|
54
|
+
**Dependencies are imports, and a dependency is constructed from its dependent**::
|
|
55
|
+
|
|
56
|
+
Another__Table(self).dataframe()
|
|
57
|
+
My__Folder(self).path()
|
|
58
|
+
|
|
59
|
+
Importing another object's module declares the dependency; Weaver reads that from
|
|
60
|
+
the source without executing it. Passing ``self`` hands over the session and the
|
|
61
|
+
resolved Lakehouse together, so a dependency always resolves against the same
|
|
62
|
+
target environment as the object reading it. A Python import only ever names an
|
|
63
|
+
object in the *same* item, which is why nothing more is needed here — a
|
|
64
|
+
cross-item dependency is an alias, and aliases are resolved during build.
|
|
65
|
+
|
|
66
|
+
**Identity comes from the class name.** ``Sales__Order`` is ``Sales.Order``: the
|
|
67
|
+
same rule the repository parser applies to the filename, and the parser has
|
|
68
|
+
already refused any file where the two disagree. Nothing is re-parsed, and
|
|
69
|
+
nothing is looked up in the catalogue, to know what an object is.
|
|
70
|
+
|
|
71
|
+
**Objects never mutate the target.** ``read()`` proposes; Weaver owns writing,
|
|
72
|
+
CRUD accounting, staging and logging. A Folder writes into its staging directory
|
|
73
|
+
and returns it; a Table returns rows.
|
|
74
|
+
|
|
75
|
+
Nothing here imports PySpark. The session is used through its ordinary API, so
|
|
76
|
+
this module stays importable anywhere.
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
from __future__ import annotations
|
|
80
|
+
|
|
81
|
+
from typing import Any
|
|
82
|
+
|
|
83
|
+
from .errors import LoadError
|
|
84
|
+
from .lakehouse import Lakehouse, default_lakehouse
|
|
85
|
+
|
|
86
|
+
#: What separates schema from object in a class name. A module name cannot carry
|
|
87
|
+
#: a dot, so a Python object spells ``Sales.Order`` as ``Sales__Order`` — the rule
|
|
88
|
+
#: :func:`weaver.declaration.source.object_id_for_filename` applies to the
|
|
89
|
+
#: filename, repeated here because the authoring surface must not import the
|
|
90
|
+
#: parser (the parser imports it, for the base classes).
|
|
91
|
+
CLASS_ID_SEPARATOR = "__"
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class WeaverObject:
|
|
95
|
+
"""Base for every authored object.
|
|
96
|
+
|
|
97
|
+
``spark`` is the session authored code runs through, and it is mandatory.
|
|
98
|
+
Another Weaver object may be passed in its place — ``Another__Table(self)`` —
|
|
99
|
+
inheriting that object's session and Lakehouse, which is how one object
|
|
100
|
+
reaches another.
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
def __init__(self, spark: Any, *, lakehouse: Lakehouse | None = None) -> None:
|
|
104
|
+
if isinstance(spark, WeaverObject):
|
|
105
|
+
owner = spark
|
|
106
|
+
spark = owner.spark
|
|
107
|
+
if lakehouse is None:
|
|
108
|
+
lakehouse = owner.lakehouse
|
|
109
|
+
if spark is None:
|
|
110
|
+
raise LoadError(
|
|
111
|
+
f"{type(self).__name__} needs the Spark session it runs through — "
|
|
112
|
+
f"construct it as {type(self).__name__}(spark), or as "
|
|
113
|
+
f"{type(self).__name__}(self) from another object"
|
|
114
|
+
)
|
|
115
|
+
if isinstance(lakehouse, str):
|
|
116
|
+
raise LoadError(
|
|
117
|
+
f"{type(self).__name__} takes a resolved Lakehouse, not the name "
|
|
118
|
+
f"{lakehouse!r} — resolve it first with "
|
|
119
|
+
f"weaver.lakehouse_for(resolver, {lakehouse!r})"
|
|
120
|
+
)
|
|
121
|
+
if lakehouse is not None and not isinstance(lakehouse, Lakehouse):
|
|
122
|
+
raise LoadError(
|
|
123
|
+
f"{type(self).__name__} takes a resolved Lakehouse, got "
|
|
124
|
+
f"{type(lakehouse).__name__}"
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
#: The session this object reads and writes through.
|
|
128
|
+
self.spark = spark
|
|
129
|
+
#: The destination this object materialises into, resolved once.
|
|
130
|
+
self.lakehouse: Lakehouse = (
|
|
131
|
+
lakehouse if lakehouse is not None else default_lakehouse(spark)
|
|
132
|
+
)
|
|
133
|
+
#: The destination's root — what Spark and Hadoop address. Tables and
|
|
134
|
+
#: folders both hang off it, so nothing an object reaches needs a mount.
|
|
135
|
+
self.spark_root = self.lakehouse.spark_root
|
|
136
|
+
|
|
137
|
+
# --- identity ---------------------------------------------------------
|
|
138
|
+
|
|
139
|
+
@property
|
|
140
|
+
def identity(self) -> tuple[str, str]:
|
|
141
|
+
"""The schema and object name this class declares."""
|
|
142
|
+
|
|
143
|
+
return _identity(type(self).__name__)
|
|
144
|
+
|
|
145
|
+
@property
|
|
146
|
+
def object_id(self) -> str:
|
|
147
|
+
"""This object's ``Schema.Object`` ID, from its class name."""
|
|
148
|
+
|
|
149
|
+
return "{}.{}".format(*self.identity)
|
|
150
|
+
|
|
151
|
+
def read(self):
|
|
152
|
+
raise NotImplementedError(f"{type(self).__name__} must implement read()")
|
|
153
|
+
|
|
154
|
+
# --- the load contract, read from this module's own docstring ----------
|
|
155
|
+
|
|
156
|
+
def _document(self):
|
|
157
|
+
"""This object's parsed declaration, from the module it was defined in.
|
|
158
|
+
|
|
159
|
+
The module *is* the contract. A deployed object in a session has no
|
|
160
|
+
repository to reopen and no catalogue to query, so if its docstring were
|
|
161
|
+
not sufficient then ``load()`` would be the tail end of an orchestration
|
|
162
|
+
rather than something runnable on its own.
|
|
163
|
+
|
|
164
|
+
It is read on every call rather than cached, which is what makes an edit
|
|
165
|
+
visible on the next reload — the notebook loop this is meant to support.
|
|
166
|
+
"""
|
|
167
|
+
|
|
168
|
+
import sys
|
|
169
|
+
|
|
170
|
+
from .runtime.load_contract import document_for_module
|
|
171
|
+
|
|
172
|
+
module = sys.modules.get(type(self).__module__)
|
|
173
|
+
if module is None: # pragma: no cover - a class with no importable module
|
|
174
|
+
raise LoadError(
|
|
175
|
+
f"{type(self).__name__} was defined outside an importable module, "
|
|
176
|
+
"so its Weaver metadata cannot be read"
|
|
177
|
+
)
|
|
178
|
+
return document_for_module(module)
|
|
179
|
+
|
|
180
|
+
def __repr__(self) -> str:
|
|
181
|
+
return f"<{type(self).__name__} {self.object_id} in {self.lakehouse.name}>"
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class Folder(WeaverObject):
|
|
185
|
+
"""Files materialised into a Lakehouse Files directory.
|
|
186
|
+
|
|
187
|
+
``read()`` writes into this object's staging directory and returns
|
|
188
|
+
``(staging_folder, files_to_delete)``.
|
|
189
|
+
"""
|
|
190
|
+
|
|
191
|
+
def path(self) -> str:
|
|
192
|
+
"""This folder's materialised location, as Spark addresses it.
|
|
193
|
+
|
|
194
|
+
What one object hands another: a table reading these files does it with
|
|
195
|
+
``spark.read``, which wants the ``abfss://`` form. Addressed by the root
|
|
196
|
+
the Lakehouse was resolved to, never by ``/lakehouse/default`` — that
|
|
197
|
+
names only whatever a notebook attached, and a load runs detached against
|
|
198
|
+
Lakehouses it resolved by name.
|
|
199
|
+
"""
|
|
200
|
+
|
|
201
|
+
return self.lakehouse.folder_path(*self.identity)
|
|
202
|
+
|
|
203
|
+
def local_path(self) -> str:
|
|
204
|
+
"""This folder's location for code that opens files rather than reading
|
|
205
|
+
them through Spark.
|
|
206
|
+
|
|
207
|
+
The same bytes as :meth:`path`, spelled as a filesystem path. In OneLake
|
|
208
|
+
that is a mount Weaver makes of the resolved root; locally the two are
|
|
209
|
+
the same directory.
|
|
210
|
+
"""
|
|
211
|
+
|
|
212
|
+
return self.lakehouse.folder_local_path(*self.identity)
|
|
213
|
+
|
|
214
|
+
def staging_folder(self) -> str:
|
|
215
|
+
"""The object-local staging directory to write into.
|
|
216
|
+
|
|
217
|
+
The destination's own path with ``_Staging`` appended — the same sibling
|
|
218
|
+
:meth:`weaver.resolution.LocalResolver.folder_staging` issues. There is no
|
|
219
|
+
shared staging area and no run identifier: staging belongs to the object,
|
|
220
|
+
so a failed load leaves exactly one directory to look at.
|
|
221
|
+
"""
|
|
222
|
+
|
|
223
|
+
return f"{self.local_path()}_Staging"
|
|
224
|
+
|
|
225
|
+
def load(self, fault_tolerant: bool = False) -> "LoadResult":
|
|
226
|
+
"""Run this folder's ``read()`` and publish what it staged.
|
|
227
|
+
|
|
228
|
+
Independently runnable, which is the point::
|
|
229
|
+
|
|
230
|
+
Sales__Export(spark).load(fault_tolerant=False)
|
|
231
|
+
|
|
232
|
+
No repository, no catalogue, no bundle and no orchestrator — the module
|
|
233
|
+
carries its own contract and this object carries its own destination.
|
|
234
|
+
"""
|
|
235
|
+
|
|
236
|
+
from .runtime.folder_load import load_folder, new_staging_folder
|
|
237
|
+
from .runtime.load_contract import FolderLoadContract
|
|
238
|
+
|
|
239
|
+
contract = FolderLoadContract.from_document(self._document())
|
|
240
|
+
# Weaver issues staging, before read() rather than after the load. A run
|
|
241
|
+
# must begin from nothing it did not itself produce, or the previous
|
|
242
|
+
# run's files are published again and a replacement concludes that
|
|
243
|
+
# nothing was retired. Clearing afterwards instead would destroy the one
|
|
244
|
+
# directory worth looking at when a load fails.
|
|
245
|
+
issued = new_staging_folder(self.local_path(), self.staging_folder())
|
|
246
|
+
staged, deletes = _load_pair(self, self.read())
|
|
247
|
+
if str(staged) != issued:
|
|
248
|
+
raise LoadError(
|
|
249
|
+
f"{type(self).__name__}.read() returned {staged!r}, which is not "
|
|
250
|
+
f"the staging folder Weaver issued ({issued!r}) — return "
|
|
251
|
+
"self.staging_folder()"
|
|
252
|
+
)
|
|
253
|
+
return load_folder(
|
|
254
|
+
contract=contract,
|
|
255
|
+
destination=self.local_path(),
|
|
256
|
+
staging=issued,
|
|
257
|
+
deletes=deletes,
|
|
258
|
+
fault_tolerant=fault_tolerant,
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
class Table(WeaverObject):
|
|
263
|
+
"""Rows materialised into a Delta table or a Warehouse table.
|
|
264
|
+
|
|
265
|
+
``read()`` returns ``(upserts, deletes)``.
|
|
266
|
+
"""
|
|
267
|
+
|
|
268
|
+
def dataframe(self) -> Any:
|
|
269
|
+
"""This table as it currently stands, read from its Delta files.
|
|
270
|
+
|
|
271
|
+
Addressed by path rather than by catalogue name, which is how Weaver
|
|
272
|
+
reaches Delta everywhere else: a path needs nothing attached, so the same
|
|
273
|
+
call serves any resolved Lakehouse.
|
|
274
|
+
"""
|
|
275
|
+
|
|
276
|
+
return self.spark.read.format("delta").load(
|
|
277
|
+
self.lakehouse.table_path(*self.identity)
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
def empty_dataframe(self) -> Any:
|
|
281
|
+
"""This table's shape with no rows — an incremental load's no-op result.
|
|
282
|
+
|
|
283
|
+
Taken from the table itself, so the columns are exactly the ones the load
|
|
284
|
+
has to match. That means the physical table must already exist, which is no
|
|
285
|
+
constraint at all: a load returning *this* table's empty shape is by
|
|
286
|
+
definition running against a target that has been built.
|
|
287
|
+
"""
|
|
288
|
+
|
|
289
|
+
return self.dataframe().limit(0)
|
|
290
|
+
|
|
291
|
+
def load(
|
|
292
|
+
self,
|
|
293
|
+
fault_tolerant: bool = False,
|
|
294
|
+
ignore_stability_threshold: bool = False,
|
|
295
|
+
) -> "LoadResult":
|
|
296
|
+
"""Run this table's ``read()`` and write what it staged.
|
|
297
|
+
|
|
298
|
+
Independently runnable, which is the point::
|
|
299
|
+
|
|
300
|
+
Sales__Customer(spark).load(fault_tolerant=True)
|
|
301
|
+
|
|
302
|
+
No repository, no catalogue, no bundle and no orchestrator — the module
|
|
303
|
+
carries its own contract and this object carries its own destination.
|
|
304
|
+
|
|
305
|
+
``ignore_stability_threshold`` waives the declared delete and update
|
|
306
|
+
limits for one run. It exists for the case where a very large change is
|
|
307
|
+
the correct answer — a genuine bulk retirement — and is a deliberate act
|
|
308
|
+
each time rather than a setting that stays on.
|
|
309
|
+
"""
|
|
310
|
+
|
|
311
|
+
from .runtime.load_contract import LoadContract
|
|
312
|
+
from .runtime.table_load import load_table
|
|
313
|
+
|
|
314
|
+
contract = LoadContract.from_document(self._document())
|
|
315
|
+
# The first value is *staging* — unvalidated, unreconciled, nothing yet
|
|
316
|
+
# classified as new or changed. Naming it so is the point.
|
|
317
|
+
staged, deletes = _load_pair(self, self.read())
|
|
318
|
+
return load_table(
|
|
319
|
+
self.spark,
|
|
320
|
+
contract=contract,
|
|
321
|
+
lakehouse=self.lakehouse,
|
|
322
|
+
staging_frame=staged,
|
|
323
|
+
deletes=deletes,
|
|
324
|
+
fault_tolerant=fault_tolerant,
|
|
325
|
+
ignore_stability_threshold=ignore_stability_threshold,
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
class View(WeaverObject):
|
|
330
|
+
"""A view over other objects, declared in SQL.
|
|
331
|
+
|
|
332
|
+
A view has no ``read()``: its definition is its query.
|
|
333
|
+
"""
|
|
334
|
+
|
|
335
|
+
def dataframe(self) -> Any:
|
|
336
|
+
"""This view's contents.
|
|
337
|
+
|
|
338
|
+
By name, not by path: a view exists only in the catalogue, so unlike a
|
|
339
|
+
table there is nothing on disk to address.
|
|
340
|
+
"""
|
|
341
|
+
|
|
342
|
+
return self.spark.table(self.lakehouse.qualify(*self.identity))
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def _load_pair(obj, returned):
|
|
346
|
+
"""Unpack what ``read()`` returned, refusing anything else by name.
|
|
347
|
+
|
|
348
|
+
Both kinds of object return a pair, and the error has to name the object
|
|
349
|
+
rather than surface as a tuple-unpacking failure three frames deeper — an
|
|
350
|
+
author who returned a single frame should be told that, not shown a
|
|
351
|
+
ValueError about lengths.
|
|
352
|
+
"""
|
|
353
|
+
|
|
354
|
+
if not isinstance(returned, tuple) or len(returned) != 2:
|
|
355
|
+
raise LoadError(
|
|
356
|
+
f"{type(obj).__name__}.read() must return a pair — "
|
|
357
|
+
f"(staging, deletes) for a Table, (staging_folder, files_to_delete) "
|
|
358
|
+
f"for a Folder — and returned {type(returned).__name__}"
|
|
359
|
+
)
|
|
360
|
+
return returned
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def _identity(class_name: str) -> tuple[str, str]:
|
|
364
|
+
"""``Sales__Order`` → ``("Sales", "Order")``; ``___Load`` → ``("_", "Load")``.
|
|
365
|
+
|
|
366
|
+
A run of leading underscores is read as a schema plus the separator, because
|
|
367
|
+
``_`` is a real schema and spelling ``_.Load`` as a class name produces three
|
|
368
|
+
of them. The rule is the parser's — see
|
|
369
|
+
:func:`weaver.declaration.source.python_id_parts` — repeated rather than
|
|
370
|
+
imported, for the same reason the separator itself is.
|
|
371
|
+
"""
|
|
372
|
+
|
|
373
|
+
leading = len(class_name) - len(class_name.lstrip("_"))
|
|
374
|
+
if leading >= len(CLASS_ID_SEPARATOR) + 1:
|
|
375
|
+
split = [
|
|
376
|
+
class_name[: leading - len(CLASS_ID_SEPARATOR)],
|
|
377
|
+
class_name[leading:],
|
|
378
|
+
]
|
|
379
|
+
else:
|
|
380
|
+
split = class_name.split(CLASS_ID_SEPARATOR)
|
|
381
|
+
parts = [part.strip() for part in split]
|
|
382
|
+
if len(parts) != 2 or not all(parts):
|
|
383
|
+
raise LoadError(
|
|
384
|
+
f"{class_name!r} does not name an object: a Weaver class separates "
|
|
385
|
+
f"schema and object with {CLASS_ID_SEPARATOR!r}, as in Sales__Order"
|
|
386
|
+
)
|
|
387
|
+
return parts[0], parts[1]
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
#: The authoring base classes, by the metadata kind that selects them.
|
|
391
|
+
BASE_CLASSES = {"Folder": Folder, "Table": Table, "View": View}
|
|
392
|
+
BASE_CLASS_NAMES = frozenset(cls.__name__ for cls in BASE_CLASSES.values())
|