pipeverify 0.2.1__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.1 → pipeverify-0.2.3}/PKG-INFO +38 -12
- {pipeverify-0.2.1 → pipeverify-0.2.3}/README.md +36 -11
- pipeverify-0.2.3/metrics/README.md +14 -0
- {pipeverify-0.2.1 → pipeverify-0.2.3}/pipeverify/__init__.py +1 -1
- {pipeverify-0.2.1 → pipeverify-0.2.3}/pipeverify/cli.py +9 -7
- {pipeverify-0.2.1 → pipeverify-0.2.3}/pipeverify/report.py +3 -2
- {pipeverify-0.2.1 → pipeverify-0.2.3}/pipeverify/sources.py +28 -2
- {pipeverify-0.2.1 → pipeverify-0.2.3}/pyproject.toml +2 -2
- {pipeverify-0.2.1 → pipeverify-0.2.3}/.gitignore +0 -0
- {pipeverify-0.2.1 → pipeverify-0.2.3}/LICENSE +0 -0
- {pipeverify-0.2.1 → pipeverify-0.2.3}/NOTICE +0 -0
- {pipeverify-0.2.1 → pipeverify-0.2.3}/pipeverify/checks.py +0 -0
- {pipeverify-0.2.1 → pipeverify-0.2.3}/pipeverify/core.py +0 -0
- {pipeverify-0.2.1 → pipeverify-0.2.3}/pipeverify/explain.py +0 -0
- {pipeverify-0.2.1 → 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
|
|
@@ -29,16 +30,29 @@ Requires-Dist: pandas>=2.0; extra == 'sql'
|
|
|
29
30
|
Requires-Dist: sqlalchemy>=2.0; extra == 'sql'
|
|
30
31
|
Description-Content-Type: text/markdown
|
|
31
32
|
|
|
32
|
-
|
|
33
|
+
<p align="center">
|
|
34
|
+
<img src="docs/assets/banner.svg" alt="pipeverify" width="720">
|
|
35
|
+
</p>
|
|
33
36
|
|
|
34
|
-
|
|
37
|
+
<h3 align="center">The open-source verification step for data pipeline changes.</h3>
|
|
38
|
+
|
|
39
|
+
<p align="center">
|
|
40
|
+
Check that a change left the data right, not just the job green. pipeverify compares tables
|
|
41
|
+
before and after a change, and against the raw source, and returns <b>PASS</b>, <b>REVIEW</b> or <b>BLOCK</b>.
|
|
42
|
+
Works with SQL, Spark, dbt, Airflow, or a fix written by an AI agent.
|
|
43
|
+
</p>
|
|
44
|
+
|
|
45
|
+
<p align="center">
|
|
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>
|
|
48
|
+
<a href="LICENSE"><img src="https://img.shields.io/badge/license-Apache--2.0-4ade80" alt="License"></a>
|
|
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>
|
|
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>
|
|
51
|
+
<a href="https://colab.research.google.com/github/akshaysharma1088/pipeverify/blob/main/examples/notebooks/quickstart.ipynb"><img src="https://colab.research.google.com/assets/colab-badge.svg" alt="Open in Colab"></a>
|
|
52
|
+
</p>
|
|
53
|
+
|
|
54
|
+
---
|
|
35
55
|
|
|
36
|
-
[](https://github.com/akshaysharma1088/pipeverify/actions/workflows/ci.yml)
|
|
37
|
-
[](LICENSE)
|
|
38
|
-

|
|
39
|
-

|
|
40
|
-
[](https://pypi.org/project/pipeverify/)
|
|
41
|
-
[](https://doi.org/10.5281/zenodo.23542277)
|
|
42
56
|
|
|
43
57
|
A pipeline change can pass every test, finish on time and keep the schema intact while quietly
|
|
44
58
|
dropping rows, duplicating keys or changing what a column means. More of these changes are now
|
|
@@ -65,6 +79,8 @@ so any team can run it on every change.
|
|
|
65
79
|
|
|
66
80
|
## Quick start
|
|
67
81
|
|
|
82
|
+
No install needed to try it: [open the 5-minute notebook in Colab](https://colab.research.google.com/github/akshaysharma1088/pipeverify/blob/main/examples/notebooks/quickstart.ipynb).
|
|
83
|
+
|
|
68
84
|
```bash
|
|
69
85
|
pip install pipeverify
|
|
70
86
|
|
|
@@ -107,7 +123,7 @@ Every one of these runs successfully and keeps the same schema.
|
|
|
107
123
|
|
|
108
124
|
| Source | CLI form | Config form |
|
|
109
125
|
|---|---|---|
|
|
110
|
-
| CSV, Parquet, JSON file | `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` |
|
|
111
127
|
| DuckDB table | `duckdb:warehouse.db:gold.orders` | `{duckdb: warehouse.db, table: gold.orders}` |
|
|
112
128
|
| DuckDB query | `"duckdb:warehouse.db:SELECT ..."` | `{duckdb: warehouse.db, query: "SELECT ..."}` |
|
|
113
129
|
| Any SQLAlchemy database (Postgres, Snowflake, BigQuery, ...) | | `{url: "postgresql://${PGUSER}@host/db", table: gold.orders}` |
|
|
@@ -116,6 +132,8 @@ Every one of these runs successfully and keeps the same schema.
|
|
|
116
132
|
SQL databases need `pip install "pipeverify[sql]"` plus the driver for your database. Environment
|
|
117
133
|
variables in URLs are expanded, so credentials stay out of the config file.
|
|
118
134
|
|
|
135
|
+
See [SQL source examples](docs/sources.md) for Postgres, Snowflake and BigQuery.
|
|
136
|
+
|
|
119
137
|
## Config
|
|
120
138
|
|
|
121
139
|
Check many tables at once with `pipeverify.yml` (`pipeverify init` writes a starter file):
|
|
@@ -215,7 +233,7 @@ jobs:
|
|
|
215
233
|
steps:
|
|
216
234
|
- uses: actions/checkout@v4
|
|
217
235
|
# ... build the "after" snapshots for this PR here ...
|
|
218
|
-
- uses: akshaysharma1088/pipeverify@v0.2.
|
|
236
|
+
- uses: akshaysharma1088/pipeverify@v0.2.2
|
|
219
237
|
with:
|
|
220
238
|
config: pipeverify.yml
|
|
221
239
|
fail-on: block # or review, or never
|
|
@@ -250,6 +268,12 @@ This adds a short plain-English explanation of what probably happened and what t
|
|
|
250
268
|
It is off by default. Only the findings are sent (check names, counts, percentages, column names);
|
|
251
269
|
example values from your data are removed first. The verdict never depends on the LLM.
|
|
252
270
|
|
|
271
|
+
## Who uses pipeverify
|
|
272
|
+
|
|
273
|
+
See [ADOPTERS.md](ADOPTERS.md). Using it at work or in a project?
|
|
274
|
+
[Tell us how](https://github.com/akshaysharma1088/pipeverify/issues/new?template=share_usage.yml), even
|
|
275
|
+
in one line. It helps other teams decide whether to try it and shapes what gets built next.
|
|
276
|
+
|
|
253
277
|
## Roadmap
|
|
254
278
|
|
|
255
279
|
- Native adapters for dbt (compare a PR's models against production), Airflow and Spark
|
|
@@ -257,7 +281,9 @@ example values from your data are removed first. The verdict never depends on th
|
|
|
257
281
|
- Rules learned from history: what "normal" change looks like for each table
|
|
258
282
|
- Agent integration: a guardrail that coding and data agents call before applying a change
|
|
259
283
|
|
|
260
|
-
Ideas and pull requests are welcome; see [CONTRIBUTING.md](CONTRIBUTING.md)
|
|
284
|
+
Ideas and pull requests are welcome; see [CONTRIBUTING.md](CONTRIBUTING.md) and the
|
|
285
|
+
[good first issues](https://github.com/akshaysharma1088/pipeverify/labels/good%20first%20issue).
|
|
286
|
+
Questions go in [Discussions](https://github.com/akshaysharma1088/pipeverify/discussions).
|
|
261
287
|
|
|
262
288
|
## Status
|
|
263
289
|
|
|
@@ -1,13 +1,26 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="docs/assets/banner.svg" alt="pipeverify" width="720">
|
|
3
|
+
</p>
|
|
2
4
|
|
|
3
|
-
|
|
5
|
+
<h3 align="center">The open-source verification step for data pipeline changes.</h3>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
Check that a change left the data right, not just the job green. pipeverify compares tables
|
|
9
|
+
before and after a change, and against the raw source, and returns <b>PASS</b>, <b>REVIEW</b> or <b>BLOCK</b>.
|
|
10
|
+
Works with SQL, Spark, dbt, Airflow, or a fix written by an AI agent.
|
|
11
|
+
</p>
|
|
12
|
+
|
|
13
|
+
<p align="center">
|
|
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>
|
|
16
|
+
<a href="LICENSE"><img src="https://img.shields.io/badge/license-Apache--2.0-4ade80" alt="License"></a>
|
|
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>
|
|
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>
|
|
19
|
+
<a href="https://colab.research.google.com/github/akshaysharma1088/pipeverify/blob/main/examples/notebooks/quickstart.ipynb"><img src="https://colab.research.google.com/assets/colab-badge.svg" alt="Open in Colab"></a>
|
|
20
|
+
</p>
|
|
21
|
+
|
|
22
|
+
---
|
|
4
23
|
|
|
5
|
-
[](https://github.com/akshaysharma1088/pipeverify/actions/workflows/ci.yml)
|
|
6
|
-
[](LICENSE)
|
|
7
|
-

|
|
8
|
-

|
|
9
|
-
[](https://pypi.org/project/pipeverify/)
|
|
10
|
-
[](https://doi.org/10.5281/zenodo.23542277)
|
|
11
24
|
|
|
12
25
|
A pipeline change can pass every test, finish on time and keep the schema intact while quietly
|
|
13
26
|
dropping rows, duplicating keys or changing what a column means. More of these changes are now
|
|
@@ -34,6 +47,8 @@ so any team can run it on every change.
|
|
|
34
47
|
|
|
35
48
|
## Quick start
|
|
36
49
|
|
|
50
|
+
No install needed to try it: [open the 5-minute notebook in Colab](https://colab.research.google.com/github/akshaysharma1088/pipeverify/blob/main/examples/notebooks/quickstart.ipynb).
|
|
51
|
+
|
|
37
52
|
```bash
|
|
38
53
|
pip install pipeverify
|
|
39
54
|
|
|
@@ -76,7 +91,7 @@ Every one of these runs successfully and keeps the same schema.
|
|
|
76
91
|
|
|
77
92
|
| Source | CLI form | Config form |
|
|
78
93
|
|---|---|---|
|
|
79
|
-
| CSV, Parquet, JSON file | `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` |
|
|
80
95
|
| DuckDB table | `duckdb:warehouse.db:gold.orders` | `{duckdb: warehouse.db, table: gold.orders}` |
|
|
81
96
|
| DuckDB query | `"duckdb:warehouse.db:SELECT ..."` | `{duckdb: warehouse.db, query: "SELECT ..."}` |
|
|
82
97
|
| Any SQLAlchemy database (Postgres, Snowflake, BigQuery, ...) | | `{url: "postgresql://${PGUSER}@host/db", table: gold.orders}` |
|
|
@@ -85,6 +100,8 @@ Every one of these runs successfully and keeps the same schema.
|
|
|
85
100
|
SQL databases need `pip install "pipeverify[sql]"` plus the driver for your database. Environment
|
|
86
101
|
variables in URLs are expanded, so credentials stay out of the config file.
|
|
87
102
|
|
|
103
|
+
See [SQL source examples](docs/sources.md) for Postgres, Snowflake and BigQuery.
|
|
104
|
+
|
|
88
105
|
## Config
|
|
89
106
|
|
|
90
107
|
Check many tables at once with `pipeverify.yml` (`pipeverify init` writes a starter file):
|
|
@@ -184,7 +201,7 @@ jobs:
|
|
|
184
201
|
steps:
|
|
185
202
|
- uses: actions/checkout@v4
|
|
186
203
|
# ... build the "after" snapshots for this PR here ...
|
|
187
|
-
- uses: akshaysharma1088/pipeverify@v0.2.
|
|
204
|
+
- uses: akshaysharma1088/pipeverify@v0.2.2
|
|
188
205
|
with:
|
|
189
206
|
config: pipeverify.yml
|
|
190
207
|
fail-on: block # or review, or never
|
|
@@ -219,6 +236,12 @@ This adds a short plain-English explanation of what probably happened and what t
|
|
|
219
236
|
It is off by default. Only the findings are sent (check names, counts, percentages, column names);
|
|
220
237
|
example values from your data are removed first. The verdict never depends on the LLM.
|
|
221
238
|
|
|
239
|
+
## Who uses pipeverify
|
|
240
|
+
|
|
241
|
+
See [ADOPTERS.md](ADOPTERS.md). Using it at work or in a project?
|
|
242
|
+
[Tell us how](https://github.com/akshaysharma1088/pipeverify/issues/new?template=share_usage.yml), even
|
|
243
|
+
in one line. It helps other teams decide whether to try it and shapes what gets built next.
|
|
244
|
+
|
|
222
245
|
## Roadmap
|
|
223
246
|
|
|
224
247
|
- Native adapters for dbt (compare a PR's models against production), Airflow and Spark
|
|
@@ -226,7 +249,9 @@ example values from your data are removed first. The verdict never depends on th
|
|
|
226
249
|
- Rules learned from history: what "normal" change looks like for each table
|
|
227
250
|
- Agent integration: a guardrail that coding and data agents call before applying a change
|
|
228
251
|
|
|
229
|
-
Ideas and pull requests are welcome; see [CONTRIBUTING.md](CONTRIBUTING.md)
|
|
252
|
+
Ideas and pull requests are welcome; see [CONTRIBUTING.md](CONTRIBUTING.md) and the
|
|
253
|
+
[good first issues](https://github.com/akshaysharma1088/pipeverify/labels/good%20first%20issue).
|
|
254
|
+
Questions go in [Discussions](https://github.com/akshaysharma1088/pipeverify/discussions).
|
|
230
255
|
|
|
231
256
|
## Status
|
|
232
257
|
|
|
@@ -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
|
|
|
@@ -96,6 +97,17 @@ def _resolve(path: str, base_dir: str) -> str:
|
|
|
96
97
|
return path if os.path.isabs(path) else os.path.normpath(os.path.join(base_dir, path))
|
|
97
98
|
|
|
98
99
|
|
|
100
|
+
COMPRESSION_SUFFIXES = (".gz", ".zst")
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _split_extension(path: str) -> tuple[str, str]:
|
|
104
|
+
"""Return (file extension, compression suffix), e.g. 'orders.csv.gz' -> ('.csv', '.gz')."""
|
|
105
|
+
base, ext = os.path.splitext(path.lower())
|
|
106
|
+
if ext in COMPRESSION_SUFFIXES:
|
|
107
|
+
return os.path.splitext(base)[1], ext
|
|
108
|
+
return ext, ""
|
|
109
|
+
|
|
110
|
+
|
|
99
111
|
def load(con: duckdb.DuckDBPyConnection, source: Source, name: str) -> None:
|
|
100
112
|
"""Materialize a source as table `name` in the DuckDB connection."""
|
|
101
113
|
if source.kind == "dataframe":
|
|
@@ -107,10 +119,24 @@ def load(con: duckdb.DuckDBPyConnection, source: Source, name: str) -> None:
|
|
|
107
119
|
path = source.path or ""
|
|
108
120
|
if not os.path.exists(path):
|
|
109
121
|
raise SourceError(f"File not found: {path}")
|
|
110
|
-
ext =
|
|
122
|
+
ext, compression = _split_extension(path)
|
|
111
123
|
reader = FILE_READERS.get(ext)
|
|
112
124
|
if reader is None:
|
|
113
|
-
raise SourceError(
|
|
125
|
+
raise SourceError(
|
|
126
|
+
f"Unsupported file type '{ext}{compression}'. "
|
|
127
|
+
"Use csv, parquet or json, optionally compressed with .gz or .zst.")
|
|
128
|
+
if compression and reader == "read_parquet":
|
|
129
|
+
raise SourceError(
|
|
130
|
+
f"Compressed Parquet ('{ext}{compression}') is not supported. Parquet is already "
|
|
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
|
|
114
140
|
con.execute(f'CREATE OR REPLACE TABLE "{name}" AS SELECT * FROM {reader}(?)', [path])
|
|
115
141
|
return
|
|
116
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"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|