Skip to content

Commit f1c3cef

Browse files
committed
hx-8da6f798 Migrate QualityCheckExecutor fallback
1 parent fafc33f commit f1c3cef

3 files changed

Lines changed: 144 additions & 19 deletions

File tree

.helix/issues.jsonl

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@
88
{"id":"hx-4db4f06e","title":"Deprecate gx_wrapper.py; migrate consumers to GXSuiteExecutor","type":"task","status":"closed","priority":2,"labels":["helix","blocked"],"deps":[],"parent":"","spec-id":"docs/helix/01-frame/features/FEAT-024-validator-consolidation.md","description":"","design":"","acceptance":"gx_wrapper.py has deprecation warning; validator.py no longer imports gx_wrapper; all GX execution uses GXSuiteExecutor","assignee":"helix","notes":"BLOCKED [2026-04-01T23:12:15-04:00]: failed to complete after 4 attempts with exponential backoff","execution-eligible":true,"superseded-by":"","replaces":"","created":"2026-04-02T03:00:01Z","updated":"2026-04-02T03:15:05Z"}
99
{"id":"hx-6a1e485c","title":"ADR-005 Phase C: migrate consumers from quality_checks to ExpectationSuite","type":"task","status":"closed","priority":2,"labels":["helix","phase:build","kind:refactor","area:validation"],"deps":["hx-4db4f06e"],"parent":"","spec-id":"docs/helix/02-design/adr/ADR-005-unified-expectation-model.md","description":"","design":"","acceptance":"All 10 source files updated to read from umf.expectations instead of quality_checks; backward-compatible fallback for old YAML files","assignee":"helix","notes":"","execution-eligible":true,"superseded-by":"","replaces":"","created":"2026-04-02T03:00:03Z","updated":"2026-04-02T03:29:43Z"}
1010
{"id":"hx-bae407fe","title":"Implement GXTestHarness fixture for agentic iteration","type":"task","status":"closed","priority":2,"labels":["helix","phase:build","kind:build","area:testing"],"deps":[],"parent":"","spec-id":"docs/helix/01-frame/features/FEAT-016-testing-infrastructure.md","description":"","design":"","acceptance":"GXTestHarness fixture in tests/conftest.py with harness.run(expectations) API; documented usage example","assignee":"helix","notes":"BLOCKED [2026-04-01T23:12:15-04:00]: failed to complete after 4 attempts with exponential backoff","execution-eligible":true,"superseded-by":"","replaces":"","created":"2026-04-02T03:00:05Z","updated":"2026-04-02T03:37:59Z"}
11-
{"id":"hx-96d0725a","title":"Resolve preview --against parameter (implement or remove)","type":"task","status":"open","priority":2,"labels":["helix","blocked"],"deps":[],"parent":"","spec-id":"docs/helix/01-frame/features/FEAT-023-authoring-tools.md","description":"","design":"","acceptance":"Either --against triggers CSV validation execution, or parameter is removed from CLI and spec updated","assignee":"","notes":"BLOCKED [2026-04-01T23:12:15-04:00]: failed to complete after 4 attempts with exponential backoff","execution-eligible":true,"superseded-by":"","replaces":"","created":"2026-04-02T03:00:07Z","updated":"2026-04-02T03:12:15Z"}
11+
{"id":"hx-96d0725a","title":"Resolve preview --against parameter (implement or remove)","type":"task","status":"closed","priority":2,"labels":["helix","phase:build","kind:build","area:authoring"],"deps":[],"parent":"","spec-id":"docs/helix/01-frame/features/FEAT-023-authoring-tools.md","description":"","design":"","acceptance":"Either --against triggers CSV validation execution, or parameter is removed from CLI and spec updated","assignee":"","notes":"BLOCKED [2026-04-01T23:12:15-04:00]: failed to complete after 4 attempts with exponential backoff","execution-eligible":true,"superseded-by":"","replaces":"","created":"2026-04-02T03:00:07Z","updated":"2026-04-02T03:39:28Z"}
1212
{"id":"hx-ac83f094","title":"Add profiling pipeline end-to-end test","type":"task","status":"closed","priority":2,"labels":["helix","phase:build","kind:test","area:profiling"],"deps":[],"parent":"","spec-id":"docs/helix/01-frame/features/FEAT-005-profiling.md","description":"","design":"","acceptance":"Integration test exercises SparkToUmfMapper -> DeequToUmfMapper -> BaselineExpectationGenerator flow end-to-end","assignee":"","notes":"","execution-eligible":true,"superseded-by":"","replaces":"","created":"2026-04-02T03:00:09Z","updated":"2026-04-02T03:08:12Z"}
1313
{"id":"hx-807a8731","title":"Publish tablespec to PyPI","type":"task","status":"closed","priority":2,"labels":["helix","phase:deploy","kind:deploy","area:distribution"],"deps":[],"parent":"","spec-id":"docs/helix/05-deploy/deployment-checklist.md","description":"","design":"","acceptance":"tablespec package available on PyPI; pip install tablespec works","assignee":"","notes":"","execution-eligible":true,"superseded-by":"","replaces":"","created":"2026-04-02T03:00:10Z","updated":"2026-04-02T03:09:52Z"}
14+
{"id":"hx-2c3c331f","title":"ADR-005 Phase C: migrate consumers to ExpectationSuite","type":"epic","status":"open","priority":2,"labels":["helix","phase:build","kind:refactor","area:validation"],"deps":[],"parent":"","spec-id":"docs/helix/02-design/adr/ADR-005-unified-expectation-model.md","description":"","design":"","acceptance":"All runtime consumers read from ExpectationSuite; quality_checks retained for backward-compatible loading only","assignee":"","notes":"","execution-eligible":true,"superseded-by":"","replaces":"","created":"2026-04-02T03:39:59Z","updated":"2026-04-02T03:39:59Z"}
15+
{"id":"hx-bdb8fff2","title":"Migrate UMFLoader to populate ExpectationSuite on read","type":"task","status":"in_progress","priority":2,"labels":["helix","phase:build","kind:refactor","area:loader"],"deps":[],"parent":"hx-2c3c331f","spec-id":"docs/helix/02-design/adr/ADR-005-unified-expectation-model.md","description":"","design":"","acceptance":"UMFLoader.load() populates umf.expectations from validation_rules + quality_checks via expectation_migration; split-format saver writes expectations.yaml","assignee":"helix","notes":"","execution-eligible":true,"superseded-by":"","replaces":"","created":"2026-04-02T03:40:07Z","updated":"2026-04-02T03:44:10Z"}
16+
{"id":"hx-8da6f798","title":"Migrate QualityCheckExecutor to read from ExpectationSuite","type":"task","status":"closed","priority":2,"labels":["helix","phase:build","kind:refactor","area:validation"],"deps":["hx-bdb8fff2"],"parent":"hx-2c3c331f","spec-id":"docs/helix/02-design/adr/ADR-005-unified-expectation-model.md","description":"","design":"","acceptance":"QualityCheckExecutor reads from umf.expectations; falls back to quality_checks if expectations is empty","assignee":"helix","notes":"","execution-eligible":true,"superseded-by":"","replaces":"","created":"2026-04-02T03:40:13Z","updated":"2026-04-02T03:48:32Z"}
17+
{"id":"hx-9ed0389d","title":"Migrate authoring commands to use ExpectationSuite","type":"task","status":"open","priority":2,"labels":["helix","phase:build","kind:refactor","area:authoring"],"deps":["hx-bdb8fff2"],"parent":"hx-2c3c331f","spec-id":"docs/helix/02-design/adr/ADR-005-unified-expectation-model.md","description":"","design":"","acceptance":"mutations.py, preview.py, apply_response.py, and cli.py search/mutate umf.expectations instead of quality_checks","assignee":"","notes":"","execution-eligible":true,"superseded-by":"","replaces":"","created":"2026-04-02T03:40:19Z","updated":"2026-04-02T03:40:19Z"}
18+
{"id":"hx-f3261259","title":"Add deprecation warnings to quality_checks and validation_rules fields","type":"task","status":"open","priority":2,"labels":["helix","phase:build","kind:refactor","area:models"],"deps":["hx-8da6f798","hx-9ed0389d"],"parent":"hx-2c3c331f","spec-id":"docs/helix/02-design/adr/ADR-005-unified-expectation-model.md","description":"","design":"","acceptance":"Pydantic model emits DeprecationWarning when quality_checks or validation_rules are populated directly; ADR-005 status updated to Phase C","assignee":"","notes":"","execution-eligible":true,"superseded-by":"","replaces":"","created":"2026-04-02T03:40:24Z","updated":"2026-04-02T03:40:24Z"}

