pipeverify 0.2.0__tar.gz → 0.2.1__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.
- {pipeverify-0.2.0 → pipeverify-0.2.1}/PKG-INFO +3 -3
- {pipeverify-0.2.0 → pipeverify-0.2.1}/README.md +2 -2
- {pipeverify-0.2.0 → pipeverify-0.2.1}/pipeverify/__init__.py +1 -1
- {pipeverify-0.2.0 → pipeverify-0.2.1}/pipeverify/checks.py +12 -12
- {pipeverify-0.2.0 → pipeverify-0.2.1}/pipeverify/core.py +1 -1
- {pipeverify-0.2.0 → pipeverify-0.2.1}/pipeverify/source_checks.py +6 -6
- {pipeverify-0.2.0 → pipeverify-0.2.1}/pyproject.toml +1 -1
- {pipeverify-0.2.0 → pipeverify-0.2.1}/.gitignore +0 -0
- {pipeverify-0.2.0 → pipeverify-0.2.1}/LICENSE +0 -0
- {pipeverify-0.2.0 → pipeverify-0.2.1}/NOTICE +0 -0
- {pipeverify-0.2.0 → pipeverify-0.2.1}/pipeverify/cli.py +0 -0
- {pipeverify-0.2.0 → pipeverify-0.2.1}/pipeverify/explain.py +0 -0
- {pipeverify-0.2.0 → pipeverify-0.2.1}/pipeverify/report.py +0 -0
- {pipeverify-0.2.0 → pipeverify-0.2.1}/pipeverify/sources.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: pipeverify
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: Check that a pipeline change left the data right, not just the job green.
|
|
5
5
|
Project-URL: Homepage, https://github.com/akshaysharma1088/pipeverify
|
|
6
6
|
Project-URL: Issues, https://github.com/akshaysharma1088/pipeverify/issues
|
|
@@ -38,7 +38,7 @@ Description-Content-Type: text/markdown
|
|
|
38
38
|

|
|
39
39
|

