tablevalidator-databricks 0.1.2__tar.gz → 0.1.4__tar.gz
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.
- {tablevalidator_databricks-0.1.2 → tablevalidator_databricks-0.1.4}/PKG-INFO +14 -7
- {tablevalidator_databricks-0.1.2 → tablevalidator_databricks-0.1.4}/README.md +13 -6
- tablevalidator_databricks-0.1.4/tablevalidator_databricks/generator/generator.py +66 -0
- {tablevalidator_databricks-0.1.2 → tablevalidator_databricks-0.1.4}/tablevalidator_databricks/templates/widget_notebook.py.tmpl +45 -21
- {tablevalidator_databricks-0.1.2 → tablevalidator_databricks-0.1.4}/tablevalidator_databricks.egg-info/PKG-INFO +14 -7
- {tablevalidator_databricks-0.1.2 → tablevalidator_databricks-0.1.4}/tablevalidator_databricks.egg-info/scm_version.json +2 -2
- {tablevalidator_databricks-0.1.2 → tablevalidator_databricks-0.1.4}/tests/test_generated_notebook_execution.py +71 -23
- {tablevalidator_databricks-0.1.2 → tablevalidator_databricks-0.1.4}/tests/test_generator.py +28 -1
- tablevalidator_databricks-0.1.2/tablevalidator_databricks/generator/generator.py +0 -39
- {tablevalidator_databricks-0.1.2 → tablevalidator_databricks-0.1.4}/pyproject.toml +0 -0
- {tablevalidator_databricks-0.1.2 → tablevalidator_databricks-0.1.4}/setup.cfg +0 -0
- {tablevalidator_databricks-0.1.2 → tablevalidator_databricks-0.1.4}/tablevalidator_databricks/__init__.py +0 -0
- {tablevalidator_databricks-0.1.2 → tablevalidator_databricks-0.1.4}/tablevalidator_databricks/cli/__init__.py +0 -0
- {tablevalidator_databricks-0.1.2 → tablevalidator_databricks-0.1.4}/tablevalidator_databricks/cli/main.py +0 -0
- {tablevalidator_databricks-0.1.2 → tablevalidator_databricks-0.1.4}/tablevalidator_databricks/generator/__init__.py +0 -0
- {tablevalidator_databricks-0.1.2 → tablevalidator_databricks-0.1.4}/tablevalidator_databricks.egg-info/SOURCES.txt +0 -0
- {tablevalidator_databricks-0.1.2 → tablevalidator_databricks-0.1.4}/tablevalidator_databricks.egg-info/dependency_links.txt +0 -0
- {tablevalidator_databricks-0.1.2 → tablevalidator_databricks-0.1.4}/tablevalidator_databricks.egg-info/entry_points.txt +0 -0
- {tablevalidator_databricks-0.1.2 → tablevalidator_databricks-0.1.4}/tablevalidator_databricks.egg-info/requires.txt +0 -0
- {tablevalidator_databricks-0.1.2 → tablevalidator_databricks-0.1.4}/tablevalidator_databricks.egg-info/scm_file_list.json +0 -0
- {tablevalidator_databricks-0.1.2 → tablevalidator_databricks-0.1.4}/tablevalidator_databricks.egg-info/top_level.txt +0 -0
- {tablevalidator_databricks-0.1.2 → tablevalidator_databricks-0.1.4}/tests/test_cli.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tablevalidator-databricks
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
4
4
|
Summary: Generates a widget-driven Databricks notebook UI on top of the table-validator package - no code required to run a comparison.
|
|
5
5
|
License: MIT
|
|
6
6
|
Requires-Python: >=3.9
|
|
@@ -45,12 +45,19 @@ Writes `./TableValidator.py` - a ready-to-use Databricks notebook. Options:
|
|
|
45
45
|
tablevalidator-databricks init --output MyValidation.py --mode full
|
|
46
46
|
```
|
|
47
47
|
|
|
48
|
-
- `--mode basic` - only the check-group
|
|
48
|
+
- `--mode basic` - only the check-group Yes/No dropdowns and table pickers.
|
|
49
49
|
- `--mode full` (default) - also shows optional filter widgets (only/
|
|
50
|
-
ignore columns, row filter, primary key).
|
|
51
|
-
- `--mode schema` - same widgets as `basic
|
|
52
|
-
schema-shape-only comparison
|
|
53
|
-
|
|
50
|
+
ignore columns, row filter, primary key). Every check defaults to "Yes".
|
|
51
|
+
- `--mode schema` - same widgets as `basic`, but the Row checks default to
|
|
52
|
+
"No" - a lightweight schema-shape-only comparison out of the box (still
|
|
53
|
+
changeable per-run).
|
|
54
|
+
|
|
55
|
+
> **Note on the check groups**: "Catalog & Schema" only verifies that
|
|
56
|
+
> catalogs/schemas/tables exist and match by name - it produces no
|
|
57
|
+
> per-table PASS/FAIL by itself, so running it alone reports every table
|
|
58
|
+
> as `SKIPPED`. The per-table verdict comes from the **Column** checks
|
|
59
|
+
> (names, types, nullability, statistics) and **Row** checks (row counts,
|
|
60
|
+
> row-level data).
|
|
54
61
|
|
|
55
62
|
Then, in your Databricks workspace: **Workspace -> Import**, format
|
|
56
63
|
"Source", and select the generated file. Databricks recognizes the
|
|
@@ -63,7 +70,7 @@ markers and opens it as a real notebook.
|
|
|
63
70
|
2. Queries Unity Catalog (`SHOW CATALOGS` / `SHOW SCHEMAS IN ...` /
|
|
64
71
|
`SHOW TABLES IN ...` via the notebook's own ambient Spark session) to
|
|
65
72
|
populate Source/Target Catalog, Schema, and Table dropdowns.
|
|
66
|
-
3. Shows a
|
|
73
|
+
3. Shows a Yes/No dropdown per check group (Catalog & Schema / Column /
|
|
67
74
|
Row - the three independently-selectable check groups the engine
|
|
68
75
|
actually supports) plus, in `full` mode, optional text widgets for
|
|
69
76
|
column filtering, a row filter, and a primary key.
|
|
@@ -31,12 +31,19 @@ Writes `./TableValidator.py` - a ready-to-use Databricks notebook. Options:
|
|
|
31
31
|
tablevalidator-databricks init --output MyValidation.py --mode full
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
-
- `--mode basic` - only the check-group
|
|
34
|
+
- `--mode basic` - only the check-group Yes/No dropdowns and table pickers.
|
|
35
35
|
- `--mode full` (default) - also shows optional filter widgets (only/
|
|
36
|
-
ignore columns, row filter, primary key).
|
|
37
|
-
- `--mode schema` - same widgets as `basic
|
|
38
|
-
schema-shape-only comparison
|
|
39
|
-
|
|
36
|
+
ignore columns, row filter, primary key). Every check defaults to "Yes".
|
|
37
|
+
- `--mode schema` - same widgets as `basic`, but the Row checks default to
|
|
38
|
+
"No" - a lightweight schema-shape-only comparison out of the box (still
|
|
39
|
+
changeable per-run).
|
|
40
|
+
|
|
41
|
+
> **Note on the check groups**: "Catalog & Schema" only verifies that
|
|
42
|
+
> catalogs/schemas/tables exist and match by name - it produces no
|
|
43
|
+
> per-table PASS/FAIL by itself, so running it alone reports every table
|
|
44
|
+
> as `SKIPPED`. The per-table verdict comes from the **Column** checks
|
|
45
|
+
> (names, types, nullability, statistics) and **Row** checks (row counts,
|
|
46
|
+
> row-level data).
|
|
40
47
|
|
|
41
48
|
Then, in your Databricks workspace: **Workspace -> Import**, format
|
|
42
49
|
"Source", and select the generated file. Databricks recognizes the
|
|
@@ -49,7 +56,7 @@ markers and opens it as a real notebook.
|
|
|
49
56
|
2. Queries Unity Catalog (`SHOW CATALOGS` / `SHOW SCHEMAS IN ...` /
|
|
50
57
|
`SHOW TABLES IN ...` via the notebook's own ambient Spark session) to
|
|
51
58
|
populate Source/Target Catalog, Schema, and Table dropdowns.
|
|
52
|
-
3. Shows a
|
|
59
|
+
3. Shows a Yes/No dropdown per check group (Catalog & Schema / Column /
|
|
53
60
|
Row - the three independently-selectable check groups the engine
|
|
54
61
|
actually supports) plus, in `full` mode, optional text widgets for
|
|
55
62
|
column filtering, a row filter, and a primary key.
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Renders the bundled widget_notebook.py.tmpl into a real Databricks
|
|
3
|
+
notebook file. No Jinja2 - the template has only a handful of
|
|
4
|
+
placeholders, substituted via plain str.replace(), so a templating
|
|
5
|
+
dependency isn't worth adding for this.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import importlib.resources
|
|
11
|
+
import re
|
|
12
|
+
from importlib.metadata import PackageNotFoundError, requires
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
VALID_MODES = ("basic", "full", "schema")
|
|
16
|
+
|
|
17
|
+
# Fallback floor for the generated notebook's `%pip install
|
|
18
|
+
# "table-validator>=X"` line, used only if this package's own declared
|
|
19
|
+
# dependency can't be read at runtime. validate_tables() - the API the
|
|
20
|
+
# generated notebook calls - first shipped in table-validator 0.1.9.
|
|
21
|
+
_FALLBACK_MIN_CORE_VERSION = "0.1.9"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _min_core_version() -> str:
|
|
25
|
+
"""The minimum `table-validator` version the generated notebook should
|
|
26
|
+
install, read from THIS package's own declared dependency so the two
|
|
27
|
+
can never drift apart. Falls back to a known-good floor if the
|
|
28
|
+
metadata isn't readable (e.g. running from a source tree that was
|
|
29
|
+
never installed)."""
|
|
30
|
+
try:
|
|
31
|
+
for req in requires("tablevalidator-databricks") or []:
|
|
32
|
+
# e.g. "table-validator>=0.1.19" / "table-validator >= 0.1.19"
|
|
33
|
+
match = re.match(r"^\s*table[-_]validator\s*>=\s*([0-9][^\s,;]*)", req)
|
|
34
|
+
if match:
|
|
35
|
+
return match.group(1)
|
|
36
|
+
except PackageNotFoundError:
|
|
37
|
+
pass
|
|
38
|
+
return _FALLBACK_MIN_CORE_VERSION
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def generate_notebook(output: Path, mode: str) -> None:
|
|
42
|
+
"""Write a widget-driven Databricks notebook to `output`.
|
|
43
|
+
|
|
44
|
+
mode controls which optional widgets/pre-selected checks the notebook
|
|
45
|
+
starts with - see tablevalidator_databricks/cli/main.py's `init`
|
|
46
|
+
command docstring for what each mode means. The underlying comparison
|
|
47
|
+
logic is identical regardless of mode; only the widgets shown differ,
|
|
48
|
+
so all three modes render from the same template file rather than
|
|
49
|
+
three separate ones.
|
|
50
|
+
"""
|
|
51
|
+
if mode not in VALID_MODES:
|
|
52
|
+
raise ValueError(f"mode must be one of {VALID_MODES}, got {mode!r}")
|
|
53
|
+
|
|
54
|
+
template = (
|
|
55
|
+
importlib.resources.files("tablevalidator_databricks.templates")
|
|
56
|
+
.joinpath("widget_notebook.py.tmpl")
|
|
57
|
+
.read_text(encoding="utf-8")
|
|
58
|
+
)
|
|
59
|
+
rendered = (
|
|
60
|
+
template.replace("{{MODE}}", mode)
|
|
61
|
+
.replace("{{SHOW_OPTIONAL_WIDGETS}}", "True" if mode == "full" else "False")
|
|
62
|
+
.replace("{{MIN_CORE_VERSION}}", _min_core_version())
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
output.parent.mkdir(parents=True, exist_ok=True)
|
|
66
|
+
output.write_text(rendered, encoding="utf-8")
|
|
@@ -31,7 +31,19 @@
|
|
|
31
31
|
|
|
32
32
|
# COMMAND ----------
|
|
33
33
|
|
|
34
|
-
# MAGIC %
|
|
34
|
+
# MAGIC %md ### Install the validation engine
|
|
35
|
+
# MAGIC Note the `>=` version floor: `validate_tables()` (the API every cell
|
|
36
|
+
# MAGIC below calls) only exists in `table-validator` {{MIN_CORE_VERSION}} and
|
|
37
|
+
# MAGIC later. **Do not pin this to a lower version** - in particular, do not
|
|
38
|
+
# MAGIC confuse it with the `tablevalidator-databricks` version that generated
|
|
39
|
+
# MAGIC this notebook; the two packages are versioned completely independently,
|
|
40
|
+
# MAGIC and pinning the engine to the generator's version number installs an
|
|
41
|
+
# MAGIC engine far too old, failing with
|
|
42
|
+
# MAGIC `ImportError: cannot import name 'validate_tables'`.
|
|
43
|
+
|
|
44
|
+
# COMMAND ----------
|
|
45
|
+
|
|
46
|
+
# MAGIC %pip install "table-validator>={{MIN_CORE_VERSION}}"
|
|
35
47
|
|
|
36
48
|
# COMMAND ----------
|
|
37
49
|
|
|
@@ -177,21 +189,35 @@ print(
|
|
|
177
189
|
# COMMAND ----------
|
|
178
190
|
|
|
179
191
|
# MAGIC %md ### Validation checks
|
|
192
|
+
# MAGIC "Catalog & Schema" only checks that the catalogs/schemas/tables exist
|
|
193
|
+
# MAGIC and match by name - it produces no per-table PASS/FAIL on its own, so
|
|
194
|
+
# MAGIC running it alone reports every table as **SKIPPED**. That's expected,
|
|
195
|
+
# MAGIC not an error: the per-table verdict comes from the **Column** checks
|
|
196
|
+
# MAGIC (names, types, nullability, statistics) and **Row** checks (row counts
|
|
197
|
+
# MAGIC and row-level data). Leave those on unless you specifically want a
|
|
198
|
+
# MAGIC name-level-only existence check.
|
|
180
199
|
|
|
181
200
|
# COMMAND ----------
|
|
182
201
|
|
|
183
|
-
|
|
184
|
-
# dbutils.widgets.multiselect
|
|
185
|
-
#
|
|
186
|
-
#
|
|
187
|
-
#
|
|
188
|
-
#
|
|
189
|
-
#
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
#
|
|
193
|
-
#
|
|
194
|
-
|
|
202
|
+
# One Yes/No dropdown per check group, instead of a single multiselect -
|
|
203
|
+
# dbutils.widgets.multiselect enforces an internal "selection sequence"
|
|
204
|
+
# validation against its own default that was observed to reject even a
|
|
205
|
+
# same-length, same-items default on some Databricks runtimes/existing-
|
|
206
|
+
# widget states (DefaultValueNotInChoicesList), with no reliable
|
|
207
|
+
# workaround. Three independent dropdowns have no such cross-widget
|
|
208
|
+
# validation and are the more robust primitive for this.
|
|
209
|
+
_YES_NO = ["Yes", "No"]
|
|
210
|
+
_schema_default = "Yes"
|
|
211
|
+
# --mode schema means "schema SHAPE only" - that shape (column names,
|
|
212
|
+
# types, nullability) is validated by the COLUMN checks, so Column stays
|
|
213
|
+
# on and only the expensive Row checks (row counts + row-level data) are
|
|
214
|
+
# off by default. Turning Column off too would leave nothing producing a
|
|
215
|
+
# per-table verdict, reporting every table as SKIPPED.
|
|
216
|
+
_column_default = "Yes"
|
|
217
|
+
_row_default = "No" if MODE == "schema" else "Yes"
|
|
218
|
+
dbutils.widgets.dropdown("check_catalog_schema", _schema_default, _YES_NO, "Run Catalog & Schema checks?")
|
|
219
|
+
dbutils.widgets.dropdown("check_column", _column_default, _YES_NO, "Run Column checks?")
|
|
220
|
+
dbutils.widgets.dropdown("check_row", _row_default, _YES_NO, "Run Row checks?")
|
|
195
221
|
|
|
196
222
|
if SHOW_OPTIONAL_WIDGETS:
|
|
197
223
|
dbutils.widgets.text("only_columns", "", "Only compare these columns (comma-separated, optional)")
|
|
@@ -279,15 +305,13 @@ def _source_or_target_string(prefix: str) -> str:
|
|
|
279
305
|
source = _source_or_target_string("source")
|
|
280
306
|
target = _source_or_target_string("target")
|
|
281
307
|
|
|
282
|
-
_CHECK_MAP = {
|
|
283
|
-
"Catalog & Schema": {ValidationType.CATALOG, ValidationType.SCHEMA},
|
|
284
|
-
"Column": {ValidationType.COLUMN},
|
|
285
|
-
"Row": {ValidationType.ROW},
|
|
286
|
-
}
|
|
287
|
-
selected_checks = [c for c in dbutils.widgets.get("checks").split(",") if c]
|
|
288
308
|
enabled_validations = set()
|
|
289
|
-
|
|
290
|
-
enabled_validations |=
|
|
309
|
+
if dbutils.widgets.get("check_catalog_schema") == "Yes":
|
|
310
|
+
enabled_validations |= {ValidationType.CATALOG, ValidationType.SCHEMA}
|
|
311
|
+
if dbutils.widgets.get("check_column") == "Yes":
|
|
312
|
+
enabled_validations |= {ValidationType.COLUMN}
|
|
313
|
+
if dbutils.widgets.get("check_row") == "Yes":
|
|
314
|
+
enabled_validations |= {ValidationType.ROW}
|
|
291
315
|
if not enabled_validations:
|
|
292
316
|
enabled_validations = None # fall back to validate_tables()'s own "run everything" default
|
|
293
317
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tablevalidator-databricks
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
4
4
|
Summary: Generates a widget-driven Databricks notebook UI on top of the table-validator package - no code required to run a comparison.
|
|
5
5
|
License: MIT
|
|
6
6
|
Requires-Python: >=3.9
|
|
@@ -45,12 +45,19 @@ Writes `./TableValidator.py` - a ready-to-use Databricks notebook. Options:
|
|
|
45
45
|
tablevalidator-databricks init --output MyValidation.py --mode full
|
|
46
46
|
```
|
|
47
47
|
|
|
48
|
-
- `--mode basic` - only the check-group
|
|
48
|
+
- `--mode basic` - only the check-group Yes/No dropdowns and table pickers.
|
|
49
49
|
- `--mode full` (default) - also shows optional filter widgets (only/
|
|
50
|
-
ignore columns, row filter, primary key).
|
|
51
|
-
- `--mode schema` - same widgets as `basic
|
|
52
|
-
schema-shape-only comparison
|
|
53
|
-
|
|
50
|
+
ignore columns, row filter, primary key). Every check defaults to "Yes".
|
|
51
|
+
- `--mode schema` - same widgets as `basic`, but the Row checks default to
|
|
52
|
+
"No" - a lightweight schema-shape-only comparison out of the box (still
|
|
53
|
+
changeable per-run).
|
|
54
|
+
|
|
55
|
+
> **Note on the check groups**: "Catalog & Schema" only verifies that
|
|
56
|
+
> catalogs/schemas/tables exist and match by name - it produces no
|
|
57
|
+
> per-table PASS/FAIL by itself, so running it alone reports every table
|
|
58
|
+
> as `SKIPPED`. The per-table verdict comes from the **Column** checks
|
|
59
|
+
> (names, types, nullability, statistics) and **Row** checks (row counts,
|
|
60
|
+
> row-level data).
|
|
54
61
|
|
|
55
62
|
Then, in your Databricks workspace: **Workspace -> Import**, format
|
|
56
63
|
"Source", and select the generated file. Databricks recognizes the
|
|
@@ -63,7 +70,7 @@ markers and opens it as a real notebook.
|
|
|
63
70
|
2. Queries Unity Catalog (`SHOW CATALOGS` / `SHOW SCHEMAS IN ...` /
|
|
64
71
|
`SHOW TABLES IN ...` via the notebook's own ambient Spark session) to
|
|
65
72
|
populate Source/Target Catalog, Schema, and Table dropdowns.
|
|
66
|
-
3. Shows a
|
|
73
|
+
3. Shows a Yes/No dropdown per check group (Catalog & Schema / Column /
|
|
67
74
|
Row - the three independently-selectable check groups the engine
|
|
68
75
|
actually supports) plus, in `full` mode, optional text widgets for
|
|
69
76
|
column filtering, a row filter, and a primary key.
|
|
@@ -15,6 +15,7 @@ from unittest.mock import MagicMock
|
|
|
15
15
|
|
|
16
16
|
import pytest
|
|
17
17
|
|
|
18
|
+
from table_validator.config.schema import ValidationType
|
|
18
19
|
from tablevalidator_databricks.generator.generator import generate_notebook
|
|
19
20
|
|
|
20
21
|
|
|
@@ -254,42 +255,89 @@ def test_catalog_override_drives_schema_and_table_lookup(notebook_path):
|
|
|
254
255
|
|
|
255
256
|
|
|
256
257
|
# ---------------------------------------------------------------------------
|
|
257
|
-
#
|
|
258
|
-
#
|
|
259
|
-
#
|
|
260
|
-
# DefaultValueNotInChoicesList
|
|
261
|
-
#
|
|
258
|
+
# Validation-checks selection - three independent Yes/No dropdowns
|
|
259
|
+
# (check_catalog_schema / check_column / check_row), replacing an earlier
|
|
260
|
+
# single dbutils.widgets.multiselect that was observed in production to
|
|
261
|
+
# raise DefaultValueNotInChoicesList even with a default that appeared to
|
|
262
|
+
# satisfy multiselect's own documented constraints - three independent
|
|
263
|
+
# dropdowns have no equivalent cross-widget validation.
|
|
262
264
|
# ---------------------------------------------------------------------------
|
|
263
|
-
def
|
|
264
|
-
|
|
265
|
-
|
|
265
|
+
def test_full_and_basic_mode_default_all_checks_to_yes(tmp_path: Path) -> None:
|
|
266
|
+
for mode in ("full", "basic"):
|
|
267
|
+
path = tmp_path / f"{mode}.py"
|
|
268
|
+
generate_notebook(path, mode)
|
|
269
|
+
spark = _fake_spark(
|
|
270
|
+
catalogs=["main"], schemas_by_catalog={"main": ["sales"]},
|
|
271
|
+
tables_by_schema={("main", "sales"): ["orders"]},
|
|
272
|
+
)
|
|
273
|
+
captured = {}
|
|
274
|
+
|
|
275
|
+
def fake_validate_tables(source, target, **kwargs):
|
|
276
|
+
captured["enabled_validations"] = kwargs.get("enabled_validations")
|
|
277
|
+
m = MagicMock()
|
|
278
|
+
m.__str__ = MagicMock(return_value="Overall status: PASS")
|
|
279
|
+
return m
|
|
280
|
+
|
|
281
|
+
_run_notebook(path, widget_values={}, spark=spark, validate_tables_mock=fake_validate_tables)
|
|
266
282
|
|
|
283
|
+
assert captured["enabled_validations"] == {
|
|
284
|
+
ValidationType.CATALOG, ValidationType.SCHEMA,
|
|
285
|
+
ValidationType.COLUMN, ValidationType.ROW,
|
|
286
|
+
}, f"mode={mode}"
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def test_schema_mode_keeps_column_checks_on_and_turns_row_off(tmp_path: Path) -> None:
|
|
290
|
+
"""--mode schema means "schema SHAPE only". That shape is validated by
|
|
291
|
+
the COLUMN checks, so Column must stay ON - only the expensive Row
|
|
292
|
+
checks default off. Turning Column off too would leave nothing
|
|
293
|
+
producing a per-table verdict, reporting every table as SKIPPED."""
|
|
294
|
+
path = tmp_path / "schema.py"
|
|
295
|
+
generate_notebook(path, "schema")
|
|
267
296
|
spark = _fake_spark(
|
|
268
|
-
catalogs=["main"],
|
|
269
|
-
schemas_by_catalog={"main": ["sales"]},
|
|
297
|
+
catalogs=["main"], schemas_by_catalog={"main": ["sales"]},
|
|
270
298
|
tables_by_schema={("main", "sales"): ["orders"]},
|
|
271
299
|
)
|
|
300
|
+
captured = {}
|
|
301
|
+
|
|
302
|
+
def fake_validate_tables(source, target, **kwargs):
|
|
303
|
+
captured["enabled_validations"] = kwargs.get("enabled_validations")
|
|
304
|
+
m = MagicMock()
|
|
305
|
+
m.__str__ = MagicMock(return_value="Overall status: PASS")
|
|
306
|
+
return m
|
|
272
307
|
|
|
273
|
-
|
|
274
|
-
_run_notebook(path, widget_values={}, spark=spark, validate_tables_mock=MagicMock())
|
|
308
|
+
_run_notebook(path, widget_values={}, spark=spark, validate_tables_mock=fake_validate_tables)
|
|
275
309
|
|
|
310
|
+
assert captured["enabled_validations"] == {
|
|
311
|
+
ValidationType.CATALOG, ValidationType.SCHEMA, ValidationType.COLUMN,
|
|
312
|
+
}
|
|
276
313
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
314
|
+
|
|
315
|
+
def test_deselecting_every_check_falls_back_to_run_everything(notebook_path):
|
|
316
|
+
"""All three Yes/No dropdowns set to "No" must fall back to
|
|
317
|
+
validate_tables()'s own "run everything" default (enabled_validations=
|
|
318
|
+
None), not silently run nothing."""
|
|
282
319
|
spark = _fake_spark(
|
|
283
|
-
catalogs=["main"],
|
|
284
|
-
schemas_by_catalog={"main": ["sales"]},
|
|
320
|
+
catalogs=["main"], schemas_by_catalog={"main": ["sales"]},
|
|
285
321
|
tables_by_schema={("main", "sales"): ["orders"]},
|
|
286
322
|
)
|
|
323
|
+
captured = {}
|
|
324
|
+
|
|
325
|
+
def fake_validate_tables(source, target, **kwargs):
|
|
326
|
+
captured["enabled_validations"] = kwargs.get("enabled_validations")
|
|
327
|
+
m = MagicMock()
|
|
328
|
+
m.__str__ = MagicMock(return_value="Overall status: PASS")
|
|
329
|
+
return m
|
|
287
330
|
|
|
288
|
-
|
|
331
|
+
_run_notebook(
|
|
332
|
+
notebook_path,
|
|
333
|
+
widget_values={
|
|
334
|
+
"check_catalog_schema": "No", "check_column": "No", "check_row": "No",
|
|
335
|
+
},
|
|
336
|
+
spark=spark,
|
|
337
|
+
validate_tables_mock=fake_validate_tables,
|
|
338
|
+
)
|
|
289
339
|
|
|
290
|
-
|
|
291
|
-
choices = fake_widgets.multiselect_choices["checks"]
|
|
292
|
-
assert fake_widgets.get("checks") == ",".join(choices)
|
|
340
|
+
assert captured["enabled_validations"] is None
|
|
293
341
|
|
|
294
342
|
|
|
295
343
|
# ---------------------------------------------------------------------------
|
|
@@ -3,11 +3,12 @@ no Databricks/Spark involved."""
|
|
|
3
3
|
|
|
4
4
|
from __future__ import annotations
|
|
5
5
|
|
|
6
|
+
import re
|
|
6
7
|
from pathlib import Path
|
|
7
8
|
|
|
8
9
|
import pytest
|
|
9
10
|
|
|
10
|
-
from tablevalidator_databricks.generator.generator import generate_notebook
|
|
11
|
+
from tablevalidator_databricks.generator.generator import _min_core_version, generate_notebook
|
|
11
12
|
|
|
12
13
|
|
|
13
14
|
def test_generate_notebook_writes_a_databricks_source_notebook(tmp_path: Path) -> None:
|
|
@@ -89,3 +90,29 @@ def test_generate_notebook_no_leftover_placeholders(tmp_path: Path) -> None:
|
|
|
89
90
|
|
|
90
91
|
assert "{{" not in content
|
|
91
92
|
assert "}}" not in content
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def test_generated_notebook_pins_a_core_version_floor(tmp_path: Path) -> None:
|
|
96
|
+
"""Regression guard for a real user-reported failure: the notebook's
|
|
97
|
+
%pip install line must carry a `>=` floor for table-validator.
|
|
98
|
+
validate_tables() only exists in 0.1.9+, and the two packages are
|
|
99
|
+
versioned independently - a user who pinned the engine to THIS
|
|
100
|
+
package's version number (0.1.3) got an engine predating
|
|
101
|
+
validate_tables() entirely and an ImportError."""
|
|
102
|
+
output = tmp_path / "TableValidator.py"
|
|
103
|
+
generate_notebook(output, "full")
|
|
104
|
+
content = output.read_text(encoding="utf-8")
|
|
105
|
+
|
|
106
|
+
assert 'pip install "table-validator>=' in content
|
|
107
|
+
# The floor must be new enough to actually contain validate_tables().
|
|
108
|
+
match = re.search(r'pip install "table-validator>=([0-9]+)\.([0-9]+)\.([0-9]+)"', content)
|
|
109
|
+
assert match, "no parseable version floor in the %pip install line"
|
|
110
|
+
major, minor, patch = (int(g) for g in match.groups())
|
|
111
|
+
assert (major, minor, patch) >= (0, 1, 9)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def test_min_core_version_reads_from_package_metadata() -> None:
|
|
115
|
+
"""The floor comes from this package's own declared table-validator
|
|
116
|
+
dependency, so the two can never silently drift apart."""
|
|
117
|
+
assert _min_core_version() != "0.0.0"
|
|
118
|
+
assert re.match(r"^[0-9]+\.[0-9]+\.[0-9]+$", _min_core_version())
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Renders the bundled widget_notebook.py.tmpl into a real Databricks
|
|
3
|
-
notebook file. No Jinja2 - the template has exactly two placeholders,
|
|
4
|
-
substituted via plain str.replace(), so a templating dependency isn't
|
|
5
|
-
worth adding for this.
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
from __future__ import annotations
|
|
9
|
-
|
|
10
|
-
import importlib.resources
|
|
11
|
-
from pathlib import Path
|
|
12
|
-
|
|
13
|
-
VALID_MODES = ("basic", "full", "schema")
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
def generate_notebook(output: Path, mode: str) -> None:
|
|
17
|
-
"""Write a widget-driven Databricks notebook to `output`.
|
|
18
|
-
|
|
19
|
-
mode controls which optional widgets/pre-selected checks the notebook
|
|
20
|
-
starts with - see tablevalidator_databricks/cli/main.py's `init`
|
|
21
|
-
command docstring for what each mode means. The underlying comparison
|
|
22
|
-
logic is identical regardless of mode; only the widgets shown differ,
|
|
23
|
-
so all three modes render from the same template file rather than
|
|
24
|
-
three separate ones.
|
|
25
|
-
"""
|
|
26
|
-
if mode not in VALID_MODES:
|
|
27
|
-
raise ValueError(f"mode must be one of {VALID_MODES}, got {mode!r}")
|
|
28
|
-
|
|
29
|
-
template = (
|
|
30
|
-
importlib.resources.files("tablevalidator_databricks.templates")
|
|
31
|
-
.joinpath("widget_notebook.py.tmpl")
|
|
32
|
-
.read_text(encoding="utf-8")
|
|
33
|
-
)
|
|
34
|
-
rendered = template.replace("{{MODE}}", mode).replace(
|
|
35
|
-
"{{SHOW_OPTIONAL_WIDGETS}}", "True" if mode == "full" else "False",
|
|
36
|
-
)
|
|
37
|
-
|
|
38
|
-
output.parent.mkdir(parents=True, exist_ok=True)
|
|
39
|
-
output.write_text(rendered, encoding="utf-8")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|