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,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"]