631 lines
27 KiB
Python
631 lines
27 KiB
Python
from __future__ import annotations
|
||
|
||
import os
|
||
import shlex
|
||
import sys
|
||
from collections.abc import Iterable
|
||
from dataclasses import dataclass
|
||
from pathlib import Path
|
||
|
||
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
|
||
|
||
|
||
@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
|
||
|
||
@property
|
||
def is_manual(self) -> bool:
|
||
return all(variant.kind == "manual" for variant in self.variants)
|
||
|
||
|
||
def arg_target(help_text: str = "Dossier d’évaluation ou fichier à traiter") -> ArgumentSpec:
|
||
return ArgumentSpec(
|
||
"target",
|
||
"Cible",
|
||
kind="path",
|
||
default=EVALUATION,
|
||
positional=True,
|
||
help=help_text,
|
||
)
|
||
|
||
|
||
def build_workflow(show_personal_steps: bool) -> list[StepDefinition]:
|
||
python = lambda ident, label, script, **kwargs: CommandVariant(
|
||
ident, label, script, "python", **kwargs
|
||
)
|
||
manual = lambda ident, label="Étape manuelle": CommandVariant(
|
||
ident, label, None, "manual"
|
||
)
|
||
|
||
steps = [
|
||
StepDefinition(
|
||
"inputs",
|
||
"Préparation",
|
||
"Vérifier les fichiers d’entré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("Dossier de l’évaluation"),
|
||
ArgumentSpec(
|
||
"restart",
|
||
"Ignorer le cache (--restart)",
|
||
kind="bool",
|
||
flag="--restart",
|
||
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.",
|
||
(CommandVariant("default", "Groupes Gemini", "statement", fixed_args=("--groups-only",)),),
|
||
arguments=(arg_target("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.",
|
||
(CommandVariant("default", "Barèmes Gemini", "statement", fixed_args=("--persp-only",)),),
|
||
arguments=(arg_target("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 à l’envers.",
|
||
(
|
||
CommandVariant(
|
||
"rotate",
|
||
"Rotation",
|
||
"copies",
|
||
"python",
|
||
("rotate",),
|
||
fixed_args_before_positionals=True,
|
||
),
|
||
),
|
||
arguments=(arg_target("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,
|
||
),
|
||
),
|
||
arguments=(arg_target("Dossier de l’évaluation"),),
|
||
),
|
||
StepDefinition(
|
||
"page_splitter",
|
||
"Prétraitement des copies",
|
||
"Séparer et réordonner les pages",
|
||
"Ouvre l’outil 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")),
|
||
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("Dossier de l’évaluation ou PDF dans Copies"),
|
||
ArgumentSpec("workers", "Copies traitées en parallèle", "int", "--workers", default=5)),
|
||
optional=True,
|
||
requires=("Copies",),
|
||
),
|
||
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"),
|
||
ArgumentSpec("marked", "Copies signalées uniquement", "bool", "--marked"),
|
||
),
|
||
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"),
|
||
),
|
||
requires=("labels", "Copies", "Cutleft"),
|
||
artifacts=("Copies/*.json",),
|
||
),
|
||
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(
|
||
"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("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 l’intégration d’un batch.",
|
||
(
|
||
python("live", "Correction immédiate", "correct"),
|
||
python("batch", "Préparer toutes les requêtes batch", "correct", fixed_args=("--batch",)),
|
||
python("hybrid", "Batch à partir d’un 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("Évaluation ou image Group_X.jpg"),
|
||
ArgumentSpec("overwrite", "Écraser les corrections existantes", "bool", "--overwrite", variants=("live",)),
|
||
ArgumentSpec("limit", "Limite d’appels Pro", "int", "--limit", variants=("live",)),
|
||
ArgumentSpec("batch_from", "Premier label envoyé en batch", "text", "--batch-from", variants=("hybrid",)),
|
||
),
|
||
requires=("Par label", "Persp", "labels"),
|
||
artifacts=("correction.json", "batch_requests_*.jsonl"),
|
||
),
|
||
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("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. L’identifiant de téléchargement est facultatif.",
|
||
(python("default", "État des batchs", "batch-status"),),
|
||
arguments=(ArgumentSpec("download", "Télécharger le job", "text", "--download"),),
|
||
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("Dossier de l’évaluation"),),
|
||
optional=True,
|
||
skip_for_live_correction=True,
|
||
),
|
||
StepDefinition(
|
||
"post_correction",
|
||
"Correction",
|
||
"Nettoyer la correction",
|
||
"Corrige certains problèmes d’encodage et prépare le texte pour LaTeX.",
|
||
(python("default", "Post-correction", "post-correction"),),
|
||
arguments=(arg_target("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("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("Dossier de l’évaluation"),
|
||
ArgumentSpec("overwrite", "Écraser les sorties", "bool", "--overwrite"),
|
||
ArgumentSpec("refaire", "Mode refaire", "bool", "--refaire", variants=("checks",)),
|
||
),
|
||
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("Dossier de l’évaluation"),
|
||
ArgumentSpec(
|
||
"annotation_dir",
|
||
"Dossier d’annotations",
|
||
"choice",
|
||
default="BGnot",
|
||
choices=("BGnot", "Bnot", "Anot"),
|
||
positional=True,
|
||
),
|
||
ArgumentSpec("refaire", "Mode refaire", "bool", "--refaire"),
|
||
),
|
||
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("Dossier de l’évaluation"),
|
||
ArgumentSpec(
|
||
"annotation_dir",
|
||
"Dossier d’annotations",
|
||
"choice",
|
||
default="BGnot",
|
||
choices=("BGnot", "Bnot", "Anot"),
|
||
positional=True,
|
||
),
|
||
ArgumentSpec("refaire", "Mode refaire", "bool", "--refaire"),
|
||
),
|
||
),
|
||
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("Dossier de l’évaluation"),
|
||
ArgumentSpec("update_score", "Réappliquer les score.json", "bool", "--update-score"),
|
||
ArgumentSpec("refaire", "Mode refaire", "bool", "--refaire", variants=("grouped",)),
|
||
),
|
||
),
|
||
StepDefinition(
|
||
"giving_names",
|
||
"Finalisation",
|
||
"Attribuer les noms et préparer A Rendre",
|
||
"Crée le dossier A Rendre à partir du dossier d’annotations choisi.",
|
||
(python("default", "Attribution des noms", "giving-names"),),
|
||
arguments=(
|
||
arg_target("Dossier de l’évaluation"),
|
||
ArgumentSpec(
|
||
"annotation_dir",
|
||
"Dossier d’annotations",
|
||
"choice",
|
||
default="BGnot",
|
||
choices=("BGnot", "Bnot", "Anot"),
|
||
positional=True,
|
||
),
|
||
),
|
||
artifacts=("A Rendre",),
|
||
),
|
||
StepDefinition(
|
||
"personal_create",
|
||
"Étapes personnelles",
|
||
"Créer l’interro 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"),),
|
||
arguments=(
|
||
arg_target("Dossier de l’évaluation"),
|
||
ArgumentSpec("sum", "Écrire seulement la somme", "bool", "--sum"),
|
||
),
|
||
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"),),
|
||
arguments=(arg_target("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 l’administration.",
|
||
(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 et score.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 et "
|
||
"score.json de A Rendre seront conservés.\n\n"
|
||
"Continuer ?"
|
||
),
|
||
),
|
||
),
|
||
arguments=(arg_target("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 s’ouvre à 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 d’export 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 d’import.", 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")),)
|
||
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),),
|
||
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 = "",
|
||
) -> 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 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())
|