Standardisation des scripts
This commit is contained in:
+23
@@ -147,6 +147,29 @@ une modification concurrente de type lire-modifier-écrire, utiliser
|
|||||||
=atomic_update_json= : cet utilitaire protège l'opération complète avec
|
=atomic_update_json= : cet utilitaire protège l'opération complète avec
|
||||||
un verrou inter-processus Linux/Windows.
|
un verrou inter-processus Linux/Windows.
|
||||||
|
|
||||||
|
*** Convention des scripts standardisés
|
||||||
|
|
||||||
|
Les scripts standardisés exposent =build_parser()=, =run(...)= et
|
||||||
|
=main(argv=None)=. Leur import ne lance aucun traitement. Ils acceptent
|
||||||
|
le dossier d'évaluation comme premier argument positionnel, utilisent
|
||||||
|
=EvaluationWorkspace= pour les chemins partagés et peuvent afficher la
|
||||||
|
trace complète d'une erreur avec =--verbose=.
|
||||||
|
|
||||||
|
Les codes de sortie communs sont :
|
||||||
|
|
||||||
|
| Code | Signification |
|
||||||
|
|------+---------------|
|
||||||
|
| 0 | réussite |
|
||||||
|
| 1 | erreur de traitement |
|
||||||
|
| 2 | arguments invalides |
|
||||||
|
| 3 | évaluation ou prérequis invalides |
|
||||||
|
| 4 | traitement partiel, avec avertissements |
|
||||||
|
| 130 | interruption par l'utilisateur |
|
||||||
|
|
||||||
|
Le GUI distingue notamment un traitement partiel d'un échec. Les
|
||||||
|
premiers scripts migrés vers cette convention sont =export.py=,
|
||||||
|
=import.py=, =giving_names.py= et =grouping.py=.
|
||||||
|
|
||||||
** Correction d'un paquet de copies
|
** Correction d'un paquet de copies
|
||||||
|
|
||||||
1. Créer un fichier =names= dans le dossier courant, avec les
|
1. Créer un fichier =names= dans le dossier courant, avec les
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
"""Core building blocks shared by Copienator scripts and interfaces."""
|
"""Core building blocks shared by Copienator scripts and interfaces."""
|
||||||
|
|
||||||
|
from .cli import (
|
||||||
|
CliError,
|
||||||
|
ExitCode,
|
||||||
|
evaluation_parser,
|
||||||
|
evaluation_workspace,
|
||||||
|
execute,
|
||||||
|
workspace_from_args,
|
||||||
|
)
|
||||||
from .json_io import (
|
from .json_io import (
|
||||||
JsonLockTimeout,
|
JsonLockTimeout,
|
||||||
atomic_update_json,
|
atomic_update_json,
|
||||||
@@ -14,12 +22,18 @@ from .workspace import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
"CliError",
|
||||||
"EvaluationWorkspace",
|
"EvaluationWorkspace",
|
||||||
|
"ExitCode",
|
||||||
"JsonLockTimeout",
|
"JsonLockTimeout",
|
||||||
"WorkspaceNotFoundError",
|
"WorkspaceNotFoundError",
|
||||||
"WorkspaceValidationError",
|
"WorkspaceValidationError",
|
||||||
"atomic_update_json",
|
"atomic_update_json",
|
||||||
"atomic_write_json",
|
"atomic_write_json",
|
||||||
"atomic_write_text",
|
"atomic_write_text",
|
||||||
|
"evaluation_parser",
|
||||||
|
"evaluation_workspace",
|
||||||
|
"execute",
|
||||||
"read_json",
|
"read_json",
|
||||||
|
"workspace_from_args",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -0,0 +1,102 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import sys
|
||||||
|
import traceback
|
||||||
|
from collections.abc import Callable, Sequence
|
||||||
|
from enum import IntEnum
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from .workspace import (
|
||||||
|
EvaluationWorkspace,
|
||||||
|
WorkspaceNotFoundError,
|
||||||
|
WorkspaceValidationError,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ExitCode(IntEnum):
|
||||||
|
SUCCESS = 0
|
||||||
|
FAILURE = 1
|
||||||
|
INVALID_ARGUMENTS = 2
|
||||||
|
INVALID_WORKSPACE = 3
|
||||||
|
PARTIAL = 4
|
||||||
|
INTERRUPTED = 130
|
||||||
|
|
||||||
|
|
||||||
|
class CliError(Exception):
|
||||||
|
def __init__(self, message: str, exit_code: ExitCode = ExitCode.FAILURE) -> None:
|
||||||
|
self.exit_code = exit_code
|
||||||
|
super().__init__(message)
|
||||||
|
|
||||||
|
|
||||||
|
def evaluation_parser(description: str) -> argparse.ArgumentParser:
|
||||||
|
parser = argparse.ArgumentParser(description=description)
|
||||||
|
parser.add_argument(
|
||||||
|
"evaluation",
|
||||||
|
type=Path,
|
||||||
|
help="Evaluation directory",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--verbose",
|
||||||
|
action="store_true",
|
||||||
|
help="Show a traceback when an unexpected error occurs",
|
||||||
|
)
|
||||||
|
return parser
|
||||||
|
|
||||||
|
|
||||||
|
def evaluation_workspace(
|
||||||
|
path: str | Path,
|
||||||
|
*,
|
||||||
|
repository: str | Path | None = None,
|
||||||
|
) -> EvaluationWorkspace:
|
||||||
|
root = Path(path).expanduser().resolve()
|
||||||
|
if not root.exists():
|
||||||
|
raise CliError(
|
||||||
|
f"Evaluation directory does not exist: {root}",
|
||||||
|
ExitCode.INVALID_WORKSPACE,
|
||||||
|
)
|
||||||
|
if not root.is_dir():
|
||||||
|
raise CliError(
|
||||||
|
f"Evaluation path is not a directory: {root}",
|
||||||
|
ExitCode.INVALID_WORKSPACE,
|
||||||
|
)
|
||||||
|
return EvaluationWorkspace(root, Path(repository) if repository is not None else None)
|
||||||
|
|
||||||
|
|
||||||
|
def workspace_from_args(
|
||||||
|
args: argparse.Namespace,
|
||||||
|
*,
|
||||||
|
repository: str | Path | None = None,
|
||||||
|
) -> EvaluationWorkspace:
|
||||||
|
return evaluation_workspace(args.evaluation, repository=repository)
|
||||||
|
|
||||||
|
|
||||||
|
def execute(
|
||||||
|
parser: argparse.ArgumentParser,
|
||||||
|
argv: Sequence[str] | None,
|
||||||
|
handler: Callable[[argparse.Namespace], int | ExitCode | None],
|
||||||
|
) -> int:
|
||||||
|
args = parser.parse_args(argv)
|
||||||
|
try:
|
||||||
|
result = handler(args)
|
||||||
|
except CliError as exc:
|
||||||
|
print(f"Error: {exc}", file=sys.stderr)
|
||||||
|
return int(exc.exit_code)
|
||||||
|
except (WorkspaceNotFoundError, WorkspaceValidationError) as exc:
|
||||||
|
print(f"Error: {exc}", file=sys.stderr)
|
||||||
|
return int(ExitCode.INVALID_WORKSPACE)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("Interrupted by user.", file=sys.stderr)
|
||||||
|
return int(ExitCode.INTERRUPTED)
|
||||||
|
except Exception as exc: # noqa: BLE001 - executable boundary
|
||||||
|
if getattr(args, "verbose", False):
|
||||||
|
traceback.print_exc()
|
||||||
|
else:
|
||||||
|
print(f"Error: {exc}", file=sys.stderr)
|
||||||
|
return int(ExitCode.FAILURE)
|
||||||
|
return int(ExitCode.SUCCESS if result is None else result)
|
||||||
|
|
||||||
|
|
||||||
|
def stderr(message: Any) -> None:
|
||||||
|
print(message, file=sys.stderr)
|
||||||
@@ -186,6 +186,24 @@ class EvaluationWorkspace:
|
|||||||
if missing:
|
if missing:
|
||||||
raise WorkspaceValidationError(self.root, missing)
|
raise WorkspaceValidationError(self.root, missing)
|
||||||
|
|
||||||
|
def require_files(self, *relative_paths: str) -> None:
|
||||||
|
missing = [
|
||||||
|
relative_path
|
||||||
|
for relative_path in relative_paths
|
||||||
|
if not (self.root / relative_path).is_file()
|
||||||
|
]
|
||||||
|
if missing:
|
||||||
|
raise WorkspaceValidationError(self.root, missing)
|
||||||
|
|
||||||
|
def require_directories(self, *relative_paths: str) -> None:
|
||||||
|
missing = [
|
||||||
|
relative_path
|
||||||
|
for relative_path in relative_paths
|
||||||
|
if not (self.root / relative_path).is_dir()
|
||||||
|
]
|
||||||
|
if missing:
|
||||||
|
raise WorkspaceValidationError(self.root, missing)
|
||||||
|
|
||||||
def ensure_control_directories(self) -> None:
|
def ensure_control_directories(self) -> None:
|
||||||
self.logs_dir.mkdir(parents=True, exist_ok=True)
|
self.logs_dir.mkdir(parents=True, exist_ok=True)
|
||||||
self.runs_dir.mkdir(parents=True, exist_ok=True)
|
self.runs_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|||||||
+13
-4
@@ -7,6 +7,7 @@ from pathlib import Path
|
|||||||
from tkinter import filedialog, messagebox, ttk
|
from tkinter import filedialog, messagebox, ttk
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
from copienator import ExitCode
|
||||||
from platform_utils import WindowsLabelError, validate_windows_labels
|
from platform_utils import WindowsLabelError, validate_windows_labels
|
||||||
|
|
||||||
from .diagnostics import collect_diagnostics
|
from .diagnostics import collect_diagnostics
|
||||||
@@ -26,6 +27,7 @@ STATUS_LABELS = {
|
|||||||
"ready": "Prête",
|
"ready": "Prête",
|
||||||
"running": "En cours",
|
"running": "En cours",
|
||||||
"success": "Réussie",
|
"success": "Réussie",
|
||||||
|
"partial": "Partielle",
|
||||||
"failed": "Échouée",
|
"failed": "Échouée",
|
||||||
"interrupted": "Interrompue",
|
"interrupted": "Interrompue",
|
||||||
"skipped": "Ignorée",
|
"skipped": "Ignorée",
|
||||||
@@ -34,6 +36,16 @@ STATUS_LABELS = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def process_status(return_code: int, interrupted: bool = False) -> str:
|
||||||
|
if interrupted or return_code == ExitCode.INTERRUPTED:
|
||||||
|
return "interrupted"
|
||||||
|
if return_code == ExitCode.SUCCESS:
|
||||||
|
return "success"
|
||||||
|
if return_code == ExitCode.PARTIAL:
|
||||||
|
return "partial"
|
||||||
|
return "failed"
|
||||||
|
|
||||||
|
|
||||||
class CopienatorApp(tk.Tk):
|
class CopienatorApp(tk.Tk):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@@ -624,10 +636,7 @@ class CopienatorApp(tk.Tk):
|
|||||||
step_id = self.active_step_id
|
step_id = self.active_step_id
|
||||||
if not step_id:
|
if not step_id:
|
||||||
return
|
return
|
||||||
if interrupted:
|
status = process_status(return_code, interrupted)
|
||||||
status = "interrupted"
|
|
||||||
else:
|
|
||||||
status = "success" if return_code == 0 else "failed"
|
|
||||||
self.state_store.update_step(step_id, status=status, return_code=return_code)
|
self.state_store.update_step(step_id, status=status, return_code=return_code)
|
||||||
self.state_store.add_history(
|
self.state_store.add_history(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,44 +1,65 @@
|
|||||||
import argparse
|
import argparse
|
||||||
import sys
|
import sys
|
||||||
|
from collections.abc import Sequence
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from config import EXPORT_DIR
|
from config import EXPORT_DIR
|
||||||
|
from copienator import (
|
||||||
|
EvaluationWorkspace,
|
||||||
|
ExitCode,
|
||||||
|
evaluation_parser,
|
||||||
|
execute,
|
||||||
|
workspace_from_args,
|
||||||
|
)
|
||||||
from platform_utils import replace_with_link_or_copy
|
from platform_utils import replace_with_link_or_copy
|
||||||
|
|
||||||
|
|
||||||
def export_directory(base_dir, source_dir_name):
|
def export_directory(
|
||||||
base_dir = Path(base_dir).expanduser().resolve()
|
workspace: EvaluationWorkspace,
|
||||||
source_dir = base_dir / source_dir_name
|
source_dir_name: str,
|
||||||
sync_dir = Path(EXPORT_DIR).expanduser() / base_dir.name
|
) -> ExitCode:
|
||||||
|
workspace.require_directories(source_dir_name)
|
||||||
|
source_dir = workspace.root / source_dir_name
|
||||||
|
sync_dir = Path(EXPORT_DIR).expanduser() / workspace.name
|
||||||
sync_dir.mkdir(parents=True, exist_ok=True)
|
sync_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
if not source_dir.is_dir():
|
|
||||||
print(f"Erreur : le sous-dossier {source_dir} n'existe pas.")
|
|
||||||
return
|
|
||||||
|
|
||||||
subdirs = [directory for directory in source_dir.iterdir() if directory.is_dir()]
|
subdirs = [directory for directory in source_dir.iterdir() if directory.is_dir()]
|
||||||
if source_dir_name == "BGnot" and subdirs:
|
if source_dir_name == "BGnot" and subdirs:
|
||||||
all_start_with_copie = all(directory.name.startswith("Copie") for directory in subdirs)
|
all_start_with_copie = all(directory.name.startswith("Copie") for directory in subdirs)
|
||||||
if not all_start_with_copie:
|
if not all_start_with_copie:
|
||||||
subdirs = [directory for directory in subdirs if not directory.name.startswith("Copie")]
|
subdirs = [directory for directory in subdirs if not directory.name.startswith("Copie")]
|
||||||
|
|
||||||
|
missing_outputs = 0
|
||||||
for subdir in subdirs:
|
for subdir in subdirs:
|
||||||
concat_file = subdir / "Concat.pdf"
|
concat_file = subdir / "Concat.pdf"
|
||||||
if not concat_file.is_file():
|
if not concat_file.is_file():
|
||||||
print(f"Attention : le fichier {concat_file} est introuvable.")
|
print(f"Warning: file not found: {concat_file}", file=sys.stderr)
|
||||||
|
missing_outputs += 1
|
||||||
continue
|
continue
|
||||||
destination = sync_dir / f"{subdir.name}.pdf"
|
destination = sync_dir / f"{subdir.name}.pdf"
|
||||||
method = replace_with_link_or_copy(concat_file, destination, prefer="hardlink")
|
method = replace_with_link_or_copy(concat_file, destination, prefer="hardlink")
|
||||||
print(f"Exported: {destination} ({method})")
|
print(f"Exported: {destination} ({method})")
|
||||||
|
return ExitCode.PARTIAL if missing_outputs else ExitCode.SUCCESS
|
||||||
|
|
||||||
|
|
||||||
|
def build_parser() -> argparse.ArgumentParser:
|
||||||
|
parser = evaluation_parser("Export annotated PDFs to the tablet directory.")
|
||||||
|
parser.add_argument("--refaire", action="store_true", help="Process only copies/labels defined in refaire.json")
|
||||||
|
return parser
|
||||||
|
|
||||||
|
|
||||||
|
def run(workspace: EvaluationWorkspace, *, refaire: bool = False) -> ExitCode:
|
||||||
|
return export_directory(workspace, "BRnot" if refaire else "BGnot")
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: Sequence[str] | None = None) -> int:
|
||||||
|
parser = build_parser()
|
||||||
|
return execute(
|
||||||
|
parser,
|
||||||
|
argv,
|
||||||
|
lambda args: run(workspace_from_args(args), refaire=args.refaire),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
parser = argparse.ArgumentParser(description="Move to tablette folder.")
|
raise SystemExit(main())
|
||||||
parser.add_argument("dir", help="The directory to process")
|
|
||||||
parser.add_argument("--refaire", action="store_true", help="Process only copies/labels defined in refaire.json")
|
|
||||||
|
|
||||||
args = parser.parse_args()
|
|
||||||
if args.refaire:
|
|
||||||
export_directory(args.dir, "BRnot")
|
|
||||||
sys.exit(0)
|
|
||||||
export_directory(args.dir, "BGnot")
|
|
||||||
|
|||||||
+136
-73
@@ -1,102 +1,165 @@
|
|||||||
import json
|
from __future__ import annotations
|
||||||
import os
|
|
||||||
|
import argparse
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
from collections.abc import Sequence
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
from copienator import (
|
||||||
|
EvaluationWorkspace,
|
||||||
|
ExitCode,
|
||||||
|
evaluation_parser,
|
||||||
|
execute,
|
||||||
|
read_json,
|
||||||
|
workspace_from_args,
|
||||||
|
)
|
||||||
from platform_utils import replace_with_link_or_copy, safe_filename
|
from platform_utils import replace_with_link_or_copy, safe_filename
|
||||||
|
|
||||||
def main():
|
ANNOTATION_CHOICES = ("BGnot", "Bnot", "Anot")
|
||||||
if len(sys.argv) < 3:
|
|
||||||
print("Usage: python giving_names.py <directory_path> <Bnot/BGnot>")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
work_dir = os.path.abspath(sys.argv[1])
|
|
||||||
copies_dir = Path(work_dir) / "Copies"
|
|
||||||
bnot_dir = sys.argv[2]
|
|
||||||
target_subdir = os.path.join(work_dir, "A Rendre")
|
|
||||||
os.makedirs(target_subdir, exist_ok=True)
|
|
||||||
|
|
||||||
# --- 1. Load the expected names list ---
|
def build_parser() -> argparse.ArgumentParser:
|
||||||
expected_names = set()
|
parser = evaluation_parser("Assign student names and prepare the return directory.")
|
||||||
names_path = os.path.join(work_dir, "names")
|
parser.add_argument(
|
||||||
if not os.path.exists(names_path):
|
"annotation_dir",
|
||||||
names_path = "names" # Fallback to current dir
|
choices=ANNOTATION_CHOICES,
|
||||||
|
help="Annotation directory to use",
|
||||||
|
)
|
||||||
|
return parser
|
||||||
|
|
||||||
if os.path.exists(names_path):
|
|
||||||
with open(names_path, 'r', encoding='utf-8') as f:
|
|
||||||
expected_names = {line.strip() for line in f if line.strip()}
|
|
||||||
else:
|
|
||||||
print(f"Warning: 'names' file not found in {work_dir} or current directory.")
|
|
||||||
|
|
||||||
# --- 2. Existing Collection Logic ---
|
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$")
|
pattern = re.compile(r"^Copie(\d+)\.json$")
|
||||||
copies_map = defaultdict(list)
|
had_errors = False
|
||||||
assigned_names = set() # To track which names were successfully linked
|
|
||||||
|
|
||||||
for filename in os.listdir(copies_dir):
|
for json_path in workspace.copies_dir.iterdir():
|
||||||
match = pattern.match(filename)
|
match = pattern.match(json_path.name)
|
||||||
if match:
|
if not match:
|
||||||
copie_id = match.group(1)
|
continue
|
||||||
json_path = os.path.join(copies_dir, filename)
|
|
||||||
try:
|
try:
|
||||||
with open(json_path, 'r', encoding='utf-8') as f:
|
data = read_json(json_path)
|
||||||
data = json.load(f)
|
if not isinstance(data, dict):
|
||||||
name = data.get("name", "Unknown").strip()
|
raise TypeError("expected a JSON object")
|
||||||
copies_map[name].append(copie_id)
|
name = str(data.get("name", "Unknown")).strip()
|
||||||
except Exception as e:
|
copies_map[name].append(match.group(1))
|
||||||
print(f"Error processing {filename}: {e}")
|
except (OSError, TypeError, ValueError) as exc:
|
||||||
|
print(f"Error processing {json_path}: {exc}", file=sys.stderr)
|
||||||
|
had_errors = True
|
||||||
|
|
||||||
# --- 3. Process and Link ---
|
assigned_names: set[str] = set()
|
||||||
for name, ids in copies_map.items():
|
selected_annotations = workspace.root / annotation_dir_name
|
||||||
|
fallback_annotations = workspace.annotation_dir("simple")
|
||||||
|
|
||||||
|
for name, copy_ids in copies_map.items():
|
||||||
if name == "Unknown":
|
if name == "Unknown":
|
||||||
print(f"ALERT: 'Unknown' name found for IDs: {', '.join(ids)}")
|
print(
|
||||||
elif len(ids) > 1:
|
f"Warning: unknown name for copies: {', '.join(copy_ids)}",
|
||||||
print(f"ALERT: Name '{name}' assigned to multiple IDs: {', '.join(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)
|
safe_name = safe_filename(name)
|
||||||
|
for copy_id in copy_ids:
|
||||||
for copie_id in ids:
|
selected = selected_annotations / f"Copie{copy_id}"
|
||||||
path_b = os.path.join(work_dir, f"{bnot_dir}/Copie{copie_id}")
|
fallback = fallback_annotations / f"Copie{copy_id}"
|
||||||
path_a = os.path.join(work_dir, f"Anot/Copie{copie_id}")
|
|
||||||
|
|
||||||
source_folder = None
|
source_folder = None
|
||||||
if os.path.exists(os.path.join(path_b, "Concat.jpg")) and os.path.exists(os.path.join(path_b, "score.json")):
|
for candidate in (selected, fallback):
|
||||||
source_folder = path_b
|
if (candidate / "Concat.jpg").exists() and (
|
||||||
elif os.path.exists(os.path.join(path_a, "Concat.jpg")) and os.path.exists(os.path.join(path_a, "score.json")):
|
candidate / "score.json"
|
||||||
source_folder = path_a
|
).exists():
|
||||||
|
source_folder = candidate
|
||||||
if not source_folder:
|
break
|
||||||
|
if source_folder is None:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# If we reached here, the link is possible
|
|
||||||
assigned_names.add(name)
|
assigned_names.add(name)
|
||||||
|
destination = workspace.return_dir / f"{safe_name} ({copy_id})"
|
||||||
dest_folder_name = f"{safe_name} ({copie_id})"
|
destination.mkdir(parents=True, exist_ok=True)
|
||||||
dest_path = os.path.join(target_subdir, dest_folder_name)
|
links = (
|
||||||
os.makedirs(dest_path, exist_ok=True)
|
("Concat.jpg", f"{safe_name}.jpg"),
|
||||||
|
("Concat_F.pdf", f"{safe_name}.pdf"),
|
||||||
links = [("Concat.jpg", f"{safe_name}.jpg"),("Concat_F.pdf", f"{safe_name}.pdf"), ("score.json", "score.json")]
|
("score.json", "score.json"),
|
||||||
for src_name, dst_name in links:
|
)
|
||||||
src_file = os.path.join(source_folder, src_name)
|
for source_name, destination_name in links:
|
||||||
dst_link = os.path.join(dest_path, dst_name)
|
source = source_folder / source_name
|
||||||
|
if not source.exists():
|
||||||
|
continue
|
||||||
try:
|
try:
|
||||||
if os.path.exists(src_file):
|
method = replace_with_link_or_copy(
|
||||||
method = replace_with_link_or_copy(src_file, dst_link, prefer="symlink")
|
source,
|
||||||
|
destination / destination_name,
|
||||||
|
prefer="symlink",
|
||||||
|
)
|
||||||
if method == "copy":
|
if method == "copy":
|
||||||
print(f"Copied {src_name} for {dest_folder_name} (links unavailable)")
|
print(
|
||||||
except OSError as e:
|
f"Copied {source_name} for {destination.name} "
|
||||||
print(f"Error linking {src_name} for {dest_folder_name}: {e}")
|
"(links unavailable)"
|
||||||
|
)
|
||||||
|
except OSError as exc:
|
||||||
|
print(
|
||||||
|
f"Error linking {source} for {destination.name}: {exc}",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
had_errors = True
|
||||||
|
|
||||||
# --- 4. Print Unassigned Names ---
|
|
||||||
unassigned = expected_names - assigned_names
|
unassigned = expected_names - assigned_names
|
||||||
if unassigned:
|
if unassigned:
|
||||||
print("\n" + "!" * 40)
|
print("Names from the list that were not assigned:", file=sys.stderr)
|
||||||
print("NAMES FROM LIST NOT ASSIGNED:")
|
for name in sorted(unassigned):
|
||||||
for n in sorted(unassigned):
|
print(f" - {name}", file=sys.stderr)
|
||||||
print(f" - {n}")
|
|
||||||
print("!" * 40)
|
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__":
|
if __name__ == "__main__":
|
||||||
main()
|
raise SystemExit(main())
|
||||||
|
|||||||
+42
-22
@@ -1,13 +1,22 @@
|
|||||||
|
import argparse
|
||||||
import os
|
import os
|
||||||
import json
|
|
||||||
import re
|
import re
|
||||||
import sys
|
|
||||||
import shutil
|
import shutil
|
||||||
from pathlib import Path
|
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
from collections.abc import Sequence
|
||||||
from concurrent.futures import ThreadPoolExecutor
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
from PIL import Image, ImageDraw, ImageFont
|
|
||||||
from pdf2image import convert_from_path, pdfinfo_from_path
|
from pdf2image import convert_from_path, pdfinfo_from_path
|
||||||
|
from PIL import Image, ImageDraw, ImageFont
|
||||||
|
|
||||||
|
from copienator import (
|
||||||
|
EvaluationWorkspace,
|
||||||
|
ExitCode,
|
||||||
|
atomic_write_json,
|
||||||
|
evaluation_parser,
|
||||||
|
execute,
|
||||||
|
workspace_from_args,
|
||||||
|
)
|
||||||
|
|
||||||
# Configuration
|
# Configuration
|
||||||
DPI = 200 # Good balance for readability and size
|
DPI = 200 # Good balance for readability and size
|
||||||
@@ -34,7 +43,7 @@ def get_pdf_height(path):
|
|||||||
|
|
||||||
# Return total height
|
# Return total height
|
||||||
return single_page_px * num_pages
|
return single_page_px * num_pages
|
||||||
except Exception as e:
|
except Exception as e: # noqa: BLE001 - pdfinfo may raise backend-specific errors
|
||||||
print(f"Error reading {path}: {e}")
|
print(f"Error reading {path}: {e}")
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
@@ -78,7 +87,7 @@ def group_files(file_list):
|
|||||||
groups = []
|
groups = []
|
||||||
|
|
||||||
for item in sorted_files:
|
for item in sorted_files:
|
||||||
dd, path, height = item
|
_, _, height = item
|
||||||
placed = False
|
placed = False
|
||||||
|
|
||||||
# 2. Try to fit item into an existing group (First Fit)
|
# 2. Try to fit item into an existing group (First Fit)
|
||||||
@@ -140,7 +149,7 @@ def create_jpg(identifier, group_index, group, root_dir):
|
|||||||
combined_img = stitch_pdf_pages(imgs)
|
combined_img = stitch_pdf_pages(imgs)
|
||||||
if combined_img:
|
if combined_img:
|
||||||
images.append((dd, combined_img))
|
images.append((dd, combined_img))
|
||||||
except Exception as e:
|
except Exception as e: # noqa: BLE001 - PDF/image backends vary by platform
|
||||||
print(f"Failed to convert {path}: {e}")
|
print(f"Failed to convert {path}: {e}")
|
||||||
|
|
||||||
if not images:
|
if not images:
|
||||||
@@ -159,7 +168,7 @@ def create_jpg(identifier, group_index, group, root_dir):
|
|||||||
# Try loading a font, fallback to default
|
# Try loading a font, fallback to default
|
||||||
try:
|
try:
|
||||||
font = ImageFont.truetype("DejaVuSans.ttf", 40)
|
font = ImageFont.truetype("DejaVuSans.ttf", 40)
|
||||||
except IOError:
|
except OSError:
|
||||||
print("font not found")
|
print("font not found")
|
||||||
font = ImageFont.load_default()
|
font = ImageFont.load_default()
|
||||||
|
|
||||||
@@ -193,8 +202,7 @@ def create_jpg(identifier, group_index, group, root_dir):
|
|||||||
# Save JSON metadata
|
# Save JSON metadata
|
||||||
json_filename = f"Group_{group_index+1}.json"
|
json_filename = f"Group_{group_index+1}.json"
|
||||||
json_path = os.path.join(target_folder, json_filename)
|
json_path = os.path.join(target_folder, json_filename)
|
||||||
with open(json_path, 'w') as f:
|
atomic_write_json(json_path, metadata, indent=None)
|
||||||
json.dump(metadata, f)
|
|
||||||
|
|
||||||
# Save with size constraints
|
# Save with size constraints
|
||||||
output_filename = f"Group_{group_index+1}.jpg"
|
output_filename = f"Group_{group_index+1}.jpg"
|
||||||
@@ -227,18 +235,16 @@ def process_identifier(identifier, files_info, output_dir):
|
|||||||
for idx, group in enumerate(file_groups):
|
for idx, group in enumerate(file_groups):
|
||||||
create_jpg(identifier, idx, group, output_dir)
|
create_jpg(identifier, idx, group, output_dir)
|
||||||
|
|
||||||
def main():
|
def build_parser() -> argparse.ArgumentParser:
|
||||||
if len(sys.argv) < 2:
|
return evaluation_parser("Group copy extracts by question label.")
|
||||||
print("Usage: python app.py <Path_to_Dir>")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
root_dir = Path(sys.argv[1])
|
|
||||||
|
|
||||||
copies_dir = root_dir / "Copies"
|
def run(workspace: EvaluationWorkspace) -> ExitCode:
|
||||||
par_label_dir = root_dir / "Par label"
|
workspace.require_directories("Copies")
|
||||||
|
workspace.groups_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
print("Scanning files...")
|
print("Scanning files...")
|
||||||
data = collect_files(copies_dir)
|
data = collect_files(workspace.copies_dir)
|
||||||
|
|
||||||
print(f"Found {len(data)} identifiers. Processing...")
|
print(f"Found {len(data)} identifiers. Processing...")
|
||||||
|
|
||||||
@@ -247,11 +253,25 @@ def main():
|
|||||||
|
|
||||||
# Process using 8 threads
|
# Process using 8 threads
|
||||||
with ThreadPoolExecutor(max_workers=8) as executor:
|
with ThreadPoolExecutor(max_workers=8) as executor:
|
||||||
for identifier in sorted_identifiers:
|
futures = [
|
||||||
executor.submit(process_identifier, identifier, data[identifier],
|
executor.submit(
|
||||||
par_label_dir)
|
process_identifier,
|
||||||
|
identifier,
|
||||||
|
data[identifier],
|
||||||
|
workspace.groups_dir,
|
||||||
|
)
|
||||||
|
for identifier in sorted_identifiers
|
||||||
|
]
|
||||||
|
for future in futures:
|
||||||
|
future.result()
|
||||||
|
|
||||||
print("Done.")
|
print("Done.")
|
||||||
|
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)))
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
raise SystemExit(main())
|
||||||
|
|||||||
@@ -1,43 +1,65 @@
|
|||||||
import sys
|
import argparse
|
||||||
import shutil
|
import shutil
|
||||||
|
import sys
|
||||||
|
from collections.abc import Sequence
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from config import IMPORT_DIR
|
from config import IMPORT_DIR
|
||||||
|
from copienator import (
|
||||||
|
EvaluationWorkspace,
|
||||||
|
ExitCode,
|
||||||
|
evaluation_parser,
|
||||||
|
execute,
|
||||||
|
workspace_from_args,
|
||||||
|
)
|
||||||
|
|
||||||
def sync_annotated(dir_arg, refaire):
|
|
||||||
if not refaire:
|
|
||||||
bgnot_dir = Path(dir_arg) / "BGnot"
|
|
||||||
else:
|
|
||||||
bgnot_dir = Path(dir_arg) / "BRnot"
|
|
||||||
|
|
||||||
annotated_dir = IMPORT_DIR
|
|
||||||
|
|
||||||
|
def sync_annotated(
|
||||||
|
workspace: EvaluationWorkspace,
|
||||||
|
*,
|
||||||
|
refaire: bool = False,
|
||||||
|
import_dir: Path,
|
||||||
|
) -> ExitCode:
|
||||||
|
annotation_name = "BRnot" if refaire else "BGnot"
|
||||||
|
workspace.require_directories(annotation_name)
|
||||||
|
annotation_dir = workspace.root / annotation_name
|
||||||
|
annotated_dir = Path(import_dir).expanduser()
|
||||||
if not annotated_dir.is_dir():
|
if not annotated_dir.is_dir():
|
||||||
print(f"Error: Directory {annotated_dir} does not exist.")
|
print(f"Error: directory does not exist: {annotated_dir}", file=sys.stderr)
|
||||||
return
|
return ExitCode.INVALID_WORKSPACE
|
||||||
|
|
||||||
# Iterate over all PDF files in the annotated directory
|
missing_targets = 0
|
||||||
for pdf_file in annotated_dir.glob("*.pdf"):
|
for pdf_file in annotated_dir.glob("*.pdf"):
|
||||||
subdir_name = pdf_file.stem # 'f' from 'f.pdf'
|
target_subdir = annotation_dir / pdf_file.stem
|
||||||
target_subdir = bgnot_dir / subdir_name
|
|
||||||
|
|
||||||
if not target_subdir.is_dir():
|
if not target_subdir.is_dir():
|
||||||
print(f"Warning: Directory {target_subdir} not found.")
|
print(f"Warning: directory not found: {target_subdir}", file=sys.stderr)
|
||||||
|
missing_targets += 1
|
||||||
else:
|
else:
|
||||||
dest_file = target_subdir / "Concat_annotated.pdf"
|
dest_file = target_subdir / "Concat_annotated.pdf"
|
||||||
print("copying ", pdf_file, " to ", dest_file)
|
print(f"Copying {pdf_file} to {dest_file}")
|
||||||
shutil.copy2(pdf_file, dest_file)
|
shutil.copy2(pdf_file, dest_file)
|
||||||
|
return ExitCode.PARTIAL if missing_targets else ExitCode.SUCCESS
|
||||||
|
|
||||||
import argparse
|
|
||||||
|
def build_parser() -> argparse.ArgumentParser:
|
||||||
|
parser = evaluation_parser("Import handwritten annotations from the tablet directory.")
|
||||||
|
parser.add_argument("--refaire", action="store_true", help="Process only copies/labels defined in refaire.json")
|
||||||
|
return parser
|
||||||
|
|
||||||
|
|
||||||
|
def run(workspace: EvaluationWorkspace, *, refaire: bool = False) -> ExitCode:
|
||||||
|
return sync_annotated(workspace, refaire=refaire, import_dir=Path(IMPORT_DIR))
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: Sequence[str] | None = None) -> int:
|
||||||
|
parser = build_parser()
|
||||||
|
return execute(
|
||||||
|
parser,
|
||||||
|
argv,
|
||||||
|
lambda args: run(workspace_from_args(args), refaire=args.refaire),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
parser = argparse.ArgumentParser(description="Move to tablette folder.")
|
|
||||||
parser.add_argument("dir", help="The directory to process")
|
|
||||||
parser.add_argument("--refaire", action="store_true", help="Process only copies/labels defined in refaire.json")
|
|
||||||
|
|
||||||
args = parser.parse_args()
|
|
||||||
root_dir = args.dir
|
|
||||||
|
|
||||||
sync_annotated(root_dir, args.refaire)
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import importlib.util
|
||||||
|
import io
|
||||||
import os
|
import os
|
||||||
import queue
|
import queue
|
||||||
import sys
|
import sys
|
||||||
@@ -7,6 +9,7 @@ import tempfile
|
|||||||
import time
|
import time
|
||||||
import unittest
|
import unittest
|
||||||
from concurrent.futures import ThreadPoolExecutor
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
|
from contextlib import redirect_stderr
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
@@ -20,6 +23,7 @@ from copienator import (
|
|||||||
atomic_write_json,
|
atomic_write_json,
|
||||||
read_json,
|
read_json,
|
||||||
)
|
)
|
||||||
|
from copienator_gui.app import process_status
|
||||||
from copienator_gui.diagnostics import collect_diagnostics
|
from copienator_gui.diagnostics import collect_diagnostics
|
||||||
from copienator_gui.runner import ProcessRunner
|
from copienator_gui.runner import ProcessRunner
|
||||||
from copienator_gui.state import StateStore
|
from copienator_gui.state import StateStore
|
||||||
@@ -36,6 +40,15 @@ from platform_utils import (
|
|||||||
REPOSITORY = Path(__file__).resolve().parents[1]
|
REPOSITORY = Path(__file__).resolve().parents[1]
|
||||||
|
|
||||||
|
|
||||||
|
def load_script_module(filename: str, module_name: str):
|
||||||
|
spec = importlib.util.spec_from_file_location(module_name, REPOSITORY / filename)
|
||||||
|
if spec is None or spec.loader is None:
|
||||||
|
raise RuntimeError(f"Could not load {filename}")
|
||||||
|
module = importlib.util.module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(module)
|
||||||
|
return module
|
||||||
|
|
||||||
|
|
||||||
class WorkspaceTests(unittest.TestCase):
|
class WorkspaceTests(unittest.TestCase):
|
||||||
def test_canonical_paths_and_no_constructor_side_effects(self) -> None:
|
def test_canonical_paths_and_no_constructor_side_effects(self) -> None:
|
||||||
with tempfile.TemporaryDirectory() as directory:
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
@@ -73,6 +86,10 @@ class WorkspaceTests(unittest.TestCase):
|
|||||||
workspace.require("enonce.pdf", "labels")
|
workspace.require("enonce.pdf", "labels")
|
||||||
self.assertEqual(context.exception.missing, ["enonce.pdf", "labels"])
|
self.assertEqual(context.exception.missing, ["enonce.pdf", "labels"])
|
||||||
|
|
||||||
|
(root / "Copies").write_text("not a directory", encoding="utf-8")
|
||||||
|
with self.assertRaises(WorkspaceValidationError):
|
||||||
|
workspace.require_directories("Copies")
|
||||||
|
|
||||||
def test_names_file_prefers_evaluation_then_repository(self) -> None:
|
def test_names_file_prefers_evaluation_then_repository(self) -> None:
|
||||||
with tempfile.TemporaryDirectory() as directory:
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
repository = Path(directory)
|
repository = Path(directory)
|
||||||
@@ -134,6 +151,143 @@ class AtomicJsonTests(unittest.TestCase):
|
|||||||
self.assertEqual(persisted["steps"]["labels"]["status"], "success")
|
self.assertEqual(persisted["steps"]["labels"]["status"], "success")
|
||||||
|
|
||||||
|
|
||||||
|
class StandardCliTests(unittest.TestCase):
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls) -> None:
|
||||||
|
cls.modules = {
|
||||||
|
"export": load_script_module("export.py", "copienator_export_test"),
|
||||||
|
"import": load_script_module("import.py", "copienator_import_test"),
|
||||||
|
"giving_names": load_script_module(
|
||||||
|
"giving_names.py", "copienator_giving_names_test"
|
||||||
|
),
|
||||||
|
"grouping": load_script_module("grouping.py", "copienator_grouping_test"),
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_missing_evaluation_has_standard_exit_code(self) -> None:
|
||||||
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
|
missing = str(Path(directory) / "missing")
|
||||||
|
invocations = {
|
||||||
|
"export": [missing],
|
||||||
|
"import": [missing],
|
||||||
|
"giving_names": [missing, "BGnot"],
|
||||||
|
"grouping": [missing],
|
||||||
|
}
|
||||||
|
for name, arguments in invocations.items():
|
||||||
|
with self.subTest(script=name), redirect_stderr(io.StringIO()):
|
||||||
|
self.assertEqual(self.modules[name].main(arguments), 3)
|
||||||
|
|
||||||
|
def test_invalid_arguments_use_argparse_exit_code(self) -> None:
|
||||||
|
with redirect_stderr(io.StringIO()), self.assertRaises(SystemExit) as context:
|
||||||
|
self.modules["grouping"].main([])
|
||||||
|
self.assertEqual(context.exception.code, 2)
|
||||||
|
|
||||||
|
def test_unexpected_processing_error_has_failure_exit_code(self) -> None:
|
||||||
|
module = self.modules["grouping"]
|
||||||
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
|
with patch.object(
|
||||||
|
module, "run", side_effect=RuntimeError("broken")
|
||||||
|
), redirect_stderr(io.StringIO()) as errors:
|
||||||
|
self.assertEqual(module.main([directory]), 1)
|
||||||
|
self.assertIn("broken", errors.getvalue())
|
||||||
|
|
||||||
|
def test_prerequisites_are_checked_before_creating_outputs(self) -> None:
|
||||||
|
module = self.modules["export"]
|
||||||
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
|
base = Path(directory)
|
||||||
|
evaluation = base / "Exam"
|
||||||
|
evaluation.mkdir()
|
||||||
|
export_dir = base / "Export"
|
||||||
|
with patch.object(module, "EXPORT_DIR", export_dir), redirect_stderr(
|
||||||
|
io.StringIO()
|
||||||
|
):
|
||||||
|
self.assertEqual(module.main([str(evaluation)]), 3)
|
||||||
|
self.assertFalse(export_dir.exists())
|
||||||
|
|
||||||
|
def test_gui_understands_standard_process_exit_codes(self) -> None:
|
||||||
|
self.assertEqual(process_status(0), "success")
|
||||||
|
self.assertEqual(process_status(1), "failed")
|
||||||
|
self.assertEqual(process_status(3), "failed")
|
||||||
|
self.assertEqual(process_status(4), "partial")
|
||||||
|
self.assertEqual(process_status(130), "interrupted")
|
||||||
|
self.assertEqual(process_status(0, interrupted=True), "interrupted")
|
||||||
|
|
||||||
|
def test_gui_commands_are_accepted_by_script_parsers(self) -> None:
|
||||||
|
steps = {step.id: step for step in build_workflow(True)}
|
||||||
|
evaluation = "Evaluation with spaces"
|
||||||
|
cases = {
|
||||||
|
"export": ("export", "default", {"target": evaluation, "refaire": True}),
|
||||||
|
"import": ("import", "default", {"target": evaluation, "refaire": True}),
|
||||||
|
"giving_names": (
|
||||||
|
"giving_names",
|
||||||
|
"default",
|
||||||
|
{"target": evaluation, "annotation_dir": "BGnot"},
|
||||||
|
),
|
||||||
|
"grouping": ("grouping", "default", {"target": evaluation}),
|
||||||
|
}
|
||||||
|
for module_name, (step_id, variant_id, values) in cases.items():
|
||||||
|
step = steps[step_id]
|
||||||
|
variant = next(item for item in step.variants if item.id == variant_id)
|
||||||
|
command = build_command(REPOSITORY, step, variant, values, evaluation)
|
||||||
|
with self.subTest(script=module_name):
|
||||||
|
parsed = self.modules[module_name].build_parser().parse_args(command[3:])
|
||||||
|
self.assertEqual(str(parsed.evaluation), evaluation)
|
||||||
|
|
||||||
|
def test_export_main_copies_outputs(self) -> None:
|
||||||
|
module = self.modules["export"]
|
||||||
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
|
base = Path(directory)
|
||||||
|
evaluation = base / "Exam"
|
||||||
|
source = evaluation / "BGnot" / "Ex 1"
|
||||||
|
source.mkdir(parents=True)
|
||||||
|
(source / "Concat.pdf").write_bytes(b"annotated")
|
||||||
|
with patch.object(module, "EXPORT_DIR", base / "Export"):
|
||||||
|
self.assertEqual(module.main([str(evaluation)]), 0)
|
||||||
|
exported = base / "Export" / "Exam" / "Ex 1.pdf"
|
||||||
|
self.assertEqual(exported.read_bytes(), b"annotated")
|
||||||
|
|
||||||
|
def test_import_main_copies_handwritten_annotations(self) -> None:
|
||||||
|
module = self.modules["import"]
|
||||||
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
|
base = Path(directory)
|
||||||
|
evaluation = base / "Exam"
|
||||||
|
target = evaluation / "BGnot" / "Ex 1"
|
||||||
|
target.mkdir(parents=True)
|
||||||
|
import_dir = base / "Import"
|
||||||
|
import_dir.mkdir()
|
||||||
|
(import_dir / "Ex 1.pdf").write_bytes(b"handwritten")
|
||||||
|
with patch.object(module, "IMPORT_DIR", import_dir):
|
||||||
|
self.assertEqual(module.main([str(evaluation)]), 0)
|
||||||
|
self.assertEqual(
|
||||||
|
(target / "Concat_annotated.pdf").read_bytes(), b"handwritten"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_giving_names_main_builds_return_directory(self) -> None:
|
||||||
|
module = self.modules["giving_names"]
|
||||||
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
|
evaluation = Path(directory) / "Exam"
|
||||||
|
copies = evaluation / "Copies"
|
||||||
|
annotations = evaluation / "BGnot" / "Copie01"
|
||||||
|
copies.mkdir(parents=True)
|
||||||
|
annotations.mkdir(parents=True)
|
||||||
|
atomic_write_json(copies / "Copie01.json", {"name": "Élève Test"})
|
||||||
|
atomic_write_json(annotations / "score.json", {"total": 10})
|
||||||
|
(annotations / "Concat.jpg").write_bytes(b"image")
|
||||||
|
(evaluation / "names").write_text("Élève Test\n", encoding="utf-8")
|
||||||
|
|
||||||
|
self.assertEqual(module.main([str(evaluation), "BGnot"]), 0)
|
||||||
|
destination = evaluation / "A Rendre" / "Élève Test (01)"
|
||||||
|
self.assertEqual((destination / "Élève Test.jpg").read_bytes(), b"image")
|
||||||
|
self.assertEqual(read_json(destination / "score.json"), {"total": 10})
|
||||||
|
|
||||||
|
def test_grouping_main_accepts_empty_copies_directory(self) -> None:
|
||||||
|
module = self.modules["grouping"]
|
||||||
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
|
evaluation = Path(directory) / "Exam"
|
||||||
|
(evaluation / "Copies").mkdir(parents=True)
|
||||||
|
self.assertEqual(module.main([str(evaluation)]), 0)
|
||||||
|
self.assertTrue((evaluation / "Par label").is_dir())
|
||||||
|
|
||||||
|
|
||||||
class WorkflowTests(unittest.TestCase):
|
class WorkflowTests(unittest.TestCase):
|
||||||
def setUp(self) -> None:
|
def setUp(self) -> None:
|
||||||
self.steps = {step.id: step for step in build_workflow(True)}
|
self.steps = {step.id: step for step in build_workflow(True)}
|
||||||
|
|||||||
Reference in New Issue
Block a user