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/fabric/store.py ADDED
@@ -0,0 +1,198 @@
1
+ """Session-native storage for Weaver running inside Microsoft Fabric.
2
+
3
+ This is the within-workspace counterpart to :class:`OneLakeDfsClient`. It uses the
4
+ ``notebookutils.fs`` object already present in a Fabric Spark session and never
5
+ authenticates back across the workspace boundary.
6
+
7
+ Directory operations came first, for wipe. Byte reads and writes came next, for
8
+ installing a build bundle in-session — the installer reads ``plan.yml`` and the
9
+ generated payloads from OneLake and writes an install report back. They go
10
+ through ``notebookutils.fs.head``/``put``, which exchange UTF-8 text: a build
11
+ bundle's manifest and payloads are UTF-8, so this is exact for them. Arbitrary
12
+ binary artefacts do not pass through those methods: recursive repository
13
+ materialisation and bundle-archive persistence use ``notebookutils.fs.cp``
14
+ between OneLake and the driver's ``file:/tmp`` filesystem.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from pathlib import Path
20
+ from typing import Any
21
+
22
+ from ..errors import CommandError
23
+ from ..locations import Location
24
+ from ..store import Entry, StoreError
25
+
26
+ #: notebookutils.fs.head reads up to this many bytes. Bundle files are tiny; this
27
+ #: ceiling is only a guard against an unexpectedly large one.
28
+ _MAX_READ_BYTES = 256 * 1024 * 1024
29
+
30
+
31
+ class FabricStore:
32
+ """Within-workspace Fabric storage, backed by ``notebookutils.fs``."""
33
+
34
+ def __init__(self, fs: Any | None = None) -> None:
35
+ if fs is None:
36
+ try:
37
+ from notebookutils import fs as notebook_fs
38
+ except ImportError as exc:
39
+ raise CommandError(
40
+ "FabricStore is available only inside a Fabric session; "
41
+ "desktop access uses OneLakeDfsClient explicitly"
42
+ ) from exc
43
+ fs = notebook_fs
44
+ self.fs = fs
45
+
46
+ @staticmethod
47
+ def _path(location: Location) -> str:
48
+ if not isinstance(location, Location):
49
+ raise CommandError(
50
+ f"store operations take a Location, got {type(location).__name__}"
51
+ )
52
+ if not location.value.startswith("abfss://"):
53
+ raise CommandError(
54
+ f"FabricStore needs an abfss location, got {location.value!r}"
55
+ )
56
+ return location.value
57
+
58
+ def exists(self, location: Location) -> bool:
59
+ return bool(self.fs.exists(self._path(location)))
60
+
61
+ def is_directory(self, location: Location) -> bool:
62
+ path = self._path(location)
63
+ if not self.fs.exists(path):
64
+ return False
65
+ parent, _, name = path.rstrip("/").rpartition("/")
66
+ if not parent:
67
+ return True
68
+ for info in self.fs.ls(parent):
69
+ info_path = str(getattr(info, "path", "")).rstrip("/")
70
+ info_name = str(getattr(info, "name", "")).rstrip("/")
71
+ if info_path == path.rstrip("/") or info_name == name:
72
+ return bool(getattr(info, "isDir", False))
73
+ return False
74
+
75
+ def list(self, location: Location, *, recursive: bool = False) -> list[Entry]:
76
+ path = self._path(location)
77
+ if not self.fs.exists(path):
78
+ raise StoreError(f"cannot list a location that does not exist: {path}")
79
+
80
+ entries = self._list_once(location)
81
+ if not recursive:
82
+ return entries
83
+
84
+ found = list(entries)
85
+ pending = [entry.location for entry in entries if entry.is_directory]
86
+ while pending:
87
+ directory = pending.pop()
88
+ children = self._list_once(directory)
89
+ found.extend(children)
90
+ pending.extend(
91
+ child.location for child in children if child.is_directory
92
+ )
93
+ return found
94
+
95
+ def _list_once(self, location: Location) -> list[Entry]:
96
+ entries = []
97
+ for info in self.fs.ls(self._path(location)):
98
+ name = str(getattr(info, "name", "")).rstrip("/")
99
+ raw_path = str(getattr(info, "path", "")).rstrip("/")
100
+ child = (
101
+ Location(raw_path)
102
+ if raw_path.startswith("abfss://")
103
+ else location / name
104
+ )
105
+ is_directory = bool(getattr(info, "isDir", False))
106
+ entries.append(
107
+ Entry(
108
+ location=child,
109
+ is_directory=is_directory,
110
+ size=None if is_directory else int(getattr(info, "size", 0)),
111
+ )
112
+ )
113
+ return entries
114
+
115
+ def read(self, location: Location) -> bytes:
116
+ """The file's bytes, decoded as the UTF-8 text a bundle is made of."""
117
+
118
+ path = self._path(location)
119
+ if not self.fs.exists(path):
120
+ raise StoreError(f"cannot read a location that does not exist: {path}")
121
+ try:
122
+ text = self.fs.head(path, _MAX_READ_BYTES)
123
+ except Exception as exc: # notebookutils raises a bare Py4J error
124
+ raise StoreError(f"cannot read {location.value}: {exc}") from exc
125
+ return text.encode("utf-8")
126
+
127
+ def write(self, location: Location, data: bytes) -> None:
128
+ """Write UTF-8 text (a bundle manifest, a payload, an install report)."""
129
+
130
+ path = self._path(location)
131
+ try:
132
+ self.fs.put(path, data.decode("utf-8"), True)
133
+ except Exception as exc:
134
+ raise StoreError(f"cannot write {location.value}: {exc}") from exc
135
+
136
+ def delete(self, location: Location, *, recursive: bool = False) -> None:
137
+ if not self.fs.rm(self._path(location), recurse=recursive):
138
+ raise StoreError(f"could not delete {location.value}")
139
+
140
+ def make_directory(self, location: Location) -> None:
141
+ if not self.fs.mkdirs(self._path(location)):
142
+ raise StoreError(f"could not create directory {location.value}")
143
+
144
+ def copy_to_local(self, source: Location, destination: Path) -> None:
145
+ """Copy OneLake content to the driver and remove Hadoop checksum debris.
146
+
147
+ ``notebookutils.fs.cp`` writes ``.filename.crc`` sidecars through
148
+ Hadoop's checked local filesystem. They are transport metadata, not
149
+ repository content. A same-shaped file that really exists in OneLake is
150
+ retained, so strict discovery still sees authored input exactly.
151
+ """
152
+
153
+ source_is_directory = self.is_directory(source)
154
+ retained_checksums: set[str] = set()
155
+ if source_is_directory:
156
+ prefix = source.value.rstrip("/") + "/"
157
+ retained_checksums = {
158
+ entry.location.value[len(prefix) :]
159
+ for entry in self.list(source, recursive=True)
160
+ if not entry.is_directory and _is_checksum_sidecar(entry.name)
161
+ }
162
+ destination.parent.mkdir(parents=True, exist_ok=True)
163
+ target = f"file:{destination.as_posix()}"
164
+ try:
165
+ copied = self.fs.cp(
166
+ self._path(source), target, source_is_directory
167
+ )
168
+ except Exception as exc:
169
+ raise StoreError(
170
+ f"cannot materialise {source.value} at {destination}: {exc}"
171
+ ) from exc
172
+ if copied is False:
173
+ raise StoreError(f"could not materialise {source.value} at {destination}")
174
+ if source_is_directory:
175
+ for sidecar in destination.rglob(".*.crc"):
176
+ relative = sidecar.relative_to(destination).as_posix()
177
+ if relative not in retained_checksums:
178
+ sidecar.unlink()
179
+ else:
180
+ generated = destination.parent / f".{destination.name}.crc"
181
+ generated.unlink(missing_ok=True)
182
+
183
+ def copy_from_local(self, source: Path, destination: Location) -> None:
184
+ """Copy one driver-local file or tree into OneLake without text decoding."""
185
+
186
+ origin = f"file:{source.as_posix()}"
187
+ try:
188
+ copied = self.fs.cp(origin, self._path(destination), source.is_dir())
189
+ except Exception as exc:
190
+ raise StoreError(
191
+ f"cannot persist {source} at {destination.value}: {exc}"
192
+ ) from exc
193
+ if copied is False:
194
+ raise StoreError(f"could not persist {source} at {destination.value}")
195
+
196
+
197
+ def _is_checksum_sidecar(name: str) -> bool:
198
+ return name.startswith(".") and name.endswith(".crc")
weaver/initialise.py ADDED
@@ -0,0 +1,209 @@
1
+ """Bootstrapping the Weaver Lakehouse — Weaver installing its own control plane.
2
+
3
+ Initialisation composes the package-owned catalogue item into the parsed
4
+ repository in memory and builds it through the *ordinary* planner and installer. There is
5
+ deliberately no second "create the control tables" path: if the catalogue needed
6
+ privileged machinery to exist, the claim that a catalogue table is an ordinary
7
+ Weaver object would be false, and every later assumption resting on that claim
8
+ would be resting on nothing.
9
+
10
+ The bootstrap looks circular and is not. One bundle does the whole of it, because
11
+ the barriers already order it correctly:
12
+
13
+ .. code-block:: text
14
+
15
+ create schema `_` and the catalogue tables
16
+ publish dictionaries and Installation as one batch
17
+ certify them in Registry last
18
+
19
+ The catalogue's own DML runs after the tables it writes to exist, so no special
20
+ first-run mode is needed and generation reads nothing — the statements are
21
+ rendered from the projection and are correct against an absent catalogue as much
22
+ as a populated one.
23
+
24
+ The built-in ``_weaver`` item's inventory is scoped to the reserved ``_`` schema,
25
+ so the ordinary authoritative prune cannot touch application schemas that happen
26
+ to share the control Lakehouse.
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ from dataclasses import dataclass
32
+ from pathlib import Path
33
+ import tempfile
34
+ from typing import Any
35
+
36
+ from .build_bundle.models import BuildPlan
37
+ from .build_bundle.installer import InstallationEnvironment, install_bundle
38
+ from .build_bundle.planner import generate_item_build_bundle
39
+ from .build_bundle.report import InstallationReport
40
+ from .build_bundle.targets import ItemBinding, ItemBindings, LakehouseBinding
41
+ from .build_bundle.workflow import read_reconciled_catalogue, read_target_inventories
42
+ from .catalogue.tables import CATALOGUE_TABLES
43
+ from .declaration import parse_item_repository
44
+ from .declaration.model import WeaverItemId
45
+ from .errors import CommandError
46
+ from .locations import Location
47
+ from .resolution import resolver_for
48
+ from .store import LocalStore, Store
49
+ from .targets import ItemRef
50
+ from .workspaces import FabricWorkspace, LocalWorkspace
51
+
52
+ #: The driver-local bundle directory used while initialisation runs. A fixed
53
+ #: name is enough because the containing temporary directory is per invocation.
54
+ INITIALISE_BUNDLE_NAME = "weaver-initialise"
55
+
56
+
57
+ @dataclass(frozen=True)
58
+ class InitialiseResult:
59
+ """What initialisation did, in terms a caller can print or assert on."""
60
+
61
+ item: str
62
+ weaver_lakehouse: str
63
+ plan: BuildPlan
64
+ report: InstallationReport
65
+
66
+ @property
67
+ def succeeded(self) -> bool:
68
+ return self.report.status == "succeeded"
69
+
70
+ @property
71
+ def tables(self) -> tuple[str, ...]:
72
+ return tuple(table.qualified for table in CATALOGUE_TABLES)
73
+
74
+ def to_mapping(self) -> dict[str, Any]:
75
+ """A plain structure, for a CLI to serialise. The CLI owns no semantics."""
76
+
77
+ return {
78
+ "item": self.item,
79
+ "weaver_lakehouse": self.weaver_lakehouse,
80
+ "bundle_id": self.plan.bundle_id,
81
+ "status": self.report.status,
82
+ "tables": list(self.tables),
83
+ }
84
+
85
+
86
+ @dataclass(frozen=True)
87
+ class PreparedWeaverLakehouse:
88
+ workspace: str
89
+ weaver_lakehouse: str
90
+ created: bool
91
+
92
+
93
+ def prepare_weaver_lakehouse(
94
+ workspace,
95
+ *,
96
+ exists_ok: bool = False,
97
+ store: Store | None = None,
98
+ client=None,
99
+ ) -> PreparedWeaverLakehouse:
100
+ """Create the configured Weaver Lakehouse and its required Files areas."""
101
+
102
+ if not workspace.weaver_lakehouse:
103
+ raise CommandError("initialise requires a configured Weaver Lakehouse")
104
+ name = workspace.weaver_lakehouse
105
+ if isinstance(workspace, LocalWorkspace):
106
+ from .store import LocalStore
107
+
108
+ store = store or LocalStore()
109
+ resolver = resolver_for(workspace)
110
+ existed = store.exists(resolver.weaver_lakehouse)
111
+ if existed and not exists_ok:
112
+ raise CommandError(
113
+ f"Weaver Lakehouse {name!r} already exists; pass --exists-ok"
114
+ )
115
+ store.make_directory(resolver.files_root(ItemRef(name)))
116
+ store.make_directory(resolver.tables_root(ItemRef(name)))
117
+ return PreparedWeaverLakehouse(str(workspace.workspace), name, not existed)
118
+
119
+ if isinstance(workspace, FabricWorkspace):
120
+ from .fabric.resources import (
121
+ LAKEHOUSE,
122
+ ItemNotFoundError,
123
+ create_lakehouse,
124
+ find_item,
125
+ find_workspace,
126
+ )
127
+
128
+ physical_workspace = find_workspace(workspace.workspace, client=client)
129
+ try:
130
+ find_item(physical_workspace, name, item_type=LAKEHOUSE, client=client)
131
+ except ItemNotFoundError:
132
+ create_lakehouse(physical_workspace, name, client=client)
133
+ created = True
134
+ else:
135
+ if not exists_ok:
136
+ raise CommandError(
137
+ f"Weaver Lakehouse {name!r} already exists; pass --exists-ok"
138
+ )
139
+ created = False
140
+ return PreparedWeaverLakehouse(workspace.workspace, name, created)
141
+
142
+ raise CommandError(f"unsupported Workspace type: {type(workspace).__name__}")
143
+
144
+
145
+ def initialise_weaver_lakehouse(
146
+ *,
147
+ weaver_lakehouse: ItemRef,
148
+ workspace,
149
+ store: Store,
150
+ spark: Any = None,
151
+ output: Location | None = None,
152
+ ) -> InitialiseResult:
153
+ """Install Weaver's catalogue into the Weaver Lakehouse, through the normal build.
154
+
155
+ Idempotent to re-run in *shape*: the same package produces the same bundle, and
156
+ the catalogue's own reconciliation is a no-op when nothing changed.
157
+
158
+ An unchanged incremental plan emits no physical table work, so re-running
159
+ initialisation preserves existing catalogue rows while its catalogue tail
160
+ reconciles the built-in item.
161
+ """
162
+
163
+ resolver = resolver_for(workspace)
164
+ control = LakehouseBinding(lakehouse=weaver_lakehouse)
165
+ bindings = ItemBindings(
166
+ (
167
+ ItemBinding(
168
+ WeaverItemId.parse("Lakehouse/_weaver"),
169
+ control,
170
+ ),
171
+ )
172
+ )
173
+ environment = InstallationEnvironment(
174
+ store=store, resolver=resolver, spark=spark, workspace=workspace
175
+ )
176
+ with tempfile.TemporaryDirectory(prefix="weaver-initialise-") as temporary:
177
+ local_store = LocalStore()
178
+ repository_root = Path(temporary) / "repository"
179
+ repository_root.mkdir()
180
+ repository = parse_item_repository(
181
+ Location(repository_root.as_posix()), store=local_store
182
+ )
183
+ inventories = read_target_inventories(bindings, environment=environment)
184
+ reconciled = read_reconciled_catalogue(
185
+ bindings,
186
+ inventories=inventories,
187
+ environment=environment,
188
+ repository=repository,
189
+ )
190
+ bundle = generate_item_build_bundle(
191
+ repository,
192
+ bindings=bindings,
193
+ output=output
194
+ or Location((Path(temporary) / INITIALISE_BUNDLE_NAME).as_posix()),
195
+ store=local_store,
196
+ control_lakehouse=control,
197
+ target_inventories=inventories,
198
+ catalogue=reconciled.catalogue,
199
+ stale_claims=reconciled.stale_claims,
200
+ )
201
+
202
+ report = install_bundle(bundle, environment=environment)
203
+
204
+ return InitialiseResult(
205
+ item="Lakehouse/_weaver",
206
+ weaver_lakehouse=weaver_lakehouse.name,
207
+ plan=bundle.plan,
208
+ report=report,
209
+ )