Skip to content

Commit d0c6a1d

Browse files
committed
Add scripts/cleanup-failed-outputs.py for one-off purge of stale failed outputs
1 parent e9025d3 commit d0c6a1d

1 file changed

Lines changed: 92 additions & 0 deletions

File tree

scripts/cleanup-failed-outputs.py

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
#!/usr/bin/env python3
2+
"""
3+
One-off cleanup: remove notebook output JSONs that recorded a failed execution.
4+
5+
These outputs were generated in environments that can't actually run the
6+
notebooks (older local builds on a Windows box with the wrong Python, batt
7+
notebooks before pathsim-batt was a CI dependency, etc.). Without this
8+
cleanup, those `success: false` files persist forever and the build-ok
9+
marker logic keeps re-running them — that's correct, but a one-time sweep
10+
is faster than waiting 6h for the next CI cron to do it incrementally.
11+
12+
Notebooks (`notebooks/*.ipynb`) and `api.json` are NOT touched. Only the
13+
`outputs/*.json` files that recorded failure are removed. The next build
14+
will regenerate them.
15+
16+
Usage:
17+
python scripts/cleanup-failed-outputs.py # apply
18+
python scripts/cleanup-failed-outputs.py --dry-run # preview only
19+
"""
20+
21+
from __future__ import annotations
22+
23+
import argparse
24+
import json
25+
import sys
26+
from pathlib import Path
27+
28+
29+
STATIC_DIR = Path(__file__).resolve().parent.parent / "static"
30+
31+
32+
def find_failed_outputs(static_dir: Path) -> list[Path]:
33+
"""Return all outputs/*.json files whose success flag is False."""
34+
failed: list[Path] = []
35+
for output_file in static_dir.glob("*/v*/outputs/*.json"):
36+
try:
37+
with open(output_file, "r", encoding="utf-8") as f:
38+
data = json.load(f)
39+
except (json.JSONDecodeError, OSError) as e:
40+
print(f" ! could not read {output_file}: {e}", file=sys.stderr)
41+
continue
42+
if data.get("success") is False:
43+
failed.append(output_file)
44+
return failed
45+
46+
47+
def main() -> int:
48+
parser = argparse.ArgumentParser(description=__doc__)
49+
parser.add_argument(
50+
"--dry-run",
51+
action="store_true",
52+
help="Show what would be deleted, change nothing.",
53+
)
54+
args = parser.parse_args()
55+
56+
if not STATIC_DIR.exists():
57+
print(f"static/ not found at {STATIC_DIR}", file=sys.stderr)
58+
return 1
59+
60+
failed = find_failed_outputs(STATIC_DIR)
61+
62+
if not failed:
63+
print("No failed outputs found — nothing to clean up.")
64+
return 0
65+
66+
# Group by version directory for readable reporting.
67+
by_version: dict[Path, list[Path]] = {}
68+
for f in failed:
69+
by_version.setdefault(f.parent.parent, []).append(f)
70+
71+
total = 0
72+
for version_dir in sorted(by_version, key=lambda p: p.as_posix()):
73+
files = by_version[version_dir]
74+
rel = version_dir.relative_to(STATIC_DIR.parent)
75+
print(f"\n{rel} ({len(files)} failed)")
76+
for f in sorted(files):
77+
print(f" - {f.name}")
78+
if not args.dry_run:
79+
f.unlink()
80+
total += 1
81+
82+
action = "Would remove" if args.dry_run else "Removed"
83+
print(f"\n{action} {total} failed output file(s) across {len(by_version)} version(s).")
84+
if args.dry_run:
85+
print("Re-run without --dry-run to apply.")
86+
else:
87+
print("Next CI build (or `python scripts/build.py`) will regenerate them.")
88+
return 0
89+
90+
91+
if __name__ == "__main__":
92+
sys.exit(main())

0 commit comments

Comments
 (0)