from __future__ import annotations import argparse import uuid from collections.abc import Sequence from pathlib import Path from pypdf import PdfReader, PdfWriter from copienator import ( EvaluationWorkspace, ExitCode, execute, standard_parser, workspace_from_args, ) def copy_pdfs(directory: Path) -> list[Path]: directory = directory.expanduser().resolve() if not directory.is_dir(): raise NotADirectoryError(f"Dossier introuvable : {directory}") return sorted( ( path for path in directory.glob("*.pdf") if path.name.casefold() not in {"enonce.pdf", "énoncé.pdf"} ), key=lambda path: path.name.casefold(), ) def rotate_pdf(path: Path) -> None: temporary = path.with_name(f".{path.stem}.rotate-{uuid.uuid4().hex}.pdf") try: with path.open("rb") as source, temporary.open("wb") as destination: reader = PdfReader(source) writer = PdfWriter() for page in reader.pages: writer.add_page(page.rotate(180)) if reader.metadata: metadata = { str(key): str(value) for key, value in reader.metadata.items() if value is not None } writer.add_metadata(metadata) writer.write(destination) temporary.replace(path) finally: if temporary.exists(): temporary.unlink() def rotate_all(directory: Path) -> int: files = copy_pdfs(directory) for path in files: rotate_pdf(path) print(f"Rotated: {path.name}") if not files: print("No PDF copies found.") return len(files) def rename_all(directory: Path) -> list[tuple[Path, Path]]: directory = directory.expanduser().resolve() files = copy_pdfs(directory) width = max(2, len(str(len(files)))) plan = [ (source, directory / f"Copie{index:0{width}d}.pdf") for index, source in enumerate(files, start=1) ] staged: list[tuple[Path, Path, Path]] = [] try: for source, destination in plan: temporary = directory / f".copienator-rename-{uuid.uuid4().hex}.pdf" source.replace(temporary) staged.append((source, temporary, destination)) completed: list[tuple[Path, Path, Path]] = [] try: for source, temporary, destination in staged: temporary.replace(destination) completed.append((source, temporary, destination)) print(f"Renamed: {source.name} -> {destination.name}") except OSError: for _source, temporary, destination in completed: if destination.exists(): destination.replace(temporary) raise except OSError: for source, temporary, _destination in staged: if temporary.exists(): temporary.replace(source) raise if not plan: print("No PDF copies found.") return [(source, destination) for source, _temporary, destination in staged] def build_parser() -> argparse.ArgumentParser: parser = standard_parser("Prepare scanned PDF copies.") subparsers = parser.add_subparsers(dest="operation", required=True) for operation in ("rotate", "rename"): subparser = subparsers.add_parser(operation) subparser.add_argument("evaluation", type=Path, help="Evaluation directory") subparser.add_argument( "--verbose", action="store_true", default=argparse.SUPPRESS, help="Show a traceback when an unexpected error occurs", ) return parser def run(workspace: EvaluationWorkspace, *, operation: str) -> ExitCode: if operation == "rotate": rotate_all(workspace.root) else: rename_all(workspace.root) return ExitCode.SUCCESS def main(argv: Sequence[str] | None = None) -> int: parser = build_parser() return execute( parser, argv, lambda args: run(workspace_from_args(args), operation=args.operation), ) if __name__ == "__main__": raise SystemExit(main())