src/tablespec/quality/executor.py

Lines changed: 28 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -201,24 +201,7 @@ def execute_quality_checks(
201201
)
202202

203203
# Extract ingested-stage checks from the unified expectation suite.
204-
suite = umf.expectations or migrate_to_expectation_suite(umf.model_dump(exclude_none=True))
205-
configured_checks: list[QualityCheck] = []
206-
thresholds_dict: dict[str, Any] | None = None
207-
208-
if suite:
209-
from tablespec.models.umf import QualityCheck as QualityCheckModel
210-
211-
configured_checks = [
212-
QualityCheckModel(
213-
expectation=exp.model_dump(),
214-
severity=exp.meta.severity,
215-
blocking=exp.meta.blocking,
216-
description=exp.meta.description,
217-
tags=list(exp.meta.tags),
218-
)
219-
for exp in suite.ingested
220-
]
221-
thresholds_dict = suite.thresholds
204+
configured_checks, thresholds_dict = self._get_configured_checks(umf)
222205

223206
# Add default checks (row count change) unless already configured
224207
checks = self._merge_with_defaults(configured_checks)
@@ -320,6 +303,33 @@ def execute_quality_checks(
320303
validated_records=validated_records,
321304
)
322305

306+
def _get_configured_checks(
307+
self,
308+
umf: Any,
309+
) -> tuple[list[QualityCheck], dict[str, Any] | None]:
310+
"""Return configured ingested checks and thresholds for a loaded UMF.
311+
312+
Prefer the unified ExpectationSuite when it contains expectations.
313+
Fall back to migrating legacy quality_checks when the suite is absent or empty.
314+
"""
315+
suite = umf.expectations
316+
if suite is None or not suite.expectations:
317+
suite = migrate_to_expectation_suite(umf.model_dump(exclude_none=True))
318+
319+
from tablespec.models.umf import QualityCheck as QualityCheckModel
320+
321+
configured_checks = [
322+
QualityCheckModel(
323+
expectation=exp.model_dump(),
324+
severity=exp.meta.severity,
325+
blocking=exp.meta.blocking,
326+
description=exp.meta.description,
327+
tags=list(exp.meta.tags),
328+
)
329+
for exp in suite.ingested
330+
]
331+
return configured_checks, suite.thresholds
332+
323333
def _execute_batch(
324334
self,
325335
spark_df: DataFrame,
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
"""Tests for QualityCheckExecutor expectation source selection."""
2+
3+
from __future__ import annotations
4+
5+
from types import SimpleNamespace
6+
from unittest.mock import patch
7+
8+
import pytest
9+
10+
from tablespec.models.umf import Expectation, ExpectationMeta, ExpectationSuite
11+
from tablespec.quality.executor import QualityCheckExecutor
12+
13+
pytestmark = [pytest.mark.fast, pytest.mark.no_spark]
14+
15+
16+
def _make_executor() -> QualityCheckExecutor:
17+
return object.__new__(QualityCheckExecutor)
18+
19+
20+
class TestConfiguredCheckSelection:
21+
def test_prefers_expectation_suite_when_populated(self) -> None:
22+
executor = _make_executor()
23+
suite = ExpectationSuite(
24+
expectations=[
25+
Expectation(
26+
type="expect_column_values_to_be_between",
27+
kwargs={"column": "amount", "min_value": 0},
28+
meta=ExpectationMeta(
29+
stage="ingested",
30+
severity="critical",
31+
blocking=True,
32+
description="amount must be non-negative",
33+
tags=["finance"],
34+
),
35+
),
36+
Expectation(
37+
type="expect_column_values_to_match_regex",
38+
kwargs={"column": "code", "regex": r"^[A-Z]+$"},
39+
meta=ExpectationMeta(stage="raw"),
40+
),
41+
],
42+
thresholds={"max_failures": 2},
43+
)
44+
umf = SimpleNamespace(
45+
expectations=suite,
46+
model_dump=lambda **_: {"quality_checks": {"checks": ["legacy"]}},
47+
)
48+
49+
with patch("tablespec.quality.executor.migrate_to_expectation_suite") as migrate:
50+
checks, thresholds = executor._get_configured_checks(umf)
51+
52+
migrate.assert_not_called()
53+
assert thresholds == {"max_failures": 2}
54+
assert len(checks) == 1
55+
assert checks[0].expectation["type"] == "expect_column_values_to_be_between"
56+
assert checks[0].severity == "critical"
57+
assert checks[0].blocking is True
58+
assert checks[0].description == "amount must be non-negative"
59+
assert checks[0].tags == ["finance"]
60+
61+
def test_falls_back_to_legacy_checks_when_suite_is_empty(self) -> None:
62+
executor = _make_executor()
63+
umf = SimpleNamespace(
64+
expectations=ExpectationSuite(expectations=[]),
65+
model_dump=lambda **_: {"quality_checks": {"checks": ["legacy"]}},
66+
)
67+
migrated_suite = ExpectationSuite(
68+
expectations=[
69+
Expectation(
70+
type="expect_column_values_to_be_in_set",
71+
kwargs={"column": "status", "value_set": ["A", "B"]},
72+
meta=ExpectationMeta(stage="ingested", severity="warning"),
73+
)
74+
],
75+
thresholds={"min_success_rate": 95.0},
76+
)
77+
78+
with patch(
79+
"tablespec.quality.executor.migrate_to_expectation_suite",
80+
return_value=migrated_suite,
81+
) as migrate:
82+
checks, thresholds = executor._get_configured_checks(umf)
83+
84+
migrate.assert_called_once_with({"quality_checks": {"checks": ["legacy"]}})
85+
assert thresholds == {"min_success_rate": 95.0}
86+
assert len(checks) == 1
87+
assert checks[0].expectation["type"] == "expect_column_values_to_be_in_set"
88+
assert checks[0].severity == "warning"
89+
90+
def test_does_not_fall_back_when_suite_has_only_raw_expectations(self) -> None:
91+
executor = _make_executor()
92+
umf = SimpleNamespace(
93+
expectations=ExpectationSuite(
94+
expectations=[
95+
Expectation(
96+
type="expect_column_values_to_match_regex",
97+
kwargs={"column": "id", "regex": r"^\d+$"},
98+
meta=ExpectationMeta(stage="raw"),
99+
)
100+
]
101+
),
102+
model_dump=lambda **_: {"quality_checks": {"checks": ["legacy"]}},
103+
)
104+
105+
with patch("tablespec.quality.executor.migrate_to_expectation_suite") as migrate:
106+
checks, thresholds = executor._get_configured_checks(umf)
107+
108+
migrate.assert_not_called()
109+
assert checks == []
110+
assert thresholds is None

0 commit comments

Comments
 (0)