Skip to content

Commit 927d2aa

Browse files
feat: add evalwire validate command and --strict flag on upload
Add DatasetValidator integration to the CLI: - New 'evalwire validate --csv PATH' command reports structural and row-level issues - New '--strict' flag on 'evalwire upload' aborts the upload when validation fails - 7 CLI tests covering valid/invalid CSV, custom column names, and strict mode
1 parent 251b521 commit 927d2aa

2 files changed

Lines changed: 159 additions & 1 deletion

File tree

src/evalwire/cli.py

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
"""evalwire CLI — ``evalwire upload`` and ``evalwire run`` commands."""
1+
"""evalwire CLI — ``evalwire upload``, ``evalwire run``, and ``evalwire validate`` commands."""
22

33
import sys
44
from typing import Literal, cast
@@ -58,6 +58,12 @@ def main() -> None:
5858
default=None,
5959
help="Pipe-split delimiter.",
6060
)
61+
@click.option(
62+
"--strict",
63+
is_flag=True,
64+
default=False,
65+
help="Abort upload if validation issues are found.",
66+
)
6167
@click.option(
6268
"--config",
6369
"config_path",
@@ -71,6 +77,7 @@ def upload_cmd(
7177
output_keys: str | None,
7278
tag_column: str | None,
7379
delimiter: str | None,
80+
strict: bool,
7481
config_path: str | None,
7582
) -> None:
7683
"""Upload a CSV testset to Arize Phoenix as one or more named datasets."""
@@ -102,6 +109,26 @@ def upload_cmd(
102109
"No CSV path provided. Use --csv or set csv_path in evalwire.toml."
103110
)
104111

112+
if strict:
113+
from evalwire.validator import DatasetValidator
114+
115+
validator = DatasetValidator()
116+
result = validator.validate(
117+
csv_path=resolved_csv,
118+
input_keys=resolved_input_keys,
119+
output_keys=resolved_output_keys,
120+
tag_column=resolved_tag_column,
121+
)
122+
if not result.is_valid:
123+
for issue in result.issues:
124+
row_info = f"row {issue.row}: " if issue.row is not None else ""
125+
click.echo(f" {row_info}{issue.message}", err=True)
126+
click.echo(
127+
f"Validation failed: {len(result.issues)} issue(s) found. Upload aborted.",
128+
err=True,
129+
)
130+
sys.exit(2)
131+
105132
from evalwire.uploader import DatasetUploader
106133

107134
client = _make_client()
@@ -204,3 +231,60 @@ def run_cmd(
204231
except Exception as exc:
205232
click.echo(f"Error: {exc}", err=True)
206233
sys.exit(2)
234+
235+
236+
@main.command("validate")
237+
@click.option("--csv", "csv_path", default=None, help="Path to the CSV file.")
238+
@click.option(
239+
"--input-keys",
240+
default="user_query",
241+
show_default=True,
242+
help="Comma-separated input column names.",
243+
)
244+
@click.option(
245+
"--output-keys",
246+
default="expected_output",
247+
show_default=True,
248+
help="Comma-separated output column names.",
249+
)
250+
@click.option(
251+
"--tag-column",
252+
default="tags",
253+
show_default=True,
254+
help="Column used for dataset splitting.",
255+
)
256+
def validate_cmd(
257+
csv_path: str | None,
258+
input_keys: str,
259+
output_keys: str,
260+
tag_column: str,
261+
) -> None:
262+
"""Validate a CSV testset for structural and content correctness."""
263+
if not csv_path:
264+
raise click.UsageError("No CSV path provided. Use --csv.")
265+
try:
266+
from evalwire.validator import DatasetValidator
267+
268+
resolved_input_keys = [k.strip() for k in input_keys.split(",")]
269+
resolved_output_keys = [k.strip() for k in output_keys.split(",")]
270+
271+
validator = DatasetValidator()
272+
result = validator.validate(
273+
csv_path=csv_path,
274+
input_keys=resolved_input_keys,
275+
output_keys=resolved_output_keys,
276+
tag_column=tag_column,
277+
)
278+
if result.is_valid:
279+
click.echo("Validation passed: testset is valid.")
280+
else:
281+
for issue in result.issues:
282+
row_info = f"row {issue.row}: " if issue.row is not None else ""
283+
click.echo(f" {row_info}{issue.message}")
284+
click.echo(f"Validation failed: {len(result.issues)} issue(s) found.")
285+
sys.exit(1)
286+
except click.UsageError:
287+
raise
288+
except Exception as exc:
289+
click.echo(f"Error: {exc}", err=True)
290+
sys.exit(2)

tests/test_cli.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,3 +207,77 @@ def test_help_text_available(self):
207207
assert result_upload.exit_code == 0
208208
result_run = _runner().invoke(main, ["run", "--help"])
209209
assert result_run.exit_code == 0
210+
211+
212+
class TestValidateCommand:
213+
def test_valid_csv_exits_zero(self, sample_csv: Path):
214+
result = _runner().invoke(main, ["validate", "--csv", str(sample_csv)])
215+
assert result.exit_code == 0
216+
assert "valid" in result.output.lower()
217+
218+
def test_missing_csv_exits_nonzero(self):
219+
result = _runner().invoke(main, ["validate"])
220+
assert result.exit_code != 0
221+
222+
def test_invalid_csv_exits_nonzero(self, tmp_path: Path):
223+
bad = tmp_path / "bad.csv"
224+
bad.write_text("wrong_col\nval\n")
225+
result = _runner().invoke(main, ["validate", "--csv", str(bad)])
226+
assert result.exit_code != 0
227+
assert "issue" in result.output.lower() or "missing" in result.output.lower()
228+
229+
def test_invalid_csv_reports_all_issues(self, tmp_path: Path):
230+
bad = tmp_path / "bad.csv"
231+
bad.write_text("wrong,also_wrong\nval,val\n")
232+
result = _runner().invoke(
233+
main,
234+
[
235+
"validate",
236+
"--csv",
237+
str(bad),
238+
"--input-keys",
239+
"user_query",
240+
"--output-keys",
241+
"expected_output",
242+
],
243+
)
244+
assert result.exit_code != 0
245+
assert "user_query" in result.output or "expected_output" in result.output
246+
247+
def test_custom_input_output_tag_columns(self, tmp_path: Path):
248+
f = tmp_path / "custom.csv"
249+
f.write_text("q,ans,grp\nhello,world,g1\n")
250+
result = _runner().invoke(
251+
main,
252+
[
253+
"validate",
254+
"--csv",
255+
str(f),
256+
"--input-keys",
257+
"q",
258+
"--output-keys",
259+
"ans",
260+
"--tag-column",
261+
"grp",
262+
],
263+
)
264+
assert result.exit_code == 0
265+
266+
267+
class TestUploadStrictFlag:
268+
def test_upload_strict_passes_for_valid_csv(self, sample_csv: Path):
269+
client = _mock_client()
270+
with patch("evalwire.cli._make_client", return_value=client):
271+
result = _runner().invoke(
272+
main, ["upload", "--csv", str(sample_csv), "--strict"]
273+
)
274+
assert result.exit_code == 0
275+
276+
def test_upload_strict_fails_for_invalid_csv(self, tmp_path: Path):
277+
bad = tmp_path / "bad.csv"
278+
bad.write_text("wrong\nval\n")
279+
client = _mock_client()
280+
with patch("evalwire.cli._make_client", return_value=client):
281+
result = _runner().invoke(main, ["upload", "--csv", str(bad), "--strict"])
282+
assert result.exit_code != 0
283+
assert client.datasets.create_dataset.call_count == 0

0 commit comments

Comments
 (0)