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,104 @@
|
|
|
1
|
+
"""What a load reports when it finishes, in one shape for all four primitives.
|
|
2
|
+
|
|
3
|
+
A Warehouse procedure, a Spark SQL program, a Python table and a Python folder
|
|
4
|
+
run on different engines and return through different transports — a T-SQL
|
|
5
|
+
result set, a Spark ``DataFrame``, a Python object. What they *mean* is the same,
|
|
6
|
+
and this module is where that meaning is written down once.
|
|
7
|
+
|
|
8
|
+
The field names are the contract, not just the dataclass. :data:`RESULT_COLUMNS`
|
|
9
|
+
names them in order, and the SQL generators build their final result row from it,
|
|
10
|
+
so a column added here reaches every transport instead of three spellings
|
|
11
|
+
drifting apart. That is the whole reason the names live beside the dataclass
|
|
12
|
+
rather than inside each generator.
|
|
13
|
+
|
|
14
|
+
**Success is not "nothing raised".** A load that rejected rows reports
|
|
15
|
+
``succeeded=False`` even when it was asked to tolerate them and did — the rows
|
|
16
|
+
did not arrive, and a caller that only checked for an exception would call that a
|
|
17
|
+
clean load. What ``fault_tolerant`` changes is whether the valid rows are still
|
|
18
|
+
written, never whether the run is reported as good (see
|
|
19
|
+
:mod:`weaver.runtime.load_contract`).
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
from dataclasses import dataclass, replace
|
|
25
|
+
|
|
26
|
+
#: The result's columns, in order. The generated T-SQL and Spark SQL programs
|
|
27
|
+
#: project exactly these names, so a transport's final row can be read straight
|
|
28
|
+
#: into :class:`LoadResult` and any mismatch is a generation bug rather than a
|
|
29
|
+
#: silently misread column.
|
|
30
|
+
RESULT_COLUMNS = (
|
|
31
|
+
"succeeded",
|
|
32
|
+
"rows_read",
|
|
33
|
+
"rows_inserted",
|
|
34
|
+
"rows_updated",
|
|
35
|
+
"rows_deleted",
|
|
36
|
+
"rows_rejected",
|
|
37
|
+
"error_message",
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass(frozen=True)
|
|
42
|
+
class LoadResult:
|
|
43
|
+
"""One object's load outcome: what happened, and whether it was acceptable.
|
|
44
|
+
|
|
45
|
+
The counts describe the *target*, not the source. ``rows_read`` is what the
|
|
46
|
+
source produced, and the rest are what the load did with it, so
|
|
47
|
+
``rows_read`` need not equal the sum of the others: an unchanged row is read
|
|
48
|
+
and neither inserted nor updated, which is the ordinary state of most rows in
|
|
49
|
+
most loads.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
succeeded: bool
|
|
53
|
+
rows_read: int = 0
|
|
54
|
+
rows_inserted: int = 0
|
|
55
|
+
rows_updated: int = 0
|
|
56
|
+
rows_deleted: int = 0
|
|
57
|
+
rows_rejected: int = 0
|
|
58
|
+
error_message: str | None = None
|
|
59
|
+
|
|
60
|
+
@classmethod
|
|
61
|
+
def failure(cls, message: str, **counts: int) -> "LoadResult":
|
|
62
|
+
"""A failed load, carrying whatever it managed to do before failing.
|
|
63
|
+
|
|
64
|
+
The counts are kept rather than zeroed because a partial load is exactly
|
|
65
|
+
the case where they matter: "failed having written nothing" and "failed
|
|
66
|
+
having written four hundred rows" are different situations to recover
|
|
67
|
+
from, and a result that reported neither would send the reader to the
|
|
68
|
+
target to find out.
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
return cls(succeeded=False, error_message=message, **counts)
|
|
72
|
+
|
|
73
|
+
def rejected(self, message: str) -> "LoadResult":
|
|
74
|
+
"""This result, marked failed for row rejections it already counted."""
|
|
75
|
+
|
|
76
|
+
return replace(self, succeeded=False, error_message=message)
|
|
77
|
+
|
|
78
|
+
def as_row(self) -> dict:
|
|
79
|
+
"""The result as a mapping keyed by :data:`RESULT_COLUMNS`."""
|
|
80
|
+
|
|
81
|
+
return {name: getattr(self, name) for name in RESULT_COLUMNS}
|
|
82
|
+
|
|
83
|
+
@classmethod
|
|
84
|
+
def from_row(cls, row) -> "LoadResult":
|
|
85
|
+
"""Read a transport's final result row back into a result.
|
|
86
|
+
|
|
87
|
+
Takes anything indexable by column name — a ``dict``, a pyodbc row
|
|
88
|
+
mapping, a Spark ``Row`` — because the three transports each hand back
|
|
89
|
+
their own type and none of them is worth converting twice.
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
values = {name: row[name] for name in RESULT_COLUMNS}
|
|
93
|
+
return cls(
|
|
94
|
+
succeeded=bool(values["succeeded"]),
|
|
95
|
+
rows_read=int(values["rows_read"]),
|
|
96
|
+
rows_inserted=int(values["rows_inserted"]),
|
|
97
|
+
rows_updated=int(values["rows_updated"]),
|
|
98
|
+
rows_deleted=int(values["rows_deleted"]),
|
|
99
|
+
rows_rejected=int(values["rows_rejected"]),
|
|
100
|
+
error_message=values["error_message"],
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
__all__ = ["RESULT_COLUMNS", "LoadResult"]
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""Running an installed Spark SQL load program.
|
|
2
|
+
|
|
3
|
+
The generated program is a list of statements, because Spark executes one
|
|
4
|
+
statement per call. This is the small amount of driving that implies: substitute
|
|
5
|
+
the run's own choice about rejects, execute in order, read the last result set.
|
|
6
|
+
|
|
7
|
+
It is deliberately thin, and the thinness is the claim. Nothing here decides
|
|
8
|
+
anything about the load — the SQL does. If this module had to know what a reject
|
|
9
|
+
was, or when to skip a write, then the installed file would not be independently
|
|
10
|
+
runnable and §9's promise would be false. What it knows is how to say 1 or 0 and
|
|
11
|
+
how to read a row back.
|
|
12
|
+
|
|
13
|
+
::
|
|
14
|
+
|
|
15
|
+
result = run_load_program(spark, installed_sql, fault_tolerant=True)
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from ..declaration.spark_load import (
|
|
21
|
+
FAULT_TOLERANT_DEFAULT,
|
|
22
|
+
FAULT_TOLERANT_MARKER,
|
|
23
|
+
IGNORE_THRESHOLD_DEFAULT,
|
|
24
|
+
IGNORE_THRESHOLD_MARKER,
|
|
25
|
+
statements_of,
|
|
26
|
+
)
|
|
27
|
+
from ..errors import LoadError
|
|
28
|
+
from .load_result import RESULT_COLUMNS, LoadResult
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def run_load_program(
|
|
32
|
+
spark,
|
|
33
|
+
program: str,
|
|
34
|
+
*,
|
|
35
|
+
fault_tolerant: bool = False,
|
|
36
|
+
ignore_stability_threshold: bool = False,
|
|
37
|
+
) -> LoadResult:
|
|
38
|
+
"""Execute an installed load program and report what it did.
|
|
39
|
+
|
|
40
|
+
``program`` is the installed file's text, whose object names are already
|
|
41
|
+
resolved — the installer addressed them as it wrote the file, because a
|
|
42
|
+
program nobody could run without a resolver would not be a primitive.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
statements = statements_of(
|
|
46
|
+
_answer(program, fault_tolerant, ignore_stability_threshold)
|
|
47
|
+
)
|
|
48
|
+
if not statements:
|
|
49
|
+
raise LoadError("the load program contains no statements")
|
|
50
|
+
|
|
51
|
+
frame = None
|
|
52
|
+
for statement in statements:
|
|
53
|
+
try:
|
|
54
|
+
frame = spark.sql(statement)
|
|
55
|
+
if _is_terminal(statement):
|
|
56
|
+
frame.collect()
|
|
57
|
+
except Exception as exc:
|
|
58
|
+
# The program raises natively when a run failed and was not asked to
|
|
59
|
+
# tolerate it, so `exec`-ing the file and calling `.load()` fail the
|
|
60
|
+
# same way. Wrapped here so a caller meets one error type whichever
|
|
61
|
+
# primitive it drove.
|
|
62
|
+
raise LoadError(str(exc)) from exc
|
|
63
|
+
|
|
64
|
+
rows = frame.collect()
|
|
65
|
+
if not rows:
|
|
66
|
+
raise LoadError(
|
|
67
|
+
"the load program's final statement returned no row — it must "
|
|
68
|
+
"project the load result"
|
|
69
|
+
)
|
|
70
|
+
row = rows[0]
|
|
71
|
+
missing = [name for name in RESULT_COLUMNS if name not in row.asDict()]
|
|
72
|
+
if missing:
|
|
73
|
+
raise LoadError(
|
|
74
|
+
"the load program's final statement is missing "
|
|
75
|
+
f"{', '.join(missing)} — it must project the load result"
|
|
76
|
+
)
|
|
77
|
+
result = LoadResult.from_row({name: row[name] for name in RESULT_COLUMNS})
|
|
78
|
+
if result.succeeded:
|
|
79
|
+
_clear(spark, program)
|
|
80
|
+
return result
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
#: The artefacts a clean run leaves behind, and does not need to. The rule is
|
|
84
|
+
#: the same on all three table primitives: a run that refused rows keeps its
|
|
85
|
+
#: evidence, a clean one keeps nothing. Done here rather than in the program
|
|
86
|
+
#: because the final statement reads the result table — cleanup has to follow
|
|
87
|
+
#: the row being taken, and only the runner knows when that has happened.
|
|
88
|
+
_ARTEFACT_SUFFIXES = ("_Staging", "_Upsert", "_Reject", "_Delete", "_LoadResult")
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _clear(spark, program: str) -> None:
|
|
92
|
+
for name in _artefact_names(program):
|
|
93
|
+
spark.sql(f"DROP TABLE IF EXISTS {name}")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _artefact_names(program: str) -> list[str]:
|
|
97
|
+
"""The artefacts this program named, read back off its own drop statements.
|
|
98
|
+
|
|
99
|
+
The program opens by dropping whatever a previous run left, so it already
|
|
100
|
+
says which relations it owns — and reading them from there means the runner
|
|
101
|
+
never has to compose a name the generator might spell differently.
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
names = []
|
|
105
|
+
for statement in statements_of(program):
|
|
106
|
+
head, _, tail = statement.partition("DROP TABLE IF EXISTS ")
|
|
107
|
+
if not tail:
|
|
108
|
+
continue
|
|
109
|
+
candidate = tail.strip().splitlines()[0].strip()
|
|
110
|
+
if candidate.endswith(_ARTEFACT_SUFFIXES) or candidate.rstrip("`").endswith(
|
|
111
|
+
_ARTEFACT_SUFFIXES
|
|
112
|
+
):
|
|
113
|
+
names.append(candidate)
|
|
114
|
+
# The result table is dropped last: everything else is read while deciding,
|
|
115
|
+
# and it is read to produce the row that was just taken.
|
|
116
|
+
return names
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _is_terminal(statement: str) -> bool:
|
|
120
|
+
"""Whether this statement must be evaluated rather than merely planned.
|
|
121
|
+
|
|
122
|
+
Spark is lazy, so a `SELECT` that raises does nothing until something reads
|
|
123
|
+
it — and the guard's entire job is to raise. DDL and DML run eagerly; the
|
|
124
|
+
guard is the one projection whose evaluation matters.
|
|
125
|
+
"""
|
|
126
|
+
|
|
127
|
+
return "raise_error(" in statement
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _answer(
|
|
131
|
+
program: str, fault_tolerant: bool, ignore_stability_threshold: bool
|
|
132
|
+
) -> str:
|
|
133
|
+
"""Substitute the one question the file leaves open.
|
|
134
|
+
|
|
135
|
+
A run's tolerance of rejects, and its willingness to waive the declared
|
|
136
|
+
stability thresholds, are properties of the run rather than of the object or
|
|
137
|
+
of where it lives — so they are the only holes the installer leaves for
|
|
138
|
+
whoever runs the program. Both already read 0, so the cautious answers need
|
|
139
|
+
no substitution at all, which is what makes an installed program runnable
|
|
140
|
+
exactly as it stands.
|
|
141
|
+
"""
|
|
142
|
+
|
|
143
|
+
if fault_tolerant:
|
|
144
|
+
program = program.replace(FAULT_TOLERANT_DEFAULT, f"{FAULT_TOLERANT_MARKER}1")
|
|
145
|
+
if ignore_stability_threshold:
|
|
146
|
+
program = program.replace(
|
|
147
|
+
IGNORE_THRESHOLD_DEFAULT, f"{IGNORE_THRESHOLD_MARKER}1"
|
|
148
|
+
)
|
|
149
|
+
return program
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
__all__ = ["run_load_program"]
|