Files
Copies/copienator_gui/workflow.py
T

832 lines
35 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from __future__ import annotations
import os
import shlex
import sys
from collections.abc import Iterable
from dataclasses import dataclass
from pathlib import Path
from copienator.configuration import ALWAYS_CROP
EVALUATION = "${evaluation}"
@dataclass(frozen=True)
class ArgumentSpec:
name: str
label: str
kind: str = "text" # text, int, bool, choice, path
flag: str | None = None
default: object = ""
choices: tuple[str, ...] = ()
help: str = ""
positional: bool = False
variants: tuple[str, ...] = ()
@dataclass(frozen=True)
class CommandVariant:
id: str
label: str
program: str | None
kind: str = "python" # python, shell, external, manual
fixed_args: tuple[str, ...] = ()
fixed_args_before_positionals: bool = False
dangerous: bool = False
danger_warning: str | None = None
supports_verbose: bool = False
@dataclass(frozen=True)
class StepDefinition:
id: str
section: str
title: str
description: str
variants: tuple[CommandVariant, ...]
arguments: tuple[ArgumentSpec, ...] = ()
optional: bool = False
personal: bool = False
requires: tuple[str, ...] = ()
artifacts: tuple[str, ...] = ()
auto_start_first_visit: bool = False
skip_for_live_correction: bool = False
skip_without_manual_conflicts: bool = False
extra_arguments_help: str = ""
extra_arguments_variants: tuple[str, ...] = ()
@property
def is_manual(self) -> bool:
return all(variant.kind == "manual" for variant in self.variants)
def arg_target(help_text: str = "un dossier d’évaluation ou un fichier à traiter") -> ArgumentSpec:
return ArgumentSpec(
"target",
"Cible",
kind="path",
default=EVALUATION,
positional=True,
help=f"La cible peut être {help_text.rstrip('.')}.",
)
def build_workflow(show_personal_steps: bool) -> list[StepDefinition]:
def python(ident, label, script, **kwargs):
supports_verbose = kwargs.pop("supports_verbose", True)
return CommandVariant(
ident,
label,
script,
"python",
supports_verbose=supports_verbose,
**kwargs,
)
manual = lambda ident, label="Étape manuelle": CommandVariant(
ident, label, None, "manual"
)
steps = [
StepDefinition(
"inputs",
"Préparation",
"Vérifier les fichiers dentrée",
"Le dossier doit contenir enonce.pdf, enonce.tex et correction.tex. "
"Le fichier names doit être présent dans l’évaluation ou à la racine du projet.",
(manual("check"),),
requires=("enonce.pdf", "enonce.tex", "correction.tex"),
),
StepDefinition(
"statement",
"Prétraitement de l’énoncé",
"Analyser l’énoncé",
"Détecte les questions, leurs groupes, le corrigé et les indications de barème.",
(
python("gemini", "Analyse avec Gemini", "statement"),
) + ((python("personal", "Énoncés et solutions personnels (SHEETINFO)", "statement-personal"),)
if show_personal_steps else ()),
arguments=(
arg_target("le dossier de l’évaluation"),
ArgumentSpec(
"restart",
"Ignorer le cache (--restart)",
kind="bool",
flag="--restart",
help="Ignore les résultats Gemini mis en cache et recommence entièrement lanalyse de l’énoncé.",
variants=("gemini",),
),
),
requires=("enonce.pdf", "enonce.tex", "correction.tex"),
artifacts=("labels", "Text", "Sol", "Persp"),
),
StepDefinition(
"statement_groups", "Prétraitement de l’énoncé", "Regrouper les questions avec Gemini",
"Facultatif après la génération : remplace les groupes par exercice par des groupes "
"proposés par Gemini, en conservant les labels, les énoncés, les solutions et les barèmes.",
(python("default", "Groupes Gemini", "statement", fixed_args=("--groups-only",)),),
arguments=(arg_target("le dossier de l’évaluation"),),
optional=True, personal=True, requires=("labels", "Text2", "Sol2"),
),
StepDefinition(
"statement_persp", "Prétraitement de l’énoncé", "Remplacer les barèmes par Gemini",
"Facultatif : remplace Persp par des barèmes Gemini sur 4 points, pour les groupes actuels. "
"Les énoncés et les solutions personnels sont conservés.",
(python("default", "Barèmes Gemini", "statement", fixed_args=("--persp-only",)),),
arguments=(arg_target("le dossier de l’évaluation"),),
optional=True, personal=True, requires=("labels", "label_groups", "Text2", "Sol2"),
),
StepDefinition(
"review_persp",
"Prétraitement de l’énoncé",
"Relire les barèmes",
"Étape manuelle facultative : vérifier et modifier les instructions de correction.",
(manual("review"),),
optional=True,
requires=("Persp",),
),
StepDefinition(
"rotate",
"Prétraitement des copies",
"Retourner toutes les copies",
"Rotation facultative de 180° lorsque les scans sont à lenvers.",
(
CommandVariant(
"rotate",
"Rotation",
"copies",
"python",
("rotate",),
fixed_args_before_positionals=True,
supports_verbose=True,
),
),
arguments=(arg_target("le dossier de l’évaluation"),),
optional=True,
),
StepDefinition(
"rename",
"Prétraitement des copies",
"Renommer les PDF en CopieXX.pdf",
"Renomme les PDF du dossier d’évaluation dans leur ordre courant.",
(
CommandVariant(
"rename",
"Renommage",
"copies",
"python",
("rename",),
fixed_args_before_positionals=True,
supports_verbose=True,
),
),
arguments=(arg_target("le dossier de l’évaluation"),),
),
StepDefinition(
"page_splitter",
"Prétraitement des copies",
"Séparer et réordonner les pages",
"Ouvre loutil interactif de découpage A3 vers A4. La cible peut être un dossier ou un PDF.",
(python("default", "Séparation des pages", "page-split"),),
arguments=(
arg_target(),
ArgumentSpec(
"marked",
"Copies signalées uniquement",
"bool",
"--marked",
help="Limite lopération aux copies signalées dans linterface au lieu de traiter toutes les copies.",
),
),
artifacts=("Copies", "Copies Originales"),
),
StepDefinition(
"crop_blank_margins",
"Prétraitement des copies",
"Rogner les zones vides",
"Facultatif : détecte les zones vides en haut et en bas malgré les lignes et les perforations, "
"puis remplace les PDF dans Copies. Les versions non rognées sont sauvegardées. "
"À effectuer avant la détection des labels. Traite plusieurs copies en parallèle.",
(python("default", "Rognage des zones vides", "crop-margins"),),
arguments=(
arg_target("le dossier de l’évaluation ou un PDF du dossier Copies"),
ArgumentSpec(
"workers",
"Copies traitées en parallèle",
"int",
"--workers",
default=5,
help="Fixe le nombre maximal de copies rognées simultanément. Une valeur élevée accélère le traitement si la machine possède assez de cœurs et de mémoire.",
),
),
optional=True,
requires=("Copies",),
auto_start_first_visit=ALWAYS_CROP,
),
StepDefinition(
"cutleft",
"Prétraitement des copies",
"Découper la marge des labels",
"Produit les images de la partie gauche des copies. Une copie précise peut être ciblée.",
(python("default", "Découpe", "crop-labels"),),
arguments=(
arg_target(),
ArgumentSpec(
"fullpage",
"Toujours utiliser la page entière",
"bool",
"--fullpage",
help="Désactive la découpe habituelle de la marge gauche et transmet chaque page entière à la détection des labels.",
),
ArgumentSpec(
"marked",
"Copies signalées uniquement",
"bool",
"--marked",
help="Limite lopération aux copies signalées dans linterface au lieu de traiter toutes les copies.",
),
),
requires=("Copies",),
artifacts=("Cutleft",),
),
StepDefinition(
"labels",
"Labels et regroupement",
"Détecter les labels avec Gemini",
"Identifie les labels dans les images produites par la découpe gauche.",
(python("default", "Détection des labels", "labels"),),
arguments=(
arg_target(),
ArgumentSpec(
"overwrite",
"Régénérer les résultats",
"bool",
"--overwrite",
help="Relance la détection même lorsquun fichier JSON de labels existe déjà pour la copie.",
),
),
requires=("labels", "Copies", "Cutleft"),
artifacts=("Copies/*.json",),
extra_arguments_help=(
"PDF de copie ou images Cutleft supplémentaires de cette évaluation, "
"séparés par des espaces. Mettez entre guillemets les chemins contenant des espaces."
),
),
StepDefinition(
"plotting",
"Labels et regroupement",
"Vérifier visuellement les labels",
"Ouvre la fenêtre de vérification. La cible peut être toute l’évaluation ou une copie.",
(python("default", "Vérification", "review-labels"),),
arguments=(arg_target(),),
requires=("labels", "Cutleft"),
artifacts=("Copies/Copie*.json",),
),
StepDefinition(
"splitting",
"Labels et regroupement",
"Découper les réponses par question",
"Découpe les copies à partir des coordonnées de labels vérifiées.",
(python("default", "Découpage", "split-answers"),),
arguments=(arg_target(),),
requires=("Copies",),
artifacts=("Copies/Copie*/*",),
auto_start_first_visit=True,
),
StepDefinition(
"crop_exercise_bottoms",
"Labels et regroupement",
"Rogner le bas des réponses",
"Facultatif après le découpage par labels : rogne uniquement les grands espaces "
"vides au bas des réponses. Les PDF modifiés sont remplacés, les originaux sont "
"sauvegardés et plusieurs fichiers sont analysés en parallèle.",
(python("default", "Rognage du bas", "crop-answer-bottoms"),),
arguments=(
arg_target("le dossier de l’évaluation"),
ArgumentSpec(
"workers",
"PDF traités en parallèle",
"int",
"--workers",
default=5,
help="Fixe le nombre maximal de PDF de réponse analysés simultanément. Une valeur élevée accélère le traitement si la machine possède assez de cœurs et de mémoire.",
),
),
optional=True,
requires=("Copies/Copie*/*.pdf",),
auto_start_first_visit=ALWAYS_CROP,
),
StepDefinition(
"grouping",
"Labels et regroupement",
"Regrouper les réponses",
"Regroupe les réponses portant le même label pour préparer les requêtes.",
(python("default", "Regroupement", "group-answers"),),
arguments=(arg_target("le dossier de l’évaluation"),),
requires=("Copies",),
artifacts=("Par label",),
auto_start_first_visit=True,
),
StepDefinition(
"correction",
"Correction",
"Lancer ou intégrer la correction",
"Choisir une correction immédiate, batch, hybride, une recorrection, ou lintégration dun batch.",
(
python("live", "Correction immédiate", "correct"),
python("batch", "Préparer toutes les requêtes batch", "correct", fixed_args=("--batch",)),
python("hybrid", "Batch à partir dun label", "correct"),
python("refaire", "Recorrection depuis refaire.json", "correct", fixed_args=("--refaire",)),
python(
"integrate",
"Intégrer les résultats batch",
"correct",
fixed_args=("--deal-with-batched",),
),
python("reset", "Réinitialiser les corrections", "correct", fixed_args=("--reset",), dangerous=True),
),
arguments=(
arg_target("le dossier de l’évaluation ou une image Group_X.jpg"),
ArgumentSpec(
"overwrite",
"Écraser les corrections existantes",
"bool",
"--overwrite",
help="Relance les corrections demandées même lorsquun résultat existe déjà.",
variants=("live", "batch", "hybrid", "refaire"),
),
ArgumentSpec(
"limit",
"Limite dappels Pro",
"int",
"--limit",
help="Limite le nombre dappels au modèle Pro pendant cette exécution. Laissez ce champ vide pour ne pas imposer de limite.",
variants=("live", "hybrid", "refaire"),
),
ArgumentSpec(
"batch_from",
"Premier label envoyé en batch",
"text",
"--batch-from",
help="Indique le premier label traité en batch ; les labels précédents sont corrigés immédiatement.",
variants=("hybrid",),
),
),
requires=("Par label", "Persp", "labels"),
artifacts=("correction.json", "batch_requests_*.jsonl"),
extra_arguments_help=(
"Images Group_X.jpg supplémentaires de cette évaluation, séparées par des espaces. "
"Mettez entre guillemets les chemins contenant des espaces."
),
extra_arguments_variants=("live", "batch", "hybrid", "refaire"),
),
StepDefinition(
"submit_batches",
"Correction",
"Envoyer les batchs",
"Envoie à Gemini les fichiers JSONL produits par le mode batch.",
(python("default", "Envoi", "batch-submit"),),
arguments=(arg_target("le dossier de l’évaluation"),),
optional=True,
artifacts=("batch_jobs.json",),
skip_for_live_correction=True,
),
StepDefinition(
"batch_status",
"Correction",
"Consulter l’état des batchs",
"Affiche les jobs Gemini en cours. Lidentifiant de téléchargement est facultatif.",
(python("default", "État des batchs", "batch-status"),),
arguments=(
ArgumentSpec(
"download",
"Télécharger le job",
"text",
"--download",
help="Saisissez lidentifiant complet dun job Gemini terminé pour télécharger son fichier de résultats.",
),
ArgumentSpec(
"output",
"Fichier JSONL de destination",
"path",
"--output",
help="Choisissez le fichier JSONL dans lequel enregistrer le job téléchargé. Ce champ nest utilisé que si un identifiant de job est fourni.",
),
),
optional=True,
skip_for_live_correction=True,
),
StepDefinition(
"fetch_batches",
"Correction",
"Récupérer les résultats batch",
"Télécharge et rassemble les réponses des jobs terminés.",
(python("default", "Récupération", "batch-fetch"),),
arguments=(arg_target("le dossier de l’évaluation"),),
optional=True,
skip_for_live_correction=True,
),
StepDefinition(
"post_correction",
"Correction",
"Nettoyer la correction",
"Corrige certains problèmes dencodage et prépare le texte pour LaTeX.",
(python("default", "Post-correction", "post-correction"),),
arguments=(arg_target("le dossier de l’évaluation"),),
requires=("correction.json",),
),
StepDefinition(
"manual_resolution",
"Correction",
"Résoudre les conflits manuels",
"Étape conditionnelle, uniquement si manual_resolutions.txt contient des conflits à traiter.",
(python("default", "Résolution", "resolve-manual"),),
arguments=(arg_target("le dossier de l’évaluation"),),
optional=True,
requires=("manual_resolutions.txt", "correction.json"),
skip_without_manual_conflicts=True,
),
StepDefinition(
"annotation",
"Génération des annotations",
"Générer les copies annotées",
"Les trois modes sont exclusifs pour un parcours donné.",
(
python("simple", "Annotations simples (Anot)", "annotate-simple"),
python("checks", "Annotations avec cases (Bnot)", "annotate-checks"),
python("grouped", "Annotations groupées (BGnot)", "annotate-grouped"),
),
arguments=(
arg_target("le dossier de l’évaluation"),
ArgumentSpec(
"overwrite",
"Écraser les sorties",
"bool",
"--overwrite",
help="Remplace les annotations déjà générées dans le dossier de sortie sélectionné.",
),
ArgumentSpec(
"refaire",
"Mode refaire",
"bool",
"--refaire",
help="Génère uniquement les copies et questions inscrites dans refaire.json, dans le dossier réservé à la reprise.",
variants=("checks", "grouped"),
),
),
requires=("correction.json",),
artifacts=("Anot", "Bnot", "BGnot"),
),
StepDefinition(
"export",
"Génération des annotations",
"Exporter",
"Exporte les annotations vers le dossier EXPORT_DIR défini dans config.py.",
(python("default", "Export", "export"),),
arguments=(
arg_target("le dossier de l’évaluation"),
ArgumentSpec(
"annotation_dir",
"Dossier dannotations",
"choice",
default="BGnot",
choices=("BGnot", "Bnot", "Anot"),
help="Choisissez le dossier dannotations à exporter : groupées, avec cases, ou simples.",
positional=True,
),
ArgumentSpec(
"refaire",
"Mode refaire",
"bool",
"--refaire",
help="Exporte les annotations du passage de reprise BRnot au lieu du dossier principal.",
),
),
optional=True,
),
StepDefinition(
"tablet",
"Correction manuscrite",
"Annoter les PDF sur la tablette",
"Étape manuelle : enregistrer chaque résultat sous le nom Concat_annotated.pdf.",
(manual("tablet"),),
),
StepDefinition(
"import",
"Correction manuscrite",
"Importer les annotations manuscrites",
"Copie les PDF présents dans IMPORT_DIR vers l’évaluation.",
(python("default", "Import", "import"),),
arguments=(
arg_target("le dossier de l’évaluation"),
ArgumentSpec(
"annotation_dir",
"Dossier dannotations",
"choice",
default="BGnot",
choices=("BGnot", "Bnot", "Anot"),
help="Choisissez le dossier principal dans lequel importer les annotations manuscrites.",
positional=True,
),
ArgumentSpec(
"refaire",
"Mode refaire",
"bool",
"--refaire",
help="Importe les annotations manuscrites dans BRnot pour le passage de reprise.",
),
),
),
StepDefinition(
"read_annotations",
"Finalisation",
"Lire les annotations manuscrites",
"Le mode doit correspondre au mode choisi lors de la génération des annotations.",
(
python("standard", "Lecture Bnot", "read-annotations"),
python("grouped", "Lecture BGnot", "read-grouped"),
),
arguments=(
arg_target("le dossier de l’évaluation"),
ArgumentSpec(
"update_score",
"Réappliquer les score.json",
"bool",
"--update-score",
help="Réutilise les valeurs présentes dans les fichiers score.json pour remplacer les scores lus dans les annotations.",
),
ArgumentSpec(
"refaire",
"Mode refaire",
"bool",
"--refaire",
help="Lit les annotations du passage de reprise BRnot et les fusionne avec les copies principales.",
variants=("grouped",),
),
ArgumentSpec(
"annotation_dir",
"Passage principal du mode refaire",
"choice",
"--annotation-dir",
default="BGnot",
choices=("BGnot", "Bnot", "Anot"),
help="Indique le dossier dannotations du passage principal dans lequel intégrer les questions refaites.",
variants=("grouped",),
),
),
),
StepDefinition(
"giving_names",
"Finalisation",
"Attribuer les noms et préparer A Rendre",
"Crée le dossier A Rendre à partir du dossier dannotations choisi.",
(python("default", "Attribution des noms", "giving-names"),),
arguments=(
arg_target("le dossier de l’évaluation"),
ArgumentSpec(
"annotation_dir",
"Dossier dannotations",
"choice",
default="BGnot",
choices=("BGnot", "Bnot", "Anot"),
help="Choisissez le dossier dannotations utilisé pour construire les fichiers nommés dans A Rendre.",
positional=True,
),
),
artifacts=("A Rendre",),
),
StepDefinition(
"personal_create",
"Étapes personnelles",
"Créer linterro dans gestion_classe",
"Exécute gestion_classe ne.",
(CommandVariant("default", "gestion_classe ne", "gestion_classe", "external", ("ne",)),),
personal=True,
),
StepDefinition(
"personal_scale",
"Étapes personnelles",
"Renseigner le barème",
"Exécute gestion_classe we.",
(CommandVariant("default", "gestion_classe we", "gestion_classe", "external", ("we",)),),
personal=True,
),
StepDefinition(
"update_ods",
"Étapes personnelles",
"Mettre à jour le fichier ODS",
"Transfère les scores, avec la possibilité de n’écrire que leur somme.",
(python("default", "Mise à jour ODS", "update-ods", supports_verbose=False),),
arguments=(
arg_target("le dossier de l’évaluation"),
ArgumentSpec(
"sum",
"Écrire seulement la somme",
"bool",
"--sum",
help="Écrit uniquement la note totale de chaque élève dans le fichier ODS, sans détailler les scores par question.",
),
),
personal=True,
),
StepDefinition(
"personal_read",
"Étapes personnelles",
"Relire avec gestion_classe",
"Exécute gestion_classe re.",
(CommandVariant("default", "gestion_classe re", "gestion_classe", "external", ("re",)),),
personal=True,
),
StepDefinition(
"personal_sent",
"Étapes personnelles",
"Marquer comme envoyé",
"Exécute gestion_classe wsent.",
(CommandVariant("default", "gestion_classe wsent", "gestion_classe", "external", ("wsent",)),),
personal=True,
),
StepDefinition(
"final_score",
"Étapes personnelles",
"Ajouter le score final",
"Génère les fichiers de diffusion avec le score final.",
(python("default", "Score final", "add-final-score", supports_verbose=False),),
arguments=(arg_target("le dossier de l’évaluation"),),
personal=True,
),
StepDefinition(
"personal_deploy",
"Étapes personnelles",
"Déployer et publier les copies",
"Étape manuelle : déployer miqmacs-copies-assets puis mettre à jour les copies depuis ladministration.",
(manual("deploy"),),
personal=True,
),
StepDefinition(
"personal_print",
"Étapes personnelles",
"Imprimer une copie",
"Étape manuelle via le lecteur PDF.",
(manual("print"),),
personal=True,
optional=True,
),
StepDefinition(
"clean",
"Archivage",
"Nettoyer les fichiers intermédiaires",
"Supprime définitivement les fichiers permettant de reprendre le parcours. "
"Conserve les PDF traités, les fichiers textuels de l’énoncé, correction.json, "
"les journaux, ainsi que les images, PDF, score.json et info.json de A Rendre.",
(
python(
"default",
"Nettoyage définitif",
"clean",
fixed_args=("--yes",),
dangerous=True,
danger_warning=(
"Le nettoyage est irréversible.\n\n"
"Toute la progression du GUI et les fichiers intermédiaires "
"seront supprimés. Il ne sera plus possible de reprendre une "
"étape sans régénérer ses données.\n\n"
"Les PDF traités, les fichiers textuels de l’énoncé, "
"correction.json, les journaux, ainsi que les images, PDF et "
"score.json et info.json de A Rendre seront conservés.\n\n"
"Continuer ?"
),
),
python(
"dry_run",
"Prévisualiser sans supprimer",
"clean",
fixed_args=("--dry-run",),
),
),
arguments=(arg_target("le dossier de l’évaluation"),),
optional=True,
requires=("Copies", "correction.json", "A Rendre"),
),
]
steps[-1:-1] = build_refaire_workflow()
return [step for step in steps if show_personal_steps or not step.personal]
def build_refaire_workflow() -> list[StepDefinition]:
from .refaire import SECTION
selection = StepDefinition(
"refaire_selection", SECTION, "Choisir les copies et les questions",
"Sélectionnez les copies et les questions à refaire, puis enregistrez la sélection. "
"Le passage principal doit être terminé ; conservez ses annotations.",
(CommandVariant("default", "Sélection", None, "manual"),),
requires=("Copies", "labels", "correction.json"),
)
definitions = [
("review", "Reprendre le découpage", "Vérifiez et ajustez les labels des copies sélectionnées. Chaque copie souvre à son tour. Fermez la fenêtre pour passer à la suivante.", "review-labels", (), True),
("split", "Redécouper les réponses", "À exécuter après une modification du découpage. Traite toutes les copies sélectionnées ; vérifiez les fichiers _new et _old en cas de résolution manuelle.", "split-answers", (), True),
("correct", "Refaire la correction", "Relance la correction des seules questions sélectionnées. Peut être ignorée pour corriger manuellement les résultats.", "correct", ("--refaire",), True),
("annotate", "Préparer les copies à vérifier", "Génère les questions sélectionnées avec des cases dans BRnot, par question ou par copie selon la sélection. Remplace le précédent passage dans BRnot.", "annotate-checks", ("--refaire", "--overwrite"), False),
("export", "Exporter vers la tablette", "Exporte BRnot vers EXPORT_DIR. Retirez les anciens fichiers dexport avant le transfert.", "export", ("--refaire",), True),
("tablet", "Vérifier sur la tablette", "Annotez les PDF exportés, puis placez les retours dans IMPORT_DIR sans changer leur nom (nom de groupe ou Copie01.pdf…). Retournez aussi les PDF sans modification. Retirez les anciens fichiers dimport.", None, (), False),
("import", "Importer les copies vérifiées", "Importe les PDF retournés dans BRnot. Vous pouvez ignorer cette étape si les fichiers Concat_annotated.pdf y sont déjà en place.", "import", ("--refaire",), True),
("merge", "Mettre à jour les copies finales", "Fusionne les questions refaites avec le reste de chaque copie dans le dossier du passage principal. Relancez ensuite la préparation de A Rendre, le calcul des notes et la diffusion.", "read-grouped", ("--refaire",), False),
]
steps = [selection]
for suffix, title, description, program, flags, optional in definitions:
arguments = (arg_target(),) if program else ()
if suffix == "merge":
arguments += (ArgumentSpec(
"annotation_dir",
"Passage principal",
"choice",
"--annotation-dir",
default="BGnot",
choices=("BGnot", "Bnot", "Anot"),
help="Indique le dossier dannotations du passage principal dans lequel intégrer les questions refaites.",
),)
requirements = ("refaire.json", "Copies", "labels", "correction.json")
if suffix in {"export", "tablet", "import", "merge"}:
requirements += ("BRnot",)
steps.append(StepDefinition(
f"refaire_{suffix}", SECTION, title, description,
(CommandVariant(
"default",
title,
program,
"python" if program else "manual",
flags,
supports_verbose=program is not None,
),),
arguments=arguments, optional=optional, requires=requirements,
))
return steps
def value_for_default(value: object, evaluation_arg: str) -> object:
return evaluation_arg if value == EVALUATION else value
def build_command(
repository: Path,
step: StepDefinition,
variant: CommandVariant,
values: dict[str, object],
evaluation_arg: str,
extra_arguments: str = "",
verbose: bool = False,
) -> list[str]:
if variant.kind == "manual" or not variant.program:
return []
program_path = repository / variant.program
if variant.kind == "python":
command = [sys.executable, "-u", "-m", "copienator", variant.program]
elif variant.kind == "shell":
command = [str(program_path)]
else:
command = [variant.program]
positionals: list[str] = []
options: list[str] = []
for spec in step.arguments:
if spec.variants and variant.id not in spec.variants:
continue
value = values.get(spec.name, value_for_default(spec.default, evaluation_arg))
if spec.kind == "bool":
if bool(value) and spec.flag:
options.append(spec.flag)
continue
if value is None or str(value).strip() == "":
continue
rendered = str(value)
if spec.positional:
positionals.append(rendered)
elif spec.flag:
options.extend((spec.flag, rendered))
if variant.fixed_args_before_positionals:
command.extend(variant.fixed_args)
command.extend(positionals)
if not variant.fixed_args_before_positionals:
command.extend(variant.fixed_args)
command.extend(options)
if verbose and variant.supports_verbose:
command.append("--verbose")
if extra_arguments.strip():
command.extend(shlex.split(extra_arguments, posix=os.name != "nt"))
return command
def command_display(command: Iterable[str]) -> str:
if os.name == "nt":
return " ".join(f'"{part}"' if " " in part else part for part in command)
return shlex.join(command)
def evaluation_argument(repository: Path, evaluation: Path) -> str:
try:
return str(evaluation.resolve().relative_to(repository.resolve()))
except ValueError:
return str(evaluation.resolve())