Unify dataset/tooling scripts into a single dwarf Typer CLI
Replace the five separately-hyphenated uv entry points (steps-to-parquet, steps-to-parquet-parallel, migrate-geant-steps, bump-dataset-version, create-root-files) plus the unregistered hparam_scan.py with one `dwarf` command exposing convert/migrate/bump-gen/bump-schema/status/ update-manifest/create-manifest/make-root/hparam-scan as subcommands. Each scripts/*.py module now only holds argparse-free business logic; scripts/dwarf.py wires it up with Typer, matching giant/cli.py's style. `dwarf convert` merges the old serial/parallel conversion scripts behind a --jobs flag (default 1: sequential with plain -o; >1: dataset-layout fan-out via subprocess). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -244,24 +244,3 @@ def test_run_all_caps_concurrency(tmp_path):
|
||||
concurrent += delta
|
||||
peak = max(peak, concurrent)
|
||||
assert peak <= 2
|
||||
|
||||
|
||||
def test_build_parser_defaults():
|
||||
args = create_root_files.build_parser().parse_args(
|
||||
[
|
||||
"--executable",
|
||||
"fake",
|
||||
"--detector",
|
||||
"pbwo4",
|
||||
"--num-files",
|
||||
"2",
|
||||
"--events-per-file",
|
||||
"10000",
|
||||
"--gen",
|
||||
"gen1",
|
||||
]
|
||||
)
|
||||
assert args.jobs == 4
|
||||
assert args.kind == "steps"
|
||||
assert args.dataset_root == "/ceph/lbogner/geant_steps"
|
||||
assert args.execute is False
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
from typer.testing import CliRunner
|
||||
|
||||
from scripts.dwarf import app
|
||||
|
||||
runner = CliRunner()
|
||||
|
||||
|
||||
def test_convert_rejects_jobs_below_one(tmp_path):
|
||||
root_file = tmp_path / "shard.root"
|
||||
root_file.touch()
|
||||
result = runner.invoke(app, ["convert", str(root_file), "--jobs", "0"])
|
||||
assert result.exit_code != 0
|
||||
assert "--jobs must be >= 1" in result.output
|
||||
|
||||
|
||||
def test_convert_rejects_output_with_multiple_files(tmp_path):
|
||||
a = tmp_path / "a.root"
|
||||
b = tmp_path / "b.root"
|
||||
a.touch()
|
||||
b.touch()
|
||||
result = runner.invoke(app, ["convert", str(a), str(b), "--output", "out.parquet"])
|
||||
assert result.exit_code != 0
|
||||
assert "--output can only be used with a single input file" in result.output
|
||||
|
||||
|
||||
def test_convert_rejects_output_with_parallel_jobs(tmp_path):
|
||||
root_file = tmp_path / "shard.root"
|
||||
root_file.touch()
|
||||
result = runner.invoke(
|
||||
app, ["convert", str(root_file), "--output", "out.parquet", "--jobs", "2"]
|
||||
)
|
||||
assert result.exit_code != 0
|
||||
assert "--output cannot be combined with --jobs > 1" in result.output
|
||||
|
||||
|
||||
def test_convert_default_jobs_is_one():
|
||||
result = runner.invoke(app, ["convert", "--help"])
|
||||
assert result.exit_code == 0
|
||||
assert "default: 1" in result.output
|
||||
|
||||
|
||||
def test_bump_gen_requires_reason():
|
||||
result = runner.invoke(app, ["bump-gen"])
|
||||
assert result.exit_code != 0
|
||||
assert "reason" in result.output.lower()
|
||||
|
||||
|
||||
def test_create_manifest_requires_exactly_one_of_output_or_pool(tmp_path):
|
||||
f = tmp_path / "a.parquet"
|
||||
f.touch()
|
||||
result = runner.invoke(app, ["create-manifest", str(f)])
|
||||
assert result.exit_code != 0
|
||||
assert "exactly one of --output or --pool is required" in result.output
|
||||
|
||||
|
||||
def test_create_manifest_requires_type_with_pool(tmp_path):
|
||||
f = tmp_path / "a.parquet"
|
||||
f.touch()
|
||||
result = runner.invoke(app, ["create-manifest", "--pool", "pbwo4", str(f)])
|
||||
assert result.exit_code != 0
|
||||
assert "--type is required when --pool is given" in result.output
|
||||
|
||||
|
||||
def test_status_reports_missing_root(tmp_path):
|
||||
missing = tmp_path / "does-not-exist"
|
||||
result = runner.invoke(app, ["status", "--root", str(missing)])
|
||||
assert result.exit_code != 0
|
||||
assert "is not a directory" in result.output
|
||||
@@ -1,4 +1,5 @@
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from scripts import steps_to_parquet_parallel
|
||||
@@ -40,7 +41,7 @@ def test_runs_one_job_per_file_and_reports_success(tmp_path):
|
||||
fake = _write_fake_executable(tmp_path, marker_dir)
|
||||
|
||||
files = [str(tmp_path / f"shard-{i:03d}.root") for i in range(3)]
|
||||
results = run_parallel(files, jobs=4, steps_to_parquet_path=fake)
|
||||
results = run_parallel(files, jobs=4, cmd_prefix=[sys.executable, str(fake)])
|
||||
|
||||
assert {r[0] for r in results} == set(files)
|
||||
assert all(code == 0 for _, code, _, _ in results)
|
||||
@@ -53,7 +54,7 @@ def test_failures_are_reported_with_nonzero_exit_code(tmp_path):
|
||||
fake = _write_fake_executable(tmp_path, marker_dir)
|
||||
|
||||
files = [str(tmp_path / "shard-000.root"), str(tmp_path / "shard-fail.root")]
|
||||
results = run_parallel(files, jobs=4, steps_to_parquet_path=fake)
|
||||
results = run_parallel(files, jobs=4, cmd_prefix=[sys.executable, str(fake)])
|
||||
|
||||
codes = {Path(f).stem: code for f, code, _, _ in results}
|
||||
assert codes["shard-000"] == 0
|
||||
@@ -66,7 +67,7 @@ def test_jobs_caps_concurrency(tmp_path):
|
||||
fake = _write_fake_executable(tmp_path, marker_dir)
|
||||
|
||||
files = [str(tmp_path / f"shard-{i:03d}.root") for i in range(6)]
|
||||
run_parallel(files, jobs=2, steps_to_parquet_path=fake)
|
||||
run_parallel(files, jobs=2, cmd_prefix=[sys.executable, str(fake)])
|
||||
|
||||
intervals = []
|
||||
for f in files:
|
||||
@@ -83,11 +84,6 @@ def test_jobs_caps_concurrency(tmp_path):
|
||||
assert peak <= 2
|
||||
|
||||
|
||||
def test_default_jobs_is_four():
|
||||
args = steps_to_parquet_parallel.build_parser().parse_args(["dummy.root"])
|
||||
assert args.jobs == 4
|
||||
|
||||
|
||||
def test_output_for_is_passed_through_as_output_flag(tmp_path):
|
||||
marker_dir = tmp_path / "markers"
|
||||
marker_dir.mkdir()
|
||||
@@ -108,7 +104,10 @@ sys.exit(0)
|
||||
root_file = str(tmp_path / "shard-000.root")
|
||||
dest = tmp_path / "processed" / "shard-000.parquet"
|
||||
run_parallel(
|
||||
[root_file], jobs=1, steps_to_parquet_path=fake, output_for={root_file: dest}
|
||||
[root_file],
|
||||
jobs=1,
|
||||
cmd_prefix=[sys.executable, str(fake)],
|
||||
output_for={root_file: dest},
|
||||
)
|
||||
assert (marker_dir / "shard-000.txt").read_text() == str(dest)
|
||||
|
||||
@@ -171,9 +170,3 @@ def test_resolve_destination_errors_on_wrong_shape(tmp_path):
|
||||
|
||||
def test_latest_schema_tag_returns_none_when_missing(tmp_path):
|
||||
assert latest_schema_tag(tmp_path / "does" / "not" / "exist") is None
|
||||
|
||||
|
||||
def test_dataset_root_and_schema_flags_default(tmp_path):
|
||||
args = steps_to_parquet_parallel.build_parser().parse_args(["dummy.root"])
|
||||
assert args.dataset_root == "/ceph/lbogner/geant_steps"
|
||||
assert args.schema is None
|
||||
|
||||
Reference in New Issue
Block a user