|
|
40
40
|
[](https://pypi.org/project/pipeverify/)
|
|
41
|
-
[](https://doi.org/10.5281/zenodo.23542277)
|
|
42
42
|
|
|
43
43
|
A pipeline change can pass every test, finish on time and keep the schema intact while quietly
|
|
44
44
|
dropping rows, duplicating keys or changing what a column means. More of these changes are now
|
|
@@ -215,7 +215,7 @@ jobs:
|
|
|
215
215
|
steps:
|
|
216
216
|
- uses: actions/checkout@v4
|
|
217
217
|
# ... build the "after" snapshots for this PR here ...
|
|
218
|
-
- uses: akshaysharma1088/pipeverify@v0.
|
|
218
|
+
- uses: akshaysharma1088/pipeverify@v0.2.0
|
|
219
219
|
with:
|
|
220
220
|
config: pipeverify.yml
|
|
221
221
|
fail-on: block # or review, or never
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|

|
|
8
8
|

|
|
9
9
|
[](https://pypi.org/project/pipeverify/)
|
|
10
|
-
[](https://doi.org/10.5281/zenodo.23542277)
|
|
11
11
|
|
|
12
12
|
A pipeline change can pass every test, finish on time and keep the schema intact while quietly
|
|
13
13
|
dropping rows, duplicating keys or changing what a column means. More of these changes are now
|
|
@@ -184,7 +184,7 @@ jobs:
|
|
|
184
184
|
steps:
|
|
185
185
|
- uses: actions/checkout@v4
|
|
186
186
|
# ... build the "after" snapshots for this PR here ...
|
|
187
|
-
- uses: akshaysharma1088/pipeverify@v0.
|
|
187
|
+
- uses: akshaysharma1088/pipeverify@v0.2.0
|
|
188
188
|
with:
|
|
189
189
|
config: pipeverify.yml
|
|
190
190
|
fail-on: block # or review, or never
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""pipeverify: check that a pipeline change left the data right, not just the job green."""
|
|
2
2
|
|
|
3
|
-
__version__ = "0.2.
|
|
3
|
+
__version__ = "0.2.1"
|
|
4
4
|
|
|
5
5
|
from .checks import Finding, Thresholds # noqa: E402
|
|
6
6
|
from .core import RunResult, TableResult, run_config, verify # noqa: E402
|
|
@@ -113,8 +113,8 @@ def run_checks(
|
|
|
113
113
|
if k not in sb or k not in sa:
|
|
114
114
|
raise ValueError(f"Key column '{k}' is missing from the before or after table")
|
|
115
115
|
|
|
116
|
-
n_before = con.execute(f"SELECT count(*) FROM {q(before)}").
|
|
117
|
-
n_after = con.execute(f"SELECT count(*) FROM {q(after)}").
|
|
116
|
+
n_before = con.execute(f"SELECT count(*) FROM {q(before)}").fetchall()[0][0]
|
|
117
|
+
n_after = con.execute(f"SELECT count(*) FROM {q(after)}").fetchall()[0][0]
|
|
118
118
|
stats: dict[str, Any] = {"rows_before": n_before, "rows_after": n_after,
|
|
119
119
|
"columns_before": len(sb), "columns_after": len(sa)}
|
|
120
120
|
|
|
@@ -152,9 +152,9 @@ def run_checks(
|
|
|
152
152
|
if key:
|
|
153
153
|
kcols = ", ".join(q(k) for k in key)
|
|
154
154
|
dup_before = con.execute(
|
|
155
|
-
f"SELECT count(*) FROM (SELECT {kcols} FROM {q(before)} GROUP BY ALL HAVING count(*) > 1)").
|
|
155
|
+
f"SELECT count(*) FROM (SELECT {kcols} FROM {q(before)} GROUP BY ALL HAVING count(*) > 1)").fetchall()[0][0]
|
|
156
156
|
dup_after = con.execute(
|
|
157
|
-
f"SELECT count(*) FROM (SELECT {kcols} FROM {q(after)} GROUP BY ALL HAVING count(*) > 1)").
|
|
157
|
+
f"SELECT count(*) FROM (SELECT {kcols} FROM {q(after)} GROUP BY ALL HAVING count(*) > 1)").fetchall()[0][0]
|
|
158
158
|
stats.update(duplicate_keys_before=dup_before, duplicate_keys_after=dup_after)
|
|
159
159
|
if dup_after > dup_before:
|
|
160
160
|
findings.append(Finding("duplicate_keys", BLOCK,
|
|
@@ -167,11 +167,11 @@ def run_checks(
|
|
|
167
167
|
on = " AND ".join(f"b.{q(k)} IS NOT DISTINCT FROM a.{q(k)}" for k in key)
|
|
168
168
|
missing = con.execute(
|
|
169
169
|
f"SELECT count(*) FROM (SELECT DISTINCT {kcols} FROM {q(before)}) b "
|
|
170
|
-
f"WHERE NOT EXISTS (SELECT 1 FROM {q(after)} a WHERE {on})").
|
|
170
|
+
f"WHERE NOT EXISTS (SELECT 1 FROM {q(after)} a WHERE {on})").fetchall()[0][0]
|
|
171
171
|
new = con.execute(
|
|
172
172
|
f"SELECT count(*) FROM (SELECT DISTINCT {kcols} FROM {q(after)}) a "
|
|
173
|
-
f"WHERE NOT EXISTS (SELECT 1 FROM {q(before)} b WHERE {on})").
|
|
174
|
-
distinct_before = con.execute(f"SELECT count(*) FROM (SELECT DISTINCT {kcols} FROM {q(before)})").
|
|
173
|
+
f"WHERE NOT EXISTS (SELECT 1 FROM {q(before)} b WHERE {on})").fetchall()[0][0]
|
|
174
|
+
distinct_before = con.execute(f"SELECT count(*) FROM (SELECT DISTINCT {kcols} FROM {q(before)})").fetchall()[0][0]
|
|
175
175
|
stats.update(missing_keys=missing, new_keys=new)
|
|
176
176
|
if missing:
|
|
177
177
|
pct = missing / max(distinct_before, 1) * 100
|
|
@@ -193,7 +193,7 @@ def run_checks(
|
|
|
193
193
|
for c in common:
|
|
194
194
|
nb, na = con.execute(
|
|
195
195
|
f"SELECT (SELECT avg(CASE WHEN {q(c)} IS NULL THEN 1.0 ELSE 0 END) FROM {q(before)}), "
|
|
196
|
-
f"(SELECT avg(CASE WHEN {q(c)} IS NULL THEN 1.0 ELSE 0 END) FROM {q(after)})").
|
|
196
|
+
f"(SELECT avg(CASE WHEN {q(c)} IS NULL THEN 1.0 ELSE 0 END) FROM {q(after)})").fetchall()[0]
|
|
197
197
|
nb, na = float(nb or 0) * 100, float(na or 0) * 100
|
|
198
198
|
rise = na - nb
|
|
199
199
|
if rise > 0:
|
|
@@ -207,7 +207,7 @@ def run_checks(
|
|
|
207
207
|
|
|
208
208
|
# 5. changed values on matched keys
|
|
209
209
|
if unique_keys_join is not None:
|
|
210
|
-
matched = con.execute(f"SELECT count(*) FROM _pv_ub b JOIN _pv_ua a ON {unique_keys_join}").
|
|
210
|
+
matched = con.execute(f"SELECT count(*) FROM _pv_ub b JOIN _pv_ua a ON {unique_keys_join}").fetchall()[0][0]
|
|
211
211
|
stats["matched_rows"] = matched
|
|
212
212
|
for c in common:
|
|
213
213
|
if c in key:
|
|
@@ -219,7 +219,7 @@ def run_checks(
|
|
|
219
219
|
else:
|
|
220
220
|
diff = f"CAST(b.{q(c)} AS VARCHAR) IS DISTINCT FROM CAST(a.{q(c)} AS VARCHAR)"
|
|
221
221
|
changed = con.execute(
|
|
222
|
-
f"SELECT count(*) FROM _pv_ub b JOIN _pv_ua a ON {unique_keys_join} WHERE {diff}").
|
|
222
|
+
f"SELECT count(*) FROM _pv_ub b JOIN _pv_ua a ON {unique_keys_join} WHERE {diff}").fetchall()[0][0]
|
|
223
223
|
if not changed:
|
|
224
224
|
continue
|
|
225
225
|
pct = changed / max(matched, 1) * 100
|
|
@@ -243,7 +243,7 @@ def run_checks(
|
|
|
243
243
|
continue
|
|
244
244
|
tb, ta = con.execute(
|
|
245
245
|
f"SELECT (SELECT sum(CAST({q(c)} AS DOUBLE)) FROM {q(before)}), "
|
|
246
|
-
f"(SELECT sum(CAST({q(c)} AS DOUBLE)) FROM {q(after)})").
|
|
246
|
+
f"(SELECT sum(CAST({q(c)} AS DOUBLE)) FROM {q(after)})").fetchall()[0]
|
|
247
247
|
if tb is None and ta is None:
|
|
248
248
|
continue
|
|
249
249
|
tb, ta = float(tb or 0), float(ta or 0)
|
|
@@ -263,7 +263,7 @@ def run_checks(
|
|
|
263
263
|
for c in common:
|
|
264
264
|
if c in key or not (_is_text(sb[c]) and _is_text(sa[c])):
|
|
265
265
|
continue
|
|
266
|
-
d = con.execute(f"SELECT count(DISTINCT {q(c)}) FROM {q(before)}").
|
|
266
|
+
d = con.execute(f"SELECT count(DISTINCT {q(c)}) FROM {q(before)}").fetchall()[0][0]
|
|
267
267
|
if d == 0 or d > t.category_max_distinct:
|
|
268
268
|
continue
|
|
269
269
|
new_vals = [r[0] for r in con.execute(
|
|
@@ -79,7 +79,7 @@ def verify(before: Any, after: Any, *, name: str = "table", key: list[str] | str
|
|
|
79
79
|
"No key given, so row-level changes were not compared. Add a key for stronger checks."))
|
|
80
80
|
else:
|
|
81
81
|
findings = []
|
|
82
|
-
stats = {"rows_after": con.execute('SELECT count(*) FROM "after"').
|
|
82
|
+
stats = {"rows_after": con.execute('SELECT count(*) FROM "after"').fetchall()[0][0]}
|
|
83
83
|
if source is not None:
|
|
84
84
|
spec = SourceSpec.from_dict(source)
|
|
85
85
|
load(con, parse_source(spec.data, base_dir), "source")
|
|
@@ -70,8 +70,8 @@ def run_source_checks(con: duckdb.DuckDBPyConnection, after: str, source: str, s
|
|
|
70
70
|
kcols = ", ".join(q("_k" + str(i)) for i in range(len(key)))
|
|
71
71
|
|
|
72
72
|
findings: list[Finding] = []
|
|
73
|
-
n_out = con.execute("SELECT count(*) FROM _pv_out").
|
|
74
|
-
n_src = con.execute(f"SELECT count(DISTINCT ({kcols})) FROM _pv_src").
|
|
73
|
+
n_out = con.execute("SELECT count(*) FROM _pv_out").fetchall()[0][0]
|
|
74
|
+
n_src = con.execute(f"SELECT count(DISTINCT ({kcols})) FROM _pv_src").fetchall()[0][0]
|
|
75
75
|
stats = {"source_keys": n_src, "output_rows": n_out}
|
|
76
76
|
|
|
77
77
|
for oc in spec.columns:
|
|
@@ -80,7 +80,7 @@ def run_source_checks(con: duckdb.DuckDBPyConnection, after: str, source: str, s
|
|
|
80
80
|
n = con.execute(
|
|
81
81
|
f"SELECT count(*) FROM (SELECT {kcols} FROM _pv_src GROUP BY ALL "
|
|
82
82
|
f"HAVING count(DISTINCT CAST({v} AS VARCHAR)) > 1) s "
|
|
83
|
-
f"WHERE EXISTS (SELECT 1 FROM _pv_out o WHERE {on})").
|
|
83
|
+
f"WHERE EXISTS (SELECT 1 FROM _pv_out o WHERE {on})").fetchall()[0][0]
|
|
84
84
|
if n:
|
|
85
85
|
ex = con.execute(
|
|
86
86
|
f"SELECT {kcols}, list(DISTINCT CAST({v} AS VARCHAR) ORDER BY CAST({v} AS VARCHAR)) FROM _pv_src "
|
|
@@ -99,7 +99,7 @@ def run_source_checks(con: duckdb.DuckDBPyConnection, after: str, source: str, s
|
|
|
99
99
|
n = con.execute(
|
|
100
100
|
f"SELECT count(*) FROM _pv_out o WHERE EXISTS (SELECT 1 FROM _pv_src s WHERE {on}) "
|
|
101
101
|
f"AND NOT EXISTS (SELECT 1 FROM _pv_src s WHERE {on} AND (({num}) OR {txt} "
|
|
102
|
-
f"OR (o.{q(oc)} IS NULL AND s.{v} IS NULL)))").
|
|
102
|
+
f"OR (o.{q(oc)} IS NULL AND s.{v} IS NULL)))").fetchall()[0][0]
|
|
103
103
|
if n:
|
|
104
104
|
pct = n / max(n_out, 1) * 100
|
|
105
105
|
sev = _grade(pct, t.source_mismatch_review_pct, t.source_mismatch_block_pct)
|
|
@@ -108,14 +108,14 @@ def run_source_checks(con: duckdb.DuckDBPyConnection, after: str, source: str, s
|
|
|
108
108
|
oc, {"count": n, "pct": round(pct, 4)}))
|
|
109
109
|
if "missing" in wanted:
|
|
110
110
|
n = con.execute(f"SELECT count(*) FROM (SELECT DISTINCT {kcols} FROM _pv_src) s "
|
|
111
|
-
f"WHERE NOT EXISTS (SELECT 1 FROM _pv_out o WHERE {on})").
|
|
111
|
+
f"WHERE NOT EXISTS (SELECT 1 FROM _pv_out o WHERE {on})").fetchall()[0][0]
|
|
112
112
|
if n:
|
|
113
113
|
pct = n / max(n_src, 1) * 100
|
|
114
114
|
findings.append(Finding("source_missing", _grade(pct, t.source_missing_review_pct, t.source_missing_block_pct),
|
|
115
115
|
f"{n:,} source key(s) never reached the output ({pct:.2f}%)", None,
|
|
116
116
|
{"count": n, "pct": round(pct, 4)}))
|
|
117
117
|
if "orphan" in wanted:
|
|
118
|
-
n = con.execute(f"SELECT count(*) FROM _pv_out o WHERE NOT EXISTS (SELECT 1 FROM _pv_src s WHERE {on})").
|
|
118
|
+
n = con.execute(f"SELECT count(*) FROM _pv_out o WHERE NOT EXISTS (SELECT 1 FROM _pv_src s WHERE {on})").fetchall()[0][0]
|
|
119
119
|
if n:
|
|
120
120
|
pct = n / max(n_out, 1) * 100
|
|
121
121
|
findings.append(Finding("source_orphan", REVIEW if pct > 0 else INFO,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|