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
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."""