from __future__ import annotations import argparse import re import sys from collections.abc import Sequence from pathlib import Path from copienator import ( EvaluationWorkspace, ExitCode, evaluation_parser, execute, read_json, workspace_from_args, ) COPY_PATTERN = re.compile(r"Copie(\d+)") def build_parser() -> argparse.ArgumentParser: return evaluation_parser("Verify that every answer PDF appears in group metadata.") def collect_source_pdfs(copies_dir: Path) -> set[tuple[str, str]]: source_pdfs: set[tuple[str, str]] = set() for copy_dir in copies_dir.iterdir(): match = COPY_PATTERN.fullmatch(copy_dir.name) if not match or not copy_dir.is_dir(): continue copy_id = match.group(1) for pdf_path in copy_dir.glob("*.pdf"): source_pdfs.add((pdf_path.stem, copy_id)) return source_pdfs def collect_grouped_pdfs(groups_dir: Path) -> tuple[set[tuple[str, str]], int]: grouped: set[tuple[str, str]] = set() read_errors = 0 for json_path in groups_dir.glob("*/Group_*.json"): try: data = read_json(json_path) if not isinstance(data, list): raise TypeError("expected a JSON array") for entry in data: grouped.add((str(entry[4]), str(entry[0]))) except (IndexError, OSError, TypeError, ValueError) as exc: print(f"Error reading {json_path}: {exc}", file=sys.stderr) read_errors += 1 return grouped, read_errors def verify_groups(workspace: EvaluationWorkspace) -> ExitCode: workspace.require_directories("Copies", "Par label") source_pdfs = collect_source_pdfs(workspace.copies_dir) grouped_pdfs, read_errors = collect_grouped_pdfs(workspace.groups_dir) missing = source_pdfs - grouped_pdfs if missing: print(f"Verification failed: {len(missing)} files missing from groups:") for label, copy_id in sorted(missing): print(f"Copie{copy_id}/{label}.pdf") return ExitCode.FAILURE if read_errors: print("Verification incomplete because some metadata could not be read.") return ExitCode.PARTIAL print("Verification successful: all files accounted for.") return ExitCode.SUCCESS def run(workspace: EvaluationWorkspace) -> ExitCode: return verify_groups(workspace) def main(argv: Sequence[str] | None = None) -> int: parser = build_parser() return execute(parser, argv, lambda args: run(workspace_from_args(args))) if __name__ == "__main__": raise SystemExit(main())