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
@@ -0,0 +1,238 @@
1
+ """The built-in Weaver document repository that declares the catalogue tables.
2
+
3
+ Weaver's catalogue is built by Weaver, from ordinary Weaver document, through the ordinary
4
+ planner and installer. There is no second "create the control tables" path — that
5
+ recursion is the point, and it is the proof that a catalogue table is an ordinary
6
+ Weaver object rather than a privileged one.
7
+
8
+ The item is rendered from :mod:`weaver.catalogue.tables`, so the declaration and
9
+ the table definitions cannot drift: there is one source of truth and the text is
10
+ derived from it. ``Lakehouse/_weaver`` is composed into the parsed repository
11
+ in memory and built through the ordinary planner.
12
+
13
+ Every table declares:
14
+
15
+ ``Static: true``
16
+ Its rows are not produced by a load. Catalogue rows are maintained only by
17
+ the DML a build appends.
18
+
19
+ ``Prohibit rebuild: true``
20
+ This stops an ordinary build treating the catalogue as a disposable
21
+ application object.
22
+
23
+ ``Dependencies: []``
24
+ Explicitly nothing. The body is literals, so there is nothing to discover
25
+ and nothing to declare.
26
+ """
27
+
28
+ from __future__ import annotations
29
+
30
+ import textwrap
31
+ from dataclasses import replace
32
+
33
+ from .tables import CATALOGUE_SCHEMA, CATALOGUE_TABLES, CatalogueColumn, CatalogueTable
34
+
35
+ #: The reserved item Weaver generates and manages inside the declaration.
36
+ ITEM_ROOT = "Lakehouse/_weaver"
37
+ SCHEMA_PATH = f"{ITEM_ROOT}/schemas/{CATALOGUE_SCHEMA}.yml"
38
+
39
+ #: The folder every top-level Weaver task writes its evidence beneath, and the
40
+ #: document that declares it. ``_`` + ``__`` + ``Log`` spells ``___Log``, which
41
+ #: the parser reads as ``_.Log`` — see
42
+ #: :func:`weaver.declaration.source.python_id_parts`.
43
+ LOG_FOLDER = "Log"
44
+ LOG_FOLDER_ID = f"{CATALOGUE_SCHEMA}.{LOG_FOLDER}"
45
+ LOG_PATH = f"{ITEM_ROOT}/Files/{CATALOGUE_SCHEMA}{'__'}{LOG_FOLDER}.py"
46
+
47
+ #: Where the folder puts its files, relative to the Weaver Lakehouse's ``Files``
48
+ #: area. Derived from the identity exactly as any Folder's location is, so the
49
+ #: logger addresses the *declared* folder rather than a path it happens to know.
50
+ LOG_ROOT = f"{CATALOGUE_SCHEMA}/{LOG_FOLDER}"
51
+
52
+ _LOG_CLASS = f"{CATALOGUE_SCHEMA}{'__'}{LOG_FOLDER}"
53
+
54
+ #: One sentence, the same on every table, saying where the rows come from. It is
55
+ #: not boilerplate: "never loaded" is the fact that makes ``Static: true``
56
+ #: correct, and a reader of any one file should be told it.
57
+ LINEAGE = (
58
+ "Projected from validated Weaver document declarations by Weaver's own build, and "
59
+ "maintained only by the catalogue DML a build appends. Never populated by a "
60
+ "load."
61
+ )
62
+
63
+ SCHEMA_DESCRIPTION = (
64
+ "Weaver's own control plane. These tables record what Weaver has built and "
65
+ "what it certifies as installed; they are declared as ordinary Weaver document and built "
66
+ "by Weaver itself, and are never authored or loaded by hand."
67
+ )
68
+
69
+ _WIDTH = 76
70
+
71
+
72
+ def _escaped(text: str) -> str:
73
+ """Metadata text with dollars escaped.
74
+
75
+ A ``$`` opens a ``$Schema.Object`` reference, and several catalogue columns are
76
+ described in terms of one — ``description_reference`` holds "the
77
+ $Schema.Object the description was copied from". Written raw, that would parse
78
+ as a reference and be refused, so it is escaped the way Weaver document specifies.
79
+ """
80
+
81
+ return text.replace("$", "$$")
82
+
83
+
84
+ def _folded(key: str, text: str, *, indent: int = 0) -> str:
85
+ """A YAML folded block, so prose can wrap without becoming multi-line text.
86
+
87
+ ``>-`` folds newlines into spaces and strips the trailing one, which keeps the
88
+ parsed value a single sentence however it is laid out in the file. Used
89
+ uniformly rather than only when needed: a plain scalar is fine until a
90
+ description happens to contain a colon, and this removes the class of problem.
91
+ """
92
+
93
+ pad = " " * indent
94
+ body = textwrap.fill(
95
+ _escaped(text), width=_WIDTH - indent - 2, initial_indent="", subsequent_indent=""
96
+ )
97
+ lines = "\n".join(f"{pad} {line}" for line in body.splitlines())
98
+ return f"{pad}{key}: >-\n{lines}"
99
+
100
+
101
+ def render_schema_file() -> str:
102
+ """The ``_schemas/_.yml`` declaration for the catalogue schema."""
103
+
104
+ return (
105
+ f"Schema ID: {CATALOGUE_SCHEMA}\n"
106
+ "\n"
107
+ f"{_folded('Description', SCHEMA_DESCRIPTION)}\n"
108
+ )
109
+
110
+
111
+ def _body(table: CatalogueTable) -> str:
112
+ """A query that declares the shape and returns no rows.
113
+
114
+ ``where 1 = 0`` with no ``FROM`` is valid Spark SQL and is the whole trick: the
115
+ executor resolves the query's schema to create the table, and resolving a
116
+ schema reads no rows. Build creates structure; this is the smallest possible
117
+ statement that describes one.
118
+ """
119
+
120
+ def line(column: CatalogueColumn, first: bool) -> str:
121
+ lead = "select" if first else " ,"
122
+ return f"{lead} cast(null as {column.type}) as `{column.name}`"
123
+
124
+ lines = [line(column, index == 0) for index, column in enumerate(table.columns)]
125
+ return "\n".join(lines) + "\n where 1 = 0\n"
126
+
127
+
128
+ def render_source(table: CatalogueTable) -> str:
129
+ """The complete Weaver document source file for one catalogue table."""
130
+
131
+ not_null = [
132
+ column.name
133
+ for column in table.columns
134
+ if column.not_null and column.name not in table.key
135
+ ]
136
+
137
+ sections: list[str] = [
138
+ f"Table ID: {table.qualified}",
139
+ _folded("Description", table.description),
140
+ _folded("Lineage", LINEAGE),
141
+ "Dependencies: []",
142
+ "Static: true",
143
+ "Prohibit rebuild: true",
144
+ # The key is declared as the primary key, so the catalogue's own tables
145
+ # describe themselves: Weaver document makes key columns not null, and the projection
146
+ # records the key in the catalogue like any other object's.
147
+ f"Primary key: {', '.join(table.key)}",
148
+ ]
149
+ if not_null:
150
+ sections.append("Not null:\n" + "\n".join(f" - {name}" for name in not_null))
151
+ sections.append(
152
+ "Schema:\n"
153
+ + "\n".join(f" {column.name}: {column.type}" for column in table.columns)
154
+ )
155
+ sections.append(
156
+ "Column notes:\n"
157
+ + "\n".join(
158
+ _folded(column.name, column.description, indent=2)
159
+ for column in table.columns
160
+ )
161
+ )
162
+
163
+ header = "\n\n".join(sections)
164
+ return f"/*\n{header}\n*/\n{_body(table)}"
165
+
166
+
167
+ def render_log_file() -> str:
168
+ """The declaration for ``Files/_/Log`` — where task evidence is written.
169
+
170
+ An ordinary Folder document, and that is the whole point. A task log could
171
+ have been a path the logger alone knew about, but then its creation, its
172
+ registration, its survival through a prune and its removal would each need a
173
+ rule of their own. Declared here it is claimed, projected, inventoried,
174
+ installed, converged and protected by the machinery that already exists, and
175
+ the logger asks the *folder* where to write rather than composing a path.
176
+
177
+ ``Static: true`` because nothing loads into it: a task writes its own
178
+ evidence beneath it, exactly as a Folder object's authored code writes files
179
+ into its destination. ``Incremental: false`` for the same reason the runtime
180
+ tree declares it — the folder itself accumulates nothing that Weaver claims
181
+ file by file.
182
+
183
+ ``File key`` claims everything beneath, because everything beneath *is*
184
+ Weaver's — task evidence and nothing else. It is an accurate statement of
185
+ ownership rather than a licence to delete: nothing loads this folder, and a
186
+ written task file is never rewritten.
187
+ """
188
+
189
+ return f'''\
190
+ """
191
+ Folder ID: {LOG_FOLDER_ID}
192
+
193
+ Description: >-
194
+ Where every top-level Weaver task — wipe, mirror, build, load and test —
195
+ writes its immutable evidence. One folder per task, partitioned by the UTC
196
+ date the task started.
197
+
198
+ Lineage: >-
199
+ Written by Weaver's own task logger as each top-level task runs. Never
200
+ authored, and never populated by a load.
201
+
202
+ File key: "**/*"
203
+
204
+ Incremental: false
205
+
206
+ Static: true
207
+ """
208
+ from weaver import Folder
209
+
210
+
211
+ class {_LOG_CLASS}(Folder):
212
+ def read(self):
213
+ return self.staging_folder(), []
214
+ '''
215
+
216
+
217
+ def render_item_sources() -> dict[str, str]:
218
+ sources = {SCHEMA_PATH: render_schema_file(), LOG_PATH: render_log_file()}
219
+ for table in CATALOGUE_TABLES:
220
+ documented = replace(
221
+ table,
222
+ columns=tuple(
223
+ replace(
224
+ column,
225
+ description=column.description
226
+ or f"The catalogue value for {column.name.replace('_', ' ')}.",
227
+ )
228
+ for column in table.columns
229
+ ),
230
+ )
231
+ sources[f"{ITEM_ROOT}/{table.qualified}.sql"] = render_source(documented)
232
+ return sources
233
+
234
+
235
+ def item_repository_files() -> dict[str, bytes]:
236
+ return {
237
+ path: text.encode("utf-8") for path, text in render_item_sources().items()
238
+ }
@@ -0,0 +1,121 @@
1
+ """Explicit catalogue ownership rules for Weaver document types.
2
+
3
+ Catalogue rows are not owned merely because their table happens to contain an
4
+ object-shaped pair of columns. Each supported document type names every table
5
+ it may populate and the predicate by which rows in that table are its claims.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass
11
+ from typing import Mapping
12
+
13
+ from ..declaration.metadata import FOLDER, TABLE, VIEW
14
+ from ..declaration.model import WeaverDocumentId
15
+ from ..errors import BuildError
16
+ from .tables import (
17
+ COLUMN_DICTIONARY,
18
+ DEPENDENCY,
19
+ FOLDER_DICTIONARY,
20
+ FOREIGN_KEY_DICTIONARY,
21
+ INDEX_DICTIONARY,
22
+ OBJECT_TYPES,
23
+ REGISTRY,
24
+ TABLE_DICTIONARY,
25
+ CatalogueTable,
26
+ )
27
+
28
+
29
+ def catalogue_schema(identity: WeaverDocumentId) -> str:
30
+ """The ``schema_name`` this identity is stored under.
31
+
32
+ A Folder carries its ``Files/`` prefix because that prefix is part of what
33
+ distinguishes it from a table of the same name. A load artefact does not get
34
+ one: its schema is already the real thing — the containing path for a file,
35
+ the Warehouse schema for a procedure — and prefixing it would store something
36
+ that is not the target's own name.
37
+ """
38
+
39
+ prefix = "Files/" if identity.is_files else ""
40
+ return f"{prefix}{identity.object_id.schema}"
41
+
42
+
43
+ @dataclass(frozen=True)
44
+ class CatalogueClaimRule:
45
+ """One table a document type may populate and how it owns its rows."""
46
+
47
+ table: CatalogueTable
48
+ predicate_columns: tuple[str, str] = ("schema_name", "object_name")
49
+
50
+ def values(self, identity: WeaverDocumentId) -> tuple[str, str]:
51
+ return catalogue_schema(identity), identity.object_id.object
52
+
53
+ def owns(self, row: Mapping[str, object], identity: WeaverDocumentId) -> bool:
54
+ expected = self.values(identity)
55
+ return all(
56
+ str(row.get(column)) == value
57
+ for column, value in zip(self.predicate_columns, expected)
58
+ )
59
+
60
+
61
+ @dataclass(frozen=True)
62
+ class CatalogueClaim:
63
+ """A concrete document claim collected for deletion."""
64
+
65
+ identity: WeaverDocumentId
66
+ rule: CatalogueClaimRule
67
+
68
+
69
+ _COMMON_OBJECT_RULES = (
70
+ CatalogueClaimRule(REGISTRY),
71
+ CatalogueClaimRule(COLUMN_DICTIONARY),
72
+ CatalogueClaimRule(INDEX_DICTIONARY),
73
+ CatalogueClaimRule(FOREIGN_KEY_DICTIONARY),
74
+ CatalogueClaimRule(DEPENDENCY),
75
+ )
76
+
77
+ # This is deliberately exhaustive. Adding another Registry object_type requires
78
+ # an ownership declaration here before it can participate in reconciliation.
79
+ CATALOGUE_CLAIMS_BY_OBJECT_TYPE: Mapping[str, tuple[CatalogueClaimRule, ...]] = {
80
+ "folder": (
81
+ _COMMON_OBJECT_RULES[0],
82
+ CatalogueClaimRule(FOLDER_DICTIONARY),
83
+ *_COMMON_OBJECT_RULES[1:],
84
+ ),
85
+ "table": (
86
+ _COMMON_OBJECT_RULES[0],
87
+ CatalogueClaimRule(TABLE_DICTIONARY),
88
+ *_COMMON_OBJECT_RULES[1:],
89
+ ),
90
+ "view": (
91
+ _COMMON_OBJECT_RULES[0],
92
+ CatalogueClaimRule(TABLE_DICTIONARY),
93
+ *_COMMON_OBJECT_RULES[1:],
94
+ ),
95
+ # A load artefact claims the Registry and nothing else. It declares no
96
+ # columns, no keys, no relationships and no dependencies — it is a deployed
97
+ # module or a generated statement, and the only thing the catalogue records
98
+ # about it is that Weaver installed it and at what signature.
99
+ "file": (CatalogueClaimRule(REGISTRY),),
100
+ "stored_procedure": (CatalogueClaimRule(REGISTRY),),
101
+ }
102
+
103
+ OBJECT_TYPE_FOR_DOCUMENT_KIND = {FOLDER: "folder", TABLE: "table", VIEW: "view"}
104
+
105
+
106
+ def claim_rules_for_object_type(object_type: str) -> tuple[CatalogueClaimRule, ...]:
107
+ try:
108
+ return CATALOGUE_CLAIMS_BY_OBJECT_TYPE[object_type]
109
+ except KeyError as exc:
110
+ expected = ", ".join(OBJECT_TYPES)
111
+ raise BuildError(
112
+ f"Registry object_type must be one of {expected}, got {object_type!r}"
113
+ ) from exc
114
+
115
+
116
+ def claim_rules_for_document_kind(kind: str) -> tuple[CatalogueClaimRule, ...]:
117
+ try:
118
+ object_type = OBJECT_TYPE_FOR_DOCUMENT_KIND[kind]
119
+ except KeyError as exc:
120
+ raise BuildError(f"unsupported Weaver document kind {kind!r}") from exc
121
+ return claim_rules_for_object_type(object_type)