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/diagnostics.py
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
"""What a local Spark run needs, and whether this machine has it.
|
|
2
|
+
|
|
3
|
+
Local build and load need a JVM and a matched Spark/Delta pair. None of that is
|
|
4
|
+
required to *use* Weaver on Fabric, so it is optional — but when it is missing
|
|
5
|
+
the failure lands deep inside a Java stack trace, which is a poor way to learn
|
|
6
|
+
you needed a JDK. This reports the same facts up front.
|
|
7
|
+
|
|
8
|
+
Nothing here imports PySpark. Versions are read from package metadata, so the
|
|
9
|
+
check stays cheap and works when the pieces are absent.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import os
|
|
15
|
+
import platform
|
|
16
|
+
import shutil
|
|
17
|
+
import subprocess
|
|
18
|
+
import sys
|
|
19
|
+
from dataclasses import dataclass, field
|
|
20
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
#: Delta and Spark move together — delta-spark 3.2.x expects Spark 3.5.x.
|
|
24
|
+
SUPPORTED_PYTHON = (3, 11)
|
|
25
|
+
SUPPORTED_PYSPARK = ("3.5",)
|
|
26
|
+
SUPPORTED_DELTA = ("3.2",)
|
|
27
|
+
#: Spark 3.5 documents Java 8, 11 and 17; 21 is not documented but runs the
|
|
28
|
+
#: local Delta suite, so it is accepted rather than preferred. The order is the
|
|
29
|
+
#: discovery preference — `find_java_home` takes the first release it can find.
|
|
30
|
+
SUPPORTED_JAVA = ("17", "11", "21")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(frozen=True)
|
|
34
|
+
class Check:
|
|
35
|
+
name: str
|
|
36
|
+
found: str | None
|
|
37
|
+
ok: bool
|
|
38
|
+
hint: str = ""
|
|
39
|
+
|
|
40
|
+
def __str__(self) -> str:
|
|
41
|
+
mark = "ok " if self.ok else "MISSING" if self.found is None else "WRONG"
|
|
42
|
+
return f"{mark:8} {self.name:14} {self.found or '-'}"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass(frozen=True)
|
|
46
|
+
class LocalSparkReport:
|
|
47
|
+
checks: tuple[Check, ...] = field(default_factory=tuple)
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def ok(self) -> bool:
|
|
51
|
+
return all(check.ok for check in self.checks)
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def hints(self) -> tuple[str, ...]:
|
|
55
|
+
"""What to do about the failures, each said once.
|
|
56
|
+
|
|
57
|
+
PySpark and Delta are installed by the same extra, so a machine missing
|
|
58
|
+
both would otherwise be told to run the same command twice.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
seen: dict[str, None] = {}
|
|
62
|
+
for check in self.checks:
|
|
63
|
+
if not check.ok and check.hint:
|
|
64
|
+
seen.setdefault(check.hint)
|
|
65
|
+
return tuple(seen)
|
|
66
|
+
|
|
67
|
+
def as_dict(self) -> dict:
|
|
68
|
+
return {
|
|
69
|
+
"ok": self.ok,
|
|
70
|
+
"checks": [
|
|
71
|
+
{"name": c.name, "found": c.found, "ok": c.ok, "hint": c.hint}
|
|
72
|
+
for c in self.checks
|
|
73
|
+
],
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def find_java_home() -> str | None:
|
|
78
|
+
"""A JDK that Spark 3.5 can use, preferring the newest supported.
|
|
79
|
+
|
|
80
|
+
``JAVA_HOME`` wins when it is already set to something that exists, so a
|
|
81
|
+
deliberately configured machine is never second-guessed.
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
existing = os.environ.get("JAVA_HOME")
|
|
85
|
+
if existing and Path(existing).exists():
|
|
86
|
+
return existing
|
|
87
|
+
|
|
88
|
+
java_home_tool = Path("/usr/libexec/java_home")
|
|
89
|
+
if java_home_tool.exists():
|
|
90
|
+
for release in SUPPORTED_JAVA:
|
|
91
|
+
try:
|
|
92
|
+
found = subprocess.run(
|
|
93
|
+
[str(java_home_tool), "-v", release],
|
|
94
|
+
capture_output=True, text=True, check=True,
|
|
95
|
+
).stdout.strip()
|
|
96
|
+
except (OSError, subprocess.CalledProcessError):
|
|
97
|
+
continue
|
|
98
|
+
if found:
|
|
99
|
+
return found
|
|
100
|
+
|
|
101
|
+
java = shutil.which("java")
|
|
102
|
+
if java:
|
|
103
|
+
return str(Path(java).resolve().parent.parent)
|
|
104
|
+
return None
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def java_launcher(java_home: str | None) -> Path | None:
|
|
108
|
+
"""The ``java`` binary inside a JAVA_HOME, whatever the platform names it.
|
|
109
|
+
|
|
110
|
+
Windows ships ``bin/java.exe``; everywhere else it is ``bin/java``. Both
|
|
111
|
+
names are tried rather than branching on the platform, so the lookup cannot
|
|
112
|
+
be wrong about the machine it is running on.
|
|
113
|
+
"""
|
|
114
|
+
|
|
115
|
+
if java_home is None:
|
|
116
|
+
return None
|
|
117
|
+
bin_dir = Path(java_home) / "bin"
|
|
118
|
+
for name in ("java", "java.exe"):
|
|
119
|
+
candidate = bin_dir / name
|
|
120
|
+
if candidate.is_file():
|
|
121
|
+
return candidate
|
|
122
|
+
return None
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def parse_java_version(output: str) -> str | None:
|
|
126
|
+
"""The release from `java -version` output: openjdk version "17.0.19" ...
|
|
127
|
+
|
|
128
|
+
The banner is not reliably the first line. A JVM started with
|
|
129
|
+
JAVA_TOOL_OPTIONS or _JAVA_OPTIONS set — a proxy's truststore, a container's
|
|
130
|
+
defaults — announces those first, so match the banner itself rather than
|
|
131
|
+
trusting its position. Output that carries no banner is no version at all,
|
|
132
|
+
not a line to be reported as one.
|
|
133
|
+
|
|
134
|
+
Kept apart from the subprocess so the parsing can be tested on every
|
|
135
|
+
platform, rather than only where a fake `java` script can be executed.
|
|
136
|
+
"""
|
|
137
|
+
|
|
138
|
+
for line in output.splitlines():
|
|
139
|
+
if 'version "' not in line:
|
|
140
|
+
continue
|
|
141
|
+
for part in line.split('"'):
|
|
142
|
+
if part and part[0].isdigit():
|
|
143
|
+
return part
|
|
144
|
+
return None
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def java_version(java_home: str | None) -> str | None:
|
|
148
|
+
java = java_launcher(java_home)
|
|
149
|
+
if java is None:
|
|
150
|
+
return None
|
|
151
|
+
try:
|
|
152
|
+
# `java -version` writes to stderr.
|
|
153
|
+
result = subprocess.run(
|
|
154
|
+
[str(java), "-version"], capture_output=True, text=True, check=True
|
|
155
|
+
)
|
|
156
|
+
except (OSError, subprocess.CalledProcessError):
|
|
157
|
+
return None
|
|
158
|
+
return parse_java_version(result.stderr or result.stdout or "")
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _installed(package: str) -> str | None:
|
|
162
|
+
try:
|
|
163
|
+
return version(package)
|
|
164
|
+
except PackageNotFoundError:
|
|
165
|
+
return None
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
#: How to install a package, per platform. A hint naming the wrong package
|
|
169
|
+
#: manager is worse than no hint — it sends someone to a command they do not
|
|
170
|
+
#: have. Keyed by `sys.platform`, with a fallback for everything else.
|
|
171
|
+
INSTALL_COMMANDS = {
|
|
172
|
+
"jdk": {
|
|
173
|
+
"darwin": "brew install openjdk@17",
|
|
174
|
+
"win32": "winget install Microsoft.OpenJDK.17",
|
|
175
|
+
None: "sudo apt install openjdk-17-jdk (or your distribution's JDK)",
|
|
176
|
+
},
|
|
177
|
+
"azure-cli": {
|
|
178
|
+
"darwin": "brew install azure-cli",
|
|
179
|
+
"win32": "winget install Microsoft.AzureCLI",
|
|
180
|
+
None: "see https://learn.microsoft.com/cli/azure/install-azure-cli",
|
|
181
|
+
},
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def install_command(what: str) -> str:
|
|
186
|
+
"""The command that installs ``what`` on this platform."""
|
|
187
|
+
|
|
188
|
+
choices = INSTALL_COMMANDS[what]
|
|
189
|
+
return choices.get(sys.platform, choices[None])
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def check_local_spark() -> LocalSparkReport:
|
|
193
|
+
"""Everything a local Delta build and load needs, checked in one pass."""
|
|
194
|
+
|
|
195
|
+
checks: list[Check] = []
|
|
196
|
+
|
|
197
|
+
python = ".".join(str(part) for part in sys.version_info[:3])
|
|
198
|
+
checks.append(
|
|
199
|
+
Check(
|
|
200
|
+
name="python",
|
|
201
|
+
found=python,
|
|
202
|
+
ok=sys.version_info[:2] >= SUPPORTED_PYTHON,
|
|
203
|
+
hint=f"weaverstack needs Python {'.'.join(map(str, SUPPORTED_PYTHON))} or later",
|
|
204
|
+
)
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
for package, supported in (("pyspark", SUPPORTED_PYSPARK), ("delta-spark", SUPPORTED_DELTA)):
|
|
208
|
+
found = _installed(package)
|
|
209
|
+
checks.append(
|
|
210
|
+
Check(
|
|
211
|
+
name=package,
|
|
212
|
+
found=found,
|
|
213
|
+
ok=found is not None and found.rsplit(".", 1)[0] in supported,
|
|
214
|
+
hint=(
|
|
215
|
+
# Not `-e '.[spark]'`: someone who installed from PyPI has
|
|
216
|
+
# no checkout for `.` to mean, and this is exactly the
|
|
217
|
+
# person who has not got Spark.
|
|
218
|
+
"install the optional extra: pip install 'weaverstack[spark]'"
|
|
219
|
+
if found is None
|
|
220
|
+
else f"{package} {'/'.join(supported)}.x is expected; "
|
|
221
|
+
"Spark and Delta are released in lockstep"
|
|
222
|
+
),
|
|
223
|
+
)
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
home = find_java_home()
|
|
227
|
+
found = java_version(home)
|
|
228
|
+
major = (found or "").split(".")[0] if found else None
|
|
229
|
+
checks.append(
|
|
230
|
+
Check(
|
|
231
|
+
name="java",
|
|
232
|
+
found=f"{found} ({home})" if found else None,
|
|
233
|
+
ok=major in SUPPORTED_JAVA,
|
|
234
|
+
hint=(
|
|
235
|
+
f"install a JDK Spark 3.5 supports: {install_command('jdk')}"
|
|
236
|
+
if found is None
|
|
237
|
+
else f"Spark 3.5 runs on Java {', '.join(SUPPORTED_JAVA)}; "
|
|
238
|
+
f"found {major}. Set JAVA_HOME to a supported JDK."
|
|
239
|
+
),
|
|
240
|
+
)
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
return LocalSparkReport(checks=tuple(checks))
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def platform_summary() -> str:
|
|
247
|
+
return f"{platform.system()} {platform.machine()}"
|
weaver/errors.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""The single Weaver error hierarchy.
|
|
2
|
+
|
|
3
|
+
Every error Weaver raises derives from :class:`WeaverError`, including errors
|
|
4
|
+
raised by the CLI adapter. Subclasses are added at the checkpoint that first
|
|
5
|
+
raises them rather than declared in advance.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class WeaverError(Exception):
|
|
12
|
+
"""Base class for every Weaver error."""
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class CommandError(WeaverError):
|
|
16
|
+
"""Raised when an explicitly requested operation is invalid."""
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ConfigError(WeaverError):
|
|
20
|
+
"""Raised when workspace configuration is invalid."""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class IdentityError(WeaverError):
|
|
24
|
+
"""Raised when a target, item or repository identity is malformed."""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class MetadataError(WeaverError):
|
|
28
|
+
"""Raised when an Weaver document document's metadata is missing, malformed or contradictory."""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class LoadError(WeaverError):
|
|
32
|
+
"""Raised when an object cannot be executed or its context is unavailable.
|
|
33
|
+
|
|
34
|
+
``result`` carries the load's counts when one was under way, because a
|
|
35
|
+
failure is still an outcome worth reporting: how many rows were read and how
|
|
36
|
+
many refused is exactly what a caller wants, and losing it to an exception
|
|
37
|
+
would force a second query against the reject table to find out.
|
|
38
|
+
|
|
39
|
+
It is optional, so the many places that raise this before a load has begun
|
|
40
|
+
stay unchanged.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def __init__(self, message: str, *, result: object | None = None) -> None:
|
|
44
|
+
super().__init__(message)
|
|
45
|
+
self.result = result
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class DiscoveryError(WeaverError):
|
|
49
|
+
"""Raised when a repository or source file breaks a structural rule."""
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class GraphError(WeaverError):
|
|
53
|
+
"""Raised for cycles or an unorderable dependency graph."""
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class BuildError(WeaverError):
|
|
57
|
+
"""Raised when a build bundle cannot be planned, written or validated."""
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class InstallError(WeaverError):
|
|
61
|
+
"""Raised when a build bundle cannot be installed."""
|