pipeverify 0.2.2__tar.gz → 0.2.3__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.2 → pipeverify-0.2.3}/PKG-INFO +4 -2
- {pipeverify-0.2.2 → pipeverify-0.2.3}/README.md +2 -1
- pipeverify-0.2.3/metrics/README.md +14 -0
- {pipeverify-0.2.2 → pipeverify-0.2.3}/pipeverify/__init__.py +1 -1
- {pipeverify-0.2.2 → pipeverify-0.2.3}/pipeverify/cli.py +9 -7
- {pipeverify-0.2.2 → pipeverify-0.2.3}/pipeverify/report.py +3 -2
- {pipeverify-0.2.2 → pipeverify-0.2.3}/pipeverify/sources.py +9 -0
- {pipeverify-0.2.2 → pipeverify-0.2.3}/pyproject.toml +2 -2
- pipeverify-0.2.2/metrics/README.md +0 -14
- {pipeverify-0.2.2 → pipeverify-0.2.3}/.gitignore +0 -0
- {pipeverify-0.2.2 → pipeverify-0.2.3}/LICENSE +0 -0
- {pipeverify-0.2.2 → pipeverify-0.2.3}/NOTICE +0 -0
- {pipeverify-0.2.2 → pipeverify-0.2.3}/pipeverify/checks.py +0 -0
- {pipeverify-0.2.2 → pipeverify-0.2.3}/pipeverify/core.py +0 -0
- {pipeverify-0.2.2 → pipeverify-0.2.3}/pipeverify/explain.py +0 -0
- {pipeverify-0.2.2 → pipeverify-0.2.3}/pipeverify/source_checks.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.3
|
|
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
|
|
@@ -22,6 +22,7 @@ Provides-Extra: ai
|
|
|
22
22
|
Requires-Dist: anthropic>=0.40; extra == 'ai'
|
|
23
23
|
Requires-Dist: openai>=1.40; extra == 'ai'
|
|
24
24
|
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: openpyxl; extra == 'dev'
|
|
25
26
|
Requires-Dist: pandas>=2.0; extra == 'dev'
|
|
26
27
|
Requires-Dist: pytest>=8; extra == 'dev'
|
|
27
28
|
Provides-Extra: sql
|
|
@@ -43,6 +44,7 @@ Works with SQL, Spark, dbt, Airflow, or a fix written by an AI agent.
|
|
|
43
44
|
|
|
44
45
|
<p align="center">
|
|
45
46
|
<a href="https://pypi.org/project/pipeverify/"><img src="https://img.shields.io/pypi/v/pipeverify?label=pypi&color=38bdf8" alt="PyPI"></a>
|
|
47
|
+
<a href="https://pepy.tech/projects/pipeverify"><img src="https://static.pepy.tech/personalized-badge/pipeverify?period=total&units=INTERNATIONAL_SYSTEM&left_color=BLACK&right_color=GREEN&left_text=downloads" alt="PyPI Downloads"></a>
|
|
46
48
|
<a href="LICENSE"><img src="https://img.shields.io/badge/license-Apache--2.0-4ade80" alt="License"></a>
|
|
47
49
|
<a href="https://github.com/akshaysharma1088/pipeverify/actions/workflows/ci.yml"><img src="https://github.com/akshaysharma1088/pipeverify/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
|
|
48
50
|
<a href="https://doi.org/10.5281/zenodo.23542277"><img src="https://zenodo.org/badge/DOI/10.5281/zenodo.23542277.svg" alt="DOI"></a>
|
|
@@ -121,7 +123,7 @@ Every one of these runs successfully and keeps the same schema.
|
|
|
121
123
|
|
|
122
124
|
| Source | CLI form | Config form |
|
|
123
125
|
|---|---|---|
|
|
124
|
-
| CSV, Parquet, JSON file (CSV and JSON can be `.gz` or `.zst` compressed) | `data/orders.parquet` | `path: data/orders.parquet` |
|
|
126
|
+
| CSV, Parquet, JSON, Excel (.xlsx) file (CSV and JSON can be `.gz` or `.zst` compressed) | `data/orders.parquet` | `path: data/orders.parquet` |
|
|
125
127
|
| DuckDB table | `duckdb:warehouse.db:gold.orders` | `{duckdb: warehouse.db, table: gold.orders}` |
|
|
126
128
|
| DuckDB query | `"duckdb:warehouse.db:SELECT ..."` | `{duckdb: warehouse.db, query: "SELECT ..."}` |
|
|
127
129
|
| Any SQLAlchemy database (Postgres, Snowflake, BigQuery, ...) | | `{url: "postgresql://${PGUSER}@host/db", table: gold.orders}` |
|
|
@@ -12,6 +12,7 @@ Works with SQL, Spark, dbt, Airflow, or a fix written by an AI agent.
|
|
|
12
12
|
|
|
13
13
|
<p align="center">
|
|
14
14
|
<a href="https://pypi.org/project/pipeverify/"><img src="https://img.shields.io/pypi/v/pipeverify?label=pypi&color=38bdf8" alt="PyPI"></a>
|
|
15
|
+
<a href="https://pepy.tech/projects/pipeverify"><img src="https://static.pepy.tech/personalized-badge/pipeverify?period=total&units=INTERNATIONAL_SYSTEM&left_color=BLACK&right_color=GREEN&left_text=downloads" alt="PyPI Downloads"></a>
|
|
15
16
|
<a href="LICENSE"><img src="https://img.shields.io/badge/license-Apache--2.0-4ade80" alt="License"></a>
|
|
16
17
|
<a href="https://github.com/akshaysharma1088/pipeverify/actions/workflows/ci.yml"><img src="https://github.com/akshaysharma1088/pipeverify/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
|
|
17
18
|
<a href="https://doi.org/10.5281/zenodo.23542277"><img src="https://zenodo.org/badge/DOI/10.5281/zenodo.23542277.svg" alt="DOI"></a>
|
|
@@ -90,7 +91,7 @@ Every one of these runs successfully and keeps the same schema.
|
|
|
90
91
|
|
|
91
92
|
| Source | CLI form | Config form |
|
|
92
93
|
|---|---|---|
|
|
93
|
-
| CSV, Parquet, JSON file (CSV and JSON can be `.gz` or `.zst` compressed) | `data/orders.parquet` | `path: data/orders.parquet` |
|
|
94
|
+
| CSV, Parquet, JSON, Excel (.xlsx) file (CSV and JSON can be `.gz` or `.zst` compressed) | `data/orders.parquet` | `path: data/orders.parquet` |
|
|
94
95
|
| DuckDB table | `duckdb:warehouse.db:gold.orders` | `{duckdb: warehouse.db, table: gold.orders}` |
|
|
95
96
|
| DuckDB query | `"duckdb:warehouse.db:SELECT ..."` | `{duckdb: warehouse.db, query: "SELECT ..."}` |
|
|
96
97
|
| Any SQLAlchemy database (Postgres, Snowflake, BigQuery, ...) | | `{url: "postgresql://${PGUSER}@host/db", table: gold.orders}` |
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Usage metrics
|
|
2
|
+
|
|
3
|
+
Updated 2026-11-09 by `scripts/collect_metrics.py`. Raw history is in the CSV files in this folder.
|
|
4
|
+
|
|
5
|
+
| Metric | Value |
|
|
6
|
+
|---|---|
|
|
7
|
+
| GitHub stars | 1 |
|
|
8
|
+
| Forks | 4 |
|
|
9
|
+
| Watchers | 0 |
|
|
10
|
+
| Repositories that depend on it | n/a |
|
|
11
|
+
| Repository views since 2026-10-08 | 128 |
|
|
12
|
+
| Repository clones since 2026-10-08 | 496 |
|
|
13
|
+
| PyPI downloads since 2026-10-21 (without mirrors) | 450 |
|
|
14
|
+
| PyPI downloads, last 30 days | 0 |
|
|
@@ -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.3"
|
|
4
4
|
|
|
5
5
|
from .checks import Finding, Thresholds # noqa: E402
|
|
6
6
|
from .core import RunResult, TableResult, run_config, verify # noqa: E402
|
|
@@ -28,19 +28,19 @@ tables:
|
|
|
28
28
|
|
|
29
29
|
|
|
30
30
|
def _emit(result: RunResult, fmt: str, output: str | None, summary: str | None, show_info: bool,
|
|
31
|
-
json_out: str | None = None, markdown_out: str | None = None) -> None:
|
|
31
|
+
json_out: str | None = None, markdown_out: str | None = None, max_examples: int = 1) -> None:
|
|
32
32
|
for path, render in ((json_out, to_json), (markdown_out, to_markdown)):
|
|
33
33
|
if path:
|
|
34
34
|
with open(path, "w", encoding="utf-8") as fh:
|
|
35
35
|
body = render(result)
|
|
36
36
|
fh.write(body if body.endswith("\n") else body + "\n")
|
|
37
|
-
text = {"text": lambda: to_text(result, show_info), "markdown": lambda: to_markdown(result),
|
|
37
|
+
text = {"text": lambda: to_text(result, show_info, max_examples), "markdown": lambda: to_markdown(result),
|
|
38
38
|
"json": lambda: to_json(result)}[fmt]()
|
|
39
39
|
if output:
|
|
40
40
|
with open(output, "w", encoding="utf-8") as fh:
|
|
41
41
|
fh.write(text if text.endswith("\n") else text + "\n")
|
|
42
42
|
if fmt != "text":
|
|
43
|
-
click.echo(to_text(result, show_info))
|
|
43
|
+
click.echo(to_text(result, show_info, max_examples))
|
|
44
44
|
else:
|
|
45
45
|
click.echo(text)
|
|
46
46
|
if summary:
|
|
@@ -79,6 +79,8 @@ output_options = [
|
|
|
79
79
|
click.option("--show-info", is_flag=True, help="Also print informational findings."),
|
|
80
80
|
click.option("--json-out", type=click.Path(dir_okay=False), help="Also write a JSON report here."),
|
|
81
81
|
click.option("--markdown-out", type=click.Path(dir_okay=False), help="Also write a Markdown report here."),
|
|
82
|
+
click.option("--max-examples", type=int, default=1, show_default=True,
|
|
83
|
+
help="Example rows to show per finding."),
|
|
82
84
|
]
|
|
83
85
|
|
|
84
86
|
|
|
@@ -110,7 +112,7 @@ def main() -> None:
|
|
|
110
112
|
@with_output_options
|
|
111
113
|
def compare(before, after, source_data, source_key, source_columns, source_where,
|
|
112
114
|
key, expect_change, ignore, expect_row_change, name,
|
|
113
|
-
fmt, output, summary, fail_on, explain, show_info, json_out, markdown_out):
|
|
115
|
+
fmt, output, summary, fail_on, explain, show_info, json_out, markdown_out, max_examples):
|
|
114
116
|
"""Compare one table BEFORE and AFTER a change.
|
|
115
117
|
|
|
116
118
|
Use - as BEFORE to run only the source checks.
|
|
@@ -135,7 +137,7 @@ def compare(before, after, source_data, source_key, source_columns, source_where
|
|
|
135
137
|
raise click.ClickException(str(exc))
|
|
136
138
|
result = RunResult(tables=[res], seconds=time.perf_counter() - start)
|
|
137
139
|
_maybe_explain(result, explain)
|
|
138
|
-
_emit(result, fmt, output, summary, show_info, json_out, markdown_out)
|
|
140
|
+
_emit(result, fmt, output, summary, show_info, json_out, markdown_out, max_examples)
|
|
139
141
|
_exit(result, fail_on)
|
|
140
142
|
|
|
141
143
|
|
|
@@ -144,7 +146,7 @@ def compare(before, after, source_data, source_key, source_columns, source_where
|
|
|
144
146
|
type=click.Path(exists=True, dir_okay=False), help="Config file listing the tables.")
|
|
145
147
|
@click.option("--table", "-t", multiple=True, help="Only check these table names. Repeatable.")
|
|
146
148
|
@with_output_options
|
|
147
|
-
def run(config, table, fmt, output, summary, fail_on, explain, show_info, json_out, markdown_out):
|
|
149
|
+
def run(config, table, fmt, output, summary, fail_on, explain, show_info, json_out, markdown_out, max_examples):
|
|
148
150
|
"""Check every table listed in a config file."""
|
|
149
151
|
try:
|
|
150
152
|
result = run_config(config, only=list(table) or None)
|
|
@@ -153,7 +155,7 @@ def run(config, table, fmt, output, summary, fail_on, explain, show_info, json_o
|
|
|
153
155
|
if not result.tables:
|
|
154
156
|
raise click.ClickException("No tables matched")
|
|
155
157
|
_maybe_explain(result, explain)
|
|
156
|
-
_emit(result, fmt, output, summary, show_info, json_out, markdown_out)
|
|
158
|
+
_emit(result, fmt, output, summary, show_info, json_out, markdown_out, max_examples)
|
|
157
159
|
_exit(result, fail_on)
|
|
158
160
|
|
|
159
161
|
|
|
@@ -29,7 +29,7 @@ def _color(text: str, sev: str) -> str:
|
|
|
29
29
|
return f"\033[{code}m{text}\033[0m"
|
|
30
30
|
|
|
31
31
|
|
|
32
|
-
def to_text(result: RunResult, show_info: bool = False) -> str:
|
|
32
|
+
def to_text(result: RunResult, show_info: bool = False, max_examples: int = 1) -> str:
|
|
33
33
|
lines = []
|
|
34
34
|
for t in result.tables:
|
|
35
35
|
lines.append(_color(f"{LABEL[t.verdict]:<7}", t.verdict) + f" {t.name}")
|
|
@@ -46,7 +46,8 @@ def to_text(result: RunResult, show_info: bool = False) -> str:
|
|
|
46
46
|
lines.append(" " + _color(f"{LABEL[f.severity]:<6}", f.severity) + f" {f.message}")
|
|
47
47
|
ex = f.metrics.get("examples") if f.metrics else None
|
|
48
48
|
if ex:
|
|
49
|
-
|
|
49
|
+
for row in ex[:max(0, max_examples)]:
|
|
50
|
+
lines.append(f" e.g. {row}")
|
|
50
51
|
lines.append("")
|
|
51
52
|
lines.append(_color(f"Verdict: {LABEL[result.verdict]}", result.verdict) + f" {_headline(result)}")
|
|
52
53
|
if result.explanation:
|
|
@@ -18,6 +18,7 @@ FILE_READERS = {
|
|
|
18
18
|
".json": "read_json_auto",
|
|
19
19
|
".jsonl": "read_json_auto",
|
|
20
20
|
".ndjson": "read_json_auto",
|
|
21
|
+
".xlsx": "read_xlsx",
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
|
|
@@ -128,6 +129,14 @@ def load(con: duckdb.DuckDBPyConnection, source: Source, name: str) -> None:
|
|
|
128
129
|
raise SourceError(
|
|
129
130
|
f"Compressed Parquet ('{ext}{compression}') is not supported. Parquet is already "
|
|
130
131
|
f"compressed internally, so use the plain {ext} file.")
|
|
132
|
+
if ext == ".xlsx":
|
|
133
|
+
try:
|
|
134
|
+
con.execute("INSTALL excel")
|
|
135
|
+
con.execute("LOAD excel")
|
|
136
|
+
except duckdb.Error as e:
|
|
137
|
+
raise SourceError(
|
|
138
|
+
f"Excel extension could not be installed or loaded: {e} "
|
|
139
|
+
) from e
|
|
131
140
|
con.execute(f'CREATE OR REPLACE TABLE "{name}" AS SELECT * FROM {reader}(?)', [path])
|
|
132
141
|
return
|
|
133
142
|
if source.kind == "duckdb":
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "pipeverify"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.3"
|
|
8
8
|
description = "Check that a pipeline change left the data right, not just the job green."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "Apache-2.0"
|
|
@@ -23,7 +23,7 @@ dependencies = ["duckdb>=1.0", "pyyaml>=6.0", "click>=8.1"]
|
|
|
23
23
|
[project.optional-dependencies]
|
|
24
24
|
sql = ["sqlalchemy>=2.0", "pandas>=2.0"]
|
|
25
25
|
ai = ["anthropic>=0.40", "openai>=1.40"]
|
|
26
|
-
dev = ["pytest>=8", "pandas>=2.0"]
|
|
26
|
+
dev = ["pytest>=8", "pandas>=2.0", "openpyxl"]
|
|
27
27
|
|
|
28
28
|
[project.scripts]
|
|
29
29
|
pipeverify = "pipeverify.cli:main"
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
# Usage metrics
|
|
2
|
-
|
|
3
|
-
Updated 2026-10-22 by `scripts/collect_metrics.py`. Raw history is in the CSV files in this folder.
|
|
4
|
-
|
|
5
|
-
| Metric | Value |
|
|
6
|
-
|---|---|
|
|
7
|
-
| GitHub stars | 1 |
|
|
8
|
-
| Forks | 0 |
|
|
9
|
-
| Watchers | 0 |
|
|
10
|
-
| Repositories that depend on it | n/a |
|
|
11
|
-
| Repository views since 2026-10-08 | 0 |
|
|
12
|
-
| Repository clones since 2026-10-08 | 162 |
|
|
13
|
-
| PyPI downloads since 2026-10-21 (without mirrors) | 228 |
|
|
14
|
-
| PyPI downloads, last 30 days | 0 |
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|