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.
Files changed (22) hide show
  1. {tablevalidator_databricks-0.1.2 → tablevalidator_databricks-0.1.4}/PKG-INFO +14 -7
  2. {tablevalidator_databricks-0.1.2 → tablevalidator_databricks-0.1.4}/README.md +13 -6
  3. tablevalidator_databricks-0.1.4/tablevalidator_databricks/generator/generator.py +66 -0
  4. {tablevalidator_databricks-0.1.2 → tablevalidator_databricks-0.1.4}/tablevalidator_databricks/templates/widget_notebook.py.tmpl +45 -21
  5. {tablevalidator_databricks-0.1.2 → tablevalidator_databricks-0.1.4}/tablevalidator_databricks.egg-info/PKG-INFO +14 -7
  6. {tablevalidator_databricks-0.1.2 → tablevalidator_databricks-0.1.4}/tablevalidator_databricks.egg-info/scm_version.json +2 -2
  7. {tablevalidator_databricks-0.1.2 → tablevalidator_databricks-0.1.4}/tests/test_generated_notebook_execution.py +71 -23
  8. {tablevalidator_databricks-0.1.2 → tablevalidator_databricks-0.1.4}/tests/test_generator.py +28 -1
  9. tablevalidator_databricks-0.1.2/tablevalidator_databricks/generator/generator.py +0 -39
  10. {tablevalidator_databricks-0.1.2 → tablevalidator_databricks-0.1.4}/pyproject.toml +0 -0
  11. {tablevalidator_databricks-0.1.2 → tablevalidator_databricks-0.1.4}/setup.cfg +0 -0
  12. {tablevalidator_databricks-0.1.2 → tablevalidator_databricks-0.1.4}/tablevalidator_databricks/__init__.py +0 -0
  13. {tablevalidator_databricks-0.1.2 → tablevalidator_databricks-0.1.4}/tablevalidator_databricks/cli/__init__.py +0 -0
  14. {tablevalidator_databricks-0.1.2 → tablevalidator_databricks-0.1.4}/tablevalidator_databricks/cli/main.py +0 -0
  15. {tablevalidator_databricks-0.1.2 → tablevalidator_databricks-0.1.4}/tablevalidator_databricks/generator/__init__.py +0 -0
  16. {tablevalidator_databricks-0.1.2 → tablevalidator_databricks-0.1.4}/tablevalidator_databricks.egg-info/SOURCES.txt +0 -0
  17. {tablevalidator_databricks-0.1.2 → tablevalidator_databricks-0.1.4}/tablevalidator_databricks.egg-info/dependency_links.txt +0 -0
  18. {tablevalidator_databricks-0.1.2 → tablevalidator_databricks-0.1.4}/tablevalidator_databricks.egg-info/entry_points.txt +0 -0
  19. {tablevalidator_databricks-0.1.2 → tablevalidator_databricks-0.1.4}/tablevalidator_databricks.egg-info/requires.txt +0 -0
  20. {tablevalidator_databricks-0.1.2 → tablevalidator_databricks-0.1.4}/tablevalidator_databricks.egg-info/scm_file_list.json +0 -0
  21. {tablevalidator_databricks-0.1.2 → tablevalidator_databricks-0.1.4}/tablevalidator_databricks.egg-info/top_level.txt +0 -0
  22. {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.2
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 multiselect and table pickers.
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`; intended for a lightweight
52
- schema-shape-only comparison (just deselect Column/Row in the checks
53
- multiselect after opening the notebook).
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 "Validation Checks" multiselect (Catalog & Schema / Column /
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 multiselect and table pickers.
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`; intended for a lightweight
38
- schema-shape-only comparison (just deselect Column/Row in the checks
39
- multiselect after opening the notebook).
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 "Validation Checks" multiselect (Catalog & Schema / Column /
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 %pip install table-validator
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
- _ALL_CHECKS = ["Catalog & Schema", "Column", "Row"]
184
- # dbutils.widgets.multiselect's default value must itself be one of the
185
- # combinations Databricks is willing to accept - in practice this means
186
- # "all choices, comma-joined in the declared order" is the only default
187
- # guaranteed to work across Databricks runtime versions (a narrower
188
- # default, e.g. just "Catalog & Schema", was observed to fail with
189
- # "DefaultValueNotInChoicesList: Selection sequence must include ..." on
190
- # some runtimes). --mode schema still gets its narrower intent honored -
191
- # see the checks-selection cell below, which treats an EMPTY selection as
192
- # "run everything" and a real narrowing as an explicit choice; this
193
- # widget just always starts fully selected regardless of --mode.
194
- dbutils.widgets.multiselect("checks", ",".join(_ALL_CHECKS), _ALL_CHECKS, "Validation Checks")
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
- for check in selected_checks:
290
- enabled_validations |= _CHECK_MAP.get(check, set())
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.2
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 multiselect and table pickers.
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`; intended for a lightweight
52
- schema-shape-only comparison (just deselect Column/Row in the checks
53
- multiselect after opening the notebook).
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 "Validation Checks" multiselect (Catalog & Schema / Column /
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.
@@ -1,7 +1,7 @@
1
1
  {
2
- "tag": "0.1.2",
2
+ "tag": "0.1.4",
3
3
  "distance": 0,
4
- "node": "gae96a50",
4
+ "node": "g131e781",
5
5
  "dirty": false,
6
6
  "branch": "HEAD",
7
7
  "node_date": "2026-09-08"
@@ -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
- # --mode schema: regression test for a real production bug. The checks
258
- # multiselect's default used to be a narrower subset of its choices in
259
- # --mode schema (just "Catalog & Schema"), which Databricks rejected with
260
- # DefaultValueNotInChoicesList - _FakeWidgets.multiselect above replicates
261
- # that exact constraint.
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 test_schema_mode_multiselect_default_does_not_crash(tmp_path: Path) -> None:
264
- path = tmp_path / "TableValidator.py"
265
- generate_notebook(path, "schema")
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
- # Must not raise _DefaultValueNotInChoicesList.
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
- def test_multiselect_default_is_every_choice(notebook_path):
278
- """The checks widget's default must always be the full choices list,
279
- comma-joined in the declared order - the only default guaranteed to
280
- be accepted by dbutils.widgets.multiselect across Databricks runtime
281
- versions."""
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
- globs = _run_notebook(notebook_path, widget_values={}, spark=spark, validate_tables_mock=MagicMock())
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
- fake_widgets = globs["dbutils"].widgets
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")