from __future__ import annotations import argparse import re import sys from collections import defaultdict from collections.abc import Sequence from pathlib import Path from copienator import ( EvaluationWorkspace, ExitCode, evaluation_parser, execute, read_json, workspace_from_args, ) from copienator.platform import replace_with_link_or_copy, safe_filename ANNOTATION_CHOICES = ("BGnot", "Bnot", "Anot") def build_parser() -> argparse.ArgumentParser: parser = evaluation_parser("Assign student names and prepare the return directory.") parser.add_argument( "annotation_dir", choices=ANNOTATION_CHOICES, help="Annotation directory to use", ) return parser def _read_expected_names(workspace: EvaluationWorkspace) -> set[str]: names_path = workspace.names_file() if not names_path.exists(): print( f"Warning: names file not found in {workspace.root} or the current directory.", file=sys.stderr, ) return set() return { line.strip() for line in names_path.read_text(encoding="utf-8").splitlines() if line.strip() } def prepare_named_returns( workspace: EvaluationWorkspace, annotation_dir_name: str, ) -> ExitCode: workspace.require_directories("Copies", annotation_dir_name) workspace.return_dir.mkdir(parents=True, exist_ok=True) expected_names = _read_expected_names(workspace) copies_map: defaultdict[str, list[str]] = defaultdict(list) pattern = re.compile(r"^Copie(\d+)\.json$") had_errors = False for json_path in workspace.copies_dir.iterdir(): match = pattern.match(json_path.name) if not match: continue try: data = read_json(json_path) if not isinstance(data, dict): raise TypeError("expected a JSON object") name = str(data.get("name", "Unknown")).strip() copies_map[name].append(match.group(1)) except (OSError, TypeError, ValueError) as exc: print(f"Error processing {json_path}: {exc}", file=sys.stderr) had_errors = True assigned_names: set[str] = set() selected_annotations = workspace.root / annotation_dir_name fallback_annotations = workspace.annotation_dir("simple") for name, copy_ids in copies_map.items(): if name == "Unknown": print( f"Warning: unknown name for copies: {', '.join(copy_ids)}", file=sys.stderr, ) elif len(copy_ids) > 1: print( f"Warning: name {name!r} is assigned to multiple copies: " f"{', '.join(copy_ids)}", file=sys.stderr, ) safe_name = safe_filename(name) for copy_id in copy_ids: selected = selected_annotations / f"Copie{copy_id}" fallback = fallback_annotations / f"Copie{copy_id}" source_folder = None for candidate in (selected, fallback): if (candidate / "Concat.jpg").exists() and ( candidate / "score.json" ).exists(): source_folder = candidate break if source_folder is None: continue assigned_names.add(name) destination = workspace.return_dir / f"{safe_name} ({copy_id})" destination.mkdir(parents=True, exist_ok=True) links = ( ("Concat.jpg", f"{safe_name}.jpg"), ("Concat_F.pdf", f"{safe_name}.pdf"), ("score.json", "score.json"), ) for source_name, destination_name in links: source = source_folder / source_name if not source.exists(): continue try: method = replace_with_link_or_copy( source, destination / destination_name, prefer="symlink", ) if method == "copy": print( f"Copied {source_name} for {destination.name} " "(links unavailable)" ) except OSError as exc: print( f"Error linking {source} for {destination.name}: {exc}", file=sys.stderr, ) had_errors = True unassigned = expected_names - assigned_names if unassigned: print("Names from the list that were not assigned:", file=sys.stderr) for name in sorted(unassigned): print(f" - {name}", file=sys.stderr) return ExitCode.PARTIAL if had_errors else ExitCode.SUCCESS def run( workspace: EvaluationWorkspace, *, annotation_dir: str, ) -> ExitCode: return prepare_named_returns(workspace, annotation_dir) def main(argv: Sequence[str] | None = None) -> int: parser = build_parser() return execute( parser, argv, lambda args: run( workspace_from_args(args, repository=Path.cwd()), annotation_dir=args.annotation_dir, ), ) if __name__ == "__main__": raise SystemExit(main())