Restructuration de l'application
This commit is contained in:
+7
-16
@@ -56,21 +56,10 @@ Les codes de sortie communs sont :
|
||||
| 4 | traitement partiel, avec avertissements |
|
||||
| 130 | interruption par l'utilisateur |
|
||||
|
||||
Le GUI distingue notamment un traitement partiel d'un échec. Les
|
||||
scripts migrés vers cette convention sont actuellement :
|
||||
|
||||
- =copies_tools.py=, =grouping.py= et =verify_groups.py= ;
|
||||
- =post-correction.py= et =resolve_manual.py= ;
|
||||
- =page_splitter.py=, =cutleft.py=, =plotting.py= et
|
||||
=splitting_int.py= ;
|
||||
- =gemini_for_labels.py= ;
|
||||
- =gemini_for_enonce.py= et =enonce_info.py= ;
|
||||
- =correction.py=, =submit_batches.py=, =batch_status.py= et
|
||||
=fetch_batched_results.py= ;
|
||||
- =annotating.py=, =annotating_with_checks.py= et
|
||||
=annotating_by_label.py= ;
|
||||
- =reading_annotations.py= et =reading_grouped_annotations.py= ;
|
||||
- =export.py=, =import.py= et =giving_names.py=.
|
||||
Le GUI distingue notamment un traitement partiel d'un échec. Toutes les
|
||||
commandes du paquet suivent désormais cette convention. Elles sont
|
||||
regroupées dans =copienator.commands= et exposées par le répartiteur
|
||||
=python -m copienator=.
|
||||
|
||||
* Organisation du code
|
||||
|
||||
@@ -78,8 +67,10 @@ scripts migrés vers cette convention sont actuellement :
|
||||
scripts, notamment =EvaluationWorkspace= et les écritures atomiques.
|
||||
- Le paquet =copienator_gui= contient la définition du workflow, l'état
|
||||
persistant, le lanceur de processus et l'application Tk.
|
||||
- Les scripts situés à la racine restent des points d'entrée autonomes.
|
||||
- Le sous-paquet =copienator.commands= contient les points d'entrée.
|
||||
Leur import ne doit pas déclencher de traitement.
|
||||
- =copienator.dispatcher= expose le CLI unifié et =copienator_gui=
|
||||
contient l'application Tk.
|
||||
|
||||
* État et exécution du GUI
|
||||
|
||||
|
||||
+7
-2
@@ -41,8 +41,13 @@ Libraries :
|
||||
|
||||
#+BEGIN_SRC bash
|
||||
pip install numpy pandas matplotlib pillow pydantic pypdf pdf2image reportlab img2pdf pymupdf ftfy ezodf google
|
||||
pip install -e .
|
||||
#+END_SRC
|
||||
|
||||
La seconde commande installe les exécutables =copienator= et
|
||||
=copienator-gui=. Sans installation, les mêmes commandes restent
|
||||
accessibles avec =python -m copienator=.
|
||||
|
||||
*** Poppler (for pdf2image)
|
||||
|
||||
+ Linux : install poppler-utils
|
||||
@@ -79,10 +84,10 @@ Copier `default_config.py` en `config.py`. Éventuellement le modifier.
|
||||
Lancer l'assistant avec :
|
||||
|
||||
#+BEGIN_SRC bash
|
||||
python gui.py
|
||||
python -m copienator gui
|
||||
#+END_SRC
|
||||
|
||||
On peut aussi ouvrir directement une évaluation avec =python gui.py
|
||||
On peut aussi ouvrir directement une évaluation avec =python -m copienator gui
|
||||
Interro=. L'interface conserve l'état et l'historique des étapes dans
|
||||
=Interro/.copienator-gui.json=, et les sorties complètes dans
|
||||
=Interro/.copienator/logs/=. Une relance d'une étape antérieure ne
|
||||
|
||||
+60
-61
@@ -11,7 +11,7 @@ les parcours alternatifs.
|
||||
|
||||
* Étapes et Script
|
||||
|
||||
Utiliser `python gui.py` ou `python gui.py Interro` pour lancer un GUI
|
||||
Utiliser `python -m copienator gui` ou `python -m copienator gui Interro` pour lancer un GUI
|
||||
qui suit automatiquement les étapes décrites ci-dessous.
|
||||
|
||||
** Prétraitement de l'énoncé
|
||||
@@ -20,8 +20,8 @@ Dans le dossier de l'évaluation, mettre les fichiers suivants de l'évaluation
|
||||
|
||||
`enonce.pdf`, `enonce.tex`, `correction.tex`.
|
||||
|
||||
- `python gemini_for_enonce.py Interro` or
|
||||
`python gemini_for_enonce.py Interro --restart`
|
||||
- `python -m copienator statement Interro` or
|
||||
`python -m copienator statement Interro --restart`
|
||||
|
||||
À partir des trois fichiers précédents, se charge de détecter les
|
||||
labels des questions et leur contenu.
|
||||
@@ -52,25 +52,25 @@ Dans le dossier de l'évaluation, mettre les fichiers suivants de l'évaluation
|
||||
|
||||
Éventuellement : vérifier et modifier les barèmes dans `Persp`.
|
||||
|
||||
- Alternative personnelle : `python enonce_info.py Interro`
|
||||
- Alternative personnelle : `python -m copienator statement-personal Interro`
|
||||
|
||||
Ces deux commandes suivent la convention des scripts standardisés.
|
||||
Leur import ne lance aucun traitement et les erreurs partielles sont
|
||||
distinguées des échecs. Les réponses d'extraction mises en cache par
|
||||
=gemini_for_enonce.py= et le fichier =labels= sont publiés
|
||||
=copienator statement= et le fichier =labels= sont publiés
|
||||
atomiquement.
|
||||
|
||||
** Prétraitement des copies
|
||||
|
||||
Mettre les copies scannées au format pdf dans =Interro=.
|
||||
|
||||
1. =python copies_tools.py rotate Interro= (facultatif)
|
||||
1. =python -m copienator copies rotate Interro= (facultatif)
|
||||
|
||||
Retourne tous les pdf de 180°, si la photocopie a été faite à
|
||||
l'envers.
|
||||
2. =python copies_tools.py rename Interro=
|
||||
2. =python -m copienator copies rename Interro=
|
||||
change le nom des copies en =Copie{id}.pdf=
|
||||
3. =python page_splitter.py Interro=
|
||||
3. =python -m copienator page-split Interro=
|
||||
|
||||
Découpe des copies A3 en pages A4, en retirant les pages vides.
|
||||
|
||||
@@ -80,21 +80,21 @@ Mettre les copies scannées au format pdf dans =Interro=.
|
||||
+ de jeter les deux pages
|
||||
+ de déplacer la délimitation à droite/gauche
|
||||
|
||||
Fix issues with =python page_splitter.py Interro14/Copies/Copie01.pdf=
|
||||
Fix issues with =python -m copienator page-split Interro14/Copies/Copie01.pdf=
|
||||
|
||||
Le PDF transformé est construit dans un dossier temporaire. La
|
||||
copie produite et la sauvegarde dans =Copies Originales= sont
|
||||
ensuite installées avec rollback : une erreur conserve les deux
|
||||
versions précédentes. Une relance ciblée lit directement la
|
||||
sauvegarde originale sans la déplacer au préalable.
|
||||
4. =python cutleft.py Interro=
|
||||
4. =python -m copienator crop-labels Interro=
|
||||
|
||||
Découpe la partie gauche des copies, là où il devrait y avoir les
|
||||
labels des exercices/questions.
|
||||
|
||||
=python cutleft.py Interro --fullpage= to use the full page always.
|
||||
=python -m copienator crop-labels Interro --fullpage= to use the full page always.
|
||||
|
||||
Rerun on a single file with =python cutleft.py Interro/Copies/Copie01.pdf=
|
||||
Rerun on a single file with =python -m copienator crop-labels Interro/Copies/Copie01.pdf=
|
||||
|
||||
Les images et le fichier =_schema.json= d'une copie sont remplacés
|
||||
ensemble. Fermer l'outil juste après la dernière validation ne peut
|
||||
@@ -104,7 +104,7 @@ Mettre les copies scannées au format pdf dans =Interro=.
|
||||
|
||||
Optional : Set proxy with ~export HTTPS_PROXY="http://10.0.0.1:3128"~
|
||||
|
||||
1. =python gemini_for_labels.py Interro=, avec éventuellement =--overwrite=
|
||||
1. =python -m copienator labels Interro=, avec éventuellement =--overwrite=
|
||||
|
||||
Fait des requêtes à Gemini pour identifier les labels des
|
||||
questions dans images générées à partir des parties gauches des copies.
|
||||
@@ -116,7 +116,7 @@ Optional : Set proxy with ~export HTTPS_PROXY="http://10.0.0.1:3128"~
|
||||
copies différentes sont traitées en parallèle. Chaque réponse JSON
|
||||
validée est écrite atomiquement. Une cible sans image correspondante
|
||||
produit le code de sortie 4.
|
||||
2. =python plotting.py Interro=
|
||||
2. =python -m copienator review-labels Interro=
|
||||
|
||||
Permet de vérifier visuellement les labels trouvés.
|
||||
+ Sous linux, on peut faire =e= pour ouvrir le fichier .json et
|
||||
@@ -128,7 +128,7 @@ Optional : Set proxy with ~export HTTPS_PROXY="http://10.0.0.1:3128"~
|
||||
+ `|…` n'est pas arrêté verticalement par son type opposé.
|
||||
+ `…|` est stoppé horizontalement par le `|…` le plus proche.
|
||||
Pour modifier une seule copie :
|
||||
=python plotting.py Interro/Copies/Copie01.pdf=
|
||||
=python -m copienator review-labels Interro/Copies/Copie01.pdf=
|
||||
|
||||
Les coordonnées agrégées sont écrites atomiquement dans le JSON de
|
||||
la copie. Fermer la fenêtre avant la fin d'une copie ne remplace pas
|
||||
@@ -137,9 +137,9 @@ Optional : Set proxy with ~export HTTPS_PROXY="http://10.0.0.1:3128"~
|
||||
It also generates les =Copie01.json=, à partir des =Copie01_01.json=
|
||||
En cas de soucis, (par exemple les pages ne sont pas dans le bon ordre)
|
||||
- Réordonner les pages du fichier pdf
|
||||
- Rerun =python cutleft.py Interro/Copie{id}=
|
||||
- Rerun =python gemini_for_labels.py Interro/Copie{id}=
|
||||
3. =python splitting_int.py Interro=
|
||||
- Rerun =python -m copienator crop-labels Interro/Copie{id}=
|
||||
- Rerun =python -m copienator labels Interro/Copie{id}=
|
||||
3. =python -m copienator split-answers Interro=
|
||||
|
||||
Découpe les copies suivant les exercices
|
||||
Peut-être appelé avec une seule copie.
|
||||
@@ -148,11 +148,11 @@ Optional : Set proxy with ~export HTTPS_PROXY="http://10.0.0.1:3128"~
|
||||
puis remplacent ensemble le dossier précédent. En cas d'erreur,
|
||||
l'ancienne version est conservée. Les réponses devenues obsolètes
|
||||
restent archivées dans le sous-dossier =Missing=.
|
||||
4. =python grouping.py Interro=
|
||||
4. =python -m copienator group-answers Interro=
|
||||
|
||||
Regroupe les mêmes questions de différentes copies en groupes de
|
||||
tailles raisonnables.
|
||||
5. Facultatif : =python verify_groups.py Interro=
|
||||
5. Facultatif : =python -m copienator verify-groups Interro=
|
||||
|
||||
Vérifie que chaque réponse PDF apparaît bien dans les métadonnées
|
||||
des groupes. La commande renvoie un code non nul si une réponse est
|
||||
@@ -163,14 +163,14 @@ Optional : Set proxy with ~export HTTPS_PROXY="http://10.0.0.1:3128"~
|
||||
Optional : Set proxy with ~export HTTPS_PROXY="http://10.0.0.1:3128"~
|
||||
|
||||
1. Il faut créer des persp, pour indication de comment corriger, et
|
||||
relancer =enonce_info.py=
|
||||
2. =python correction.py Interro --limit 240= OU
|
||||
=python correction.py Interro/Par\ label/Ex\ 2/Group_1.jpg= OU
|
||||
=python correction.py Interro --overwrite=
|
||||
relancer =copienator statement-personal=
|
||||
2. =python -m copienator correct Interro --limit 240= OU
|
||||
=python -m copienator correct Interro/Par\ label/Ex\ 2/Group_1.jpg= OU
|
||||
=python -m copienator correct Interro --overwrite=
|
||||
|
||||
Fais les requêtes de correction à Gemini.
|
||||
|
||||
=correction.py= peut être relancé sans supprimer son état. Les
|
||||
=copienator correct= peut être relancé sans supprimer son état. Les
|
||||
fichiers =correction.json= et =correction_progress.json= sont mis à
|
||||
jour atomiquement. Avec =--overwrite=, leur version précédente reste
|
||||
en place jusqu'à la première écriture réussie de la nouvelle
|
||||
@@ -183,23 +183,23 @@ Optional : Set proxy with ~export HTTPS_PROXY="http://10.0.0.1:3128"~
|
||||
|
||||
Pour diminuer le coût, il est possible de batch les requêtes, qui
|
||||
seront alors traitées sous au plus 24h.
|
||||
+ =python correction.py Interro --batch=
|
||||
+ OU =python correction.py Interro --batch-from 'Ex 4'=
|
||||
+ =python submit_batches.py Interro=
|
||||
+ =python batch_status.py=
|
||||
+ =python fetch_batched_results.py Interro=
|
||||
+ =python correction.py Interro --deal-with-batched=
|
||||
+ =python -m copienator correct Interro --batch=
|
||||
+ OU =python -m copienator correct Interro --batch-from 'Ex 4'=
|
||||
+ =python -m copienator batch-submit Interro=
|
||||
+ =python -m copienator batch-status=
|
||||
+ =python -m copienator batch-fetch Interro=
|
||||
+ =python -m copienator correct Interro --deal-with-batched=
|
||||
|
||||
Les quatre commandes de ce flux suivent la convention des scripts
|
||||
standardisés. Les fichiers de requêtes et le résultat JSONL combiné
|
||||
sont publiés atomiquement : une interruption ne laisse pas de fichier
|
||||
final partiellement écrit. =submit_batches.py= conserve aussi les
|
||||
final partiellement écrit. =copienator batch-submit= conserve aussi les
|
||||
identifiants distants dans =batch_jobs.json= ; la récupération les
|
||||
utilise en priorité et garde la recherche par nom pour les anciens
|
||||
batchs. =batch_status.py --download JOB --output
|
||||
batchs. =python -m copienator batch-status --download JOB --output
|
||||
resultat.jsonl= permet aussi de télécharger atomiquement le résultat
|
||||
d'un job particulier.
|
||||
3. =python post-correction.py Interro=
|
||||
3. =python -m copienator post-correction Interro=
|
||||
|
||||
- Essaye de corriger des erreurs d'encodage/d'accents dans
|
||||
=correction.json=.
|
||||
@@ -214,29 +214,29 @@ Optional : Set proxy with ~export HTTPS_PROXY="http://10.0.0.1:3128"~
|
||||
+ `xx` : move to goal.
|
||||
+ `xs` : remove old, keep goal.
|
||||
|
||||
Then call `python resolve_manual.py Interro`
|
||||
5. Call `python correction.py Interro --refaire`.
|
||||
Then call `python -m copienator resolve-manual Interro`
|
||||
5. Call `python -m copienator correct Interro --refaire`.
|
||||
|
||||
|
||||
** Génération des copies annotées
|
||||
|
||||
1. =python annotating.py Interro= (facultatif)
|
||||
1. =python -m copienator annotate-simple Interro= (facultatif)
|
||||
|
||||
Ajoute les annotations Gemini aux copies, enregistrées dans le dossier =Anot=.
|
||||
On peut passer l'argument =--overwrite=.
|
||||
OU
|
||||
2. =python annotating_with_checks.py Interro=
|
||||
2. =python -m copienator annotate-checks Interro=
|
||||
|
||||
Ajoute les annotations Gemini, et des checkboxes à cocher.
|
||||
Enregistrées dans le dossier =Bnot=,
|
||||
=--overwrite=
|
||||
|
||||
Une seule copie peut être ciblée avec, par exemple,
|
||||
=python annotating_with_checks.py Interro/Copies/Copie01.pdf=.
|
||||
=python -m copienator annotate-checks Interro/Copies/Copie01.pdf=.
|
||||
Le mode =--refaire= exige un fichier =refaire.json= et écrit dans
|
||||
=BRnot=.
|
||||
OU
|
||||
2. =python annotating_by_label.py Interro= dans =BGnot=
|
||||
2. =python -m copienator annotate-grouped Interro= dans =BGnot=
|
||||
|
||||
Ajoute les annotations Gemini, et des checkboxes, et regroupe les
|
||||
réponses par question.
|
||||
@@ -245,7 +245,7 @@ OU
|
||||
_Needs_ : label_groups file (made automatically by this function),
|
||||
qui dit quelles questions regrouper.
|
||||
|
||||
3. =python export.py Interro BGnot= (gestion perso)
|
||||
3. =python -m copienator export Interro BGnot= (gestion perso)
|
||||
Cela déplace les groupes dans le dossier configuré par =EXPORT_DIR=
|
||||
(par défaut =Export=).
|
||||
|
||||
@@ -263,7 +263,7 @@ tablette graphique.
|
||||
_Before_ : vider le dossier configuré par =IMPORT_DIR= (par défaut
|
||||
=Import=), puis y copier ou synchroniser les fichiers depuis la tablette.
|
||||
|
||||
1. =python import.py Interro BGnot=
|
||||
1. =python -m copienator import Interro BGnot=
|
||||
|
||||
Une fois les corrections manuelles appliquées aux fichiers
|
||||
=Concat.pdf=, il faut enregistrer le fichier annoté au même endroit,
|
||||
@@ -274,7 +274,7 @@ _Before_ : vider le dossier configuré par =IMPORT_DIR= (par défaut
|
||||
export. Pour =Anot=, l'image est importée sous le nom
|
||||
=Concat_annotated.jpg=.
|
||||
|
||||
2. =python reading_annotations.py Interro=
|
||||
2. =python -m copienator read-annotations Interro=
|
||||
|
||||
Lit les =Concat_annotated= dans =Bnot=, regénère les copies avec
|
||||
les modifications. Les fichiers générés (=score.json=,
|
||||
@@ -282,12 +282,12 @@ _Before_ : vider le dossier configuré par =IMPORT_DIR= (par défaut
|
||||
ensemble. Les entrées du dossier =Bnot= restent en place et une
|
||||
erreur de génération conserve les anciennes sorties.
|
||||
OU
|
||||
2. =python reading_grouped_annotations.py Interro=
|
||||
2. =python -m copienator read-grouped Interro=
|
||||
|
||||
Idem, mais pour =BGnot=. Les tâches parallèles remontent leurs
|
||||
erreurs au processus principal au lieu de les ignorer.
|
||||
|
||||
3. =python giving_names.py Interro BGnot=
|
||||
3. =python -m copienator giving-names Interro BGnot=
|
||||
|
||||
Crée un dossier =A Rendre= avec des liens symboliques vers
|
||||
+ La copie à rendre
|
||||
@@ -297,17 +297,17 @@ OU
|
||||
4. Éventuellement, faire des modifications manuelles aux =score.json=.
|
||||
|
||||
Puis
|
||||
- `python reading_annotations.py --update-score Interro`
|
||||
- `python reading_grouped_annotations.py --update-score Interro`
|
||||
- `python -m copienator read-annotations --update-score Interro`
|
||||
- `python -m copienator read-grouped --update-score Interro`
|
||||
pour mettre à jour les scores dans les images.
|
||||
4. (gestion perso)
|
||||
+ =gestion_classe ne= pour créer l'interro puis
|
||||
+ =gestion_classe we= (set barème here)
|
||||
+ =python update_ods.py Interro=
|
||||
ou =python update_ods.py Interro --sum= (en l'absence de barème)
|
||||
+ =python -m copienator update-ods Interro=
|
||||
ou =python -m copienator update-ods Interro --sum= (en l'absence de barème)
|
||||
+ =gestion_classe re=
|
||||
+ =gestion_classe wsent=
|
||||
+ =python add_final_score.py Interro21=
|
||||
+ =python -m copienator add-final-score Interro21=
|
||||
(this makes files in =Server/copies=)
|
||||
5. (gestion perso)
|
||||
+ Deploy =miqmacs-copies-assets=, and
|
||||
@@ -321,8 +321,8 @@ OU
|
||||
groupée into refaire !!
|
||||
|
||||
1. Redécoupage
|
||||
+ =python plotting.py InterroTest/Copie01.pdf=
|
||||
+ =python splitting_int.py InterroTest/Copie20.pdf=
|
||||
+ =python -m copienator review-labels InterroTest/Copie01.pdf=
|
||||
+ =python -m copienator split-answers InterroTest/Copie20.pdf=
|
||||
2. Créer =refaire.json=, avec un contenu comme
|
||||
: [["Copie02", []],
|
||||
: ["Copie01", ["Ex 1 : 1)"]]]
|
||||
@@ -333,10 +333,10 @@ groupée into refaire !!
|
||||
Ou non, si tu veux le faire à la main.
|
||||
4. ?? Si je fais refaire, avant d'avoir créer les annotating with
|
||||
checks, que se passe-t-il ???
|
||||
5. Appeler =annotating_with_checks.py --refaire --overwrite=
|
||||
6. =python export.py --refaire Interro24=
|
||||
6. =python import.py --refaire Interro24=
|
||||
7. =python reading_grouped_annotations.py --refaire Interro24=
|
||||
5. Appeler =python -m copienator annotate-checks --refaire --overwrite=
|
||||
6. =python -m copienator export --refaire Interro24=
|
||||
6. =python -m copienator import --refaire Interro24=
|
||||
7. =python -m copienator read-grouped --refaire Interro24=
|
||||
|
||||
Avec =--refaire=, =refaire.json= et le dossier =BRnot= sont des
|
||||
prérequis obligatoires ; leur absence produit le code de sortie 3.
|
||||
@@ -344,11 +344,10 @@ groupée into refaire !!
|
||||
** Exemple de replotting, refaire d'une copie
|
||||
|
||||
1. replot it.
|
||||
2. `python splitting_int.py DS09VA/Copies/Copie25.pdf`
|
||||
2. `python -m copienator split-answers DS09VA/Copies/Copie25.pdf`
|
||||
this will get rid of old/new.
|
||||
!! Attention, et si ça dégage un new : bad bad bad.
|
||||
3. Make `refaire.json`, avec la copie, et les labels à refaire.
|
||||
4. `python correction.py DS09VA --refaire`
|
||||
5. `python annotating_with_checks.py DS09VA --refaire`
|
||||
6. `python import.py Interro24 --refaire`
|
||||
|
||||
4. `python -m copienator correct DS09VA --refaire`
|
||||
5. `python -m copienator annotate-checks DS09VA --refaire`
|
||||
6. `python -m copienator import Interro24 --refaire`
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
from .dispatcher import main
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,5 @@
|
||||
"""Executable Copienator commands.
|
||||
|
||||
Command modules expose a ``main(argv=None)`` entry point and may also expose
|
||||
their processing functions for reuse and tests.
|
||||
"""
|
||||
@@ -7,7 +7,7 @@ from pathlib import Path
|
||||
import pandas as pd
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
|
||||
from config import FINAL_SCORE_FONT_PATH, FINAL_SCORE_ODS_PATH, FINAL_SCORE_OUTPUT_DIR
|
||||
from copienator.configuration import FINAL_SCORE_FONT_PATH, FINAL_SCORE_ODS_PATH, FINAL_SCORE_OUTPUT_DIR
|
||||
|
||||
# Configuration constants
|
||||
ODS_PATH = Path(FINAL_SCORE_ODS_PATH).expanduser()
|
||||
@@ -112,14 +112,17 @@ def process_images(base_dir, output_dir):
|
||||
shutil.copy(str(pdf_path), str(save_path))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
def main(argv=None):
|
||||
parser = argparse.ArgumentParser(description="Stamp scores on exam copies.")
|
||||
parser.add_argument("dir", type=Path, help="Root directory containing 'A Rendre' folder")
|
||||
|
||||
|
||||
args = parser.parse_args()
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
base_dir = args.dir.expanduser().resolve()
|
||||
output_dir = OUTPUT_DIR / base_dir.name
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
process_images(base_dir, output_dir)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -19,8 +19,8 @@ from matplotlib import pyplot as plt
|
||||
from pdf2image import convert_from_path
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
|
||||
import utils
|
||||
from config import LATEX_ANOT_AFTER, LATEX_ANOT_BEFORE
|
||||
from copienator import utils
|
||||
from copienator.configuration import LATEX_ANOT_AFTER, LATEX_ANOT_BEFORE
|
||||
from copienator import (
|
||||
EvaluationWorkspace,
|
||||
ExitCode,
|
||||
@@ -31,7 +31,7 @@ from copienator import (
|
||||
)
|
||||
from copienator.annotation_data import load_annotation_data
|
||||
from copienator.filesystem import staged_directory
|
||||
from utils import natural_key
|
||||
from copienator.utils import natural_key
|
||||
|
||||
MARGIN_LEFT = 300
|
||||
ANNOT_WIDTH = 600
|
||||
@@ -10,9 +10,9 @@ from typing import Any
|
||||
from PIL import Image, ImageDraw
|
||||
from reportlab.pdfgen import canvas
|
||||
|
||||
import annotating
|
||||
import annotating_with_checks
|
||||
import utils
|
||||
from copienator.commands import annotating
|
||||
from copienator.commands import annotating_with_checks
|
||||
from copienator import utils
|
||||
from copienator import (
|
||||
CliError,
|
||||
EvaluationWorkspace,
|
||||
@@ -26,7 +26,7 @@ from copienator import (
|
||||
)
|
||||
from copienator.annotation_data import load_annotation_data
|
||||
from copienator.filesystem import staged_directory
|
||||
from utils import natural_key
|
||||
from copienator.utils import natural_key
|
||||
|
||||
MAX_HEIGHT_PX = 25000
|
||||
|
||||
@@ -14,8 +14,8 @@ matplotlib.use("Agg")
|
||||
from PIL import Image, ImageFont
|
||||
from reportlab.pdfgen import canvas
|
||||
|
||||
import annotating
|
||||
import utils
|
||||
from copienator.commands import annotating
|
||||
from copienator import utils
|
||||
from copienator import (
|
||||
CliError,
|
||||
EvaluationWorkspace,
|
||||
@@ -28,7 +28,7 @@ from copienator import (
|
||||
)
|
||||
from copienator.annotation_data import load_annotation_data
|
||||
from copienator.filesystem import staged_directory
|
||||
from utils import natural_key
|
||||
from copienator.utils import natural_key
|
||||
|
||||
BOX_SIZE = 30
|
||||
SCORE_BOX_SIZE = 40
|
||||
@@ -328,3 +328,4 @@ def main(argv: Sequence[str] | None = None) -> int:
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
||||
@@ -6,7 +6,7 @@ from pathlib import Path
|
||||
|
||||
from google import genai
|
||||
|
||||
import config
|
||||
from copienator import configuration as config
|
||||
from copienator import (
|
||||
CliError,
|
||||
ExitCode,
|
||||
@@ -134,3 +134,4 @@ def main(argv: Sequence[str] | None = None) -> int:
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
||||
@@ -15,9 +15,9 @@ from pathlib import Path
|
||||
|
||||
from google import genai
|
||||
|
||||
import config
|
||||
import grouping
|
||||
import prompting
|
||||
from copienator import configuration as config
|
||||
from copienator.commands import grouping
|
||||
from copienator import prompting
|
||||
from copienator import (
|
||||
CliError,
|
||||
EvaluationWorkspace,
|
||||
@@ -29,7 +29,7 @@ from copienator import (
|
||||
target_parser,
|
||||
workspace_from_target,
|
||||
)
|
||||
from utils import enonce_total, read_all_labels
|
||||
from copienator.utils import enonce_total, read_all_labels
|
||||
|
||||
NB_THREADS = 12
|
||||
|
||||
@@ -874,7 +874,7 @@ def run_configured(args: argparse.Namespace) -> ExitCode:
|
||||
+ "\n",
|
||||
)
|
||||
print(f"\n[!] Unresolved delayed tasks found! Wrote to {manual_path}.")
|
||||
print(" Please edit it manually, then run `python resolve_manual.py <InputDir>`")
|
||||
print(" Please edit it manually, then run `python -m copienator resolve-manual <InputDir>`")
|
||||
|
||||
end_time = time.time()
|
||||
print("Time elapsed : ", end_time - start_time)
|
||||
@@ -884,7 +884,7 @@ def run_configured(args: argparse.Namespace) -> ExitCode:
|
||||
for (err, file) in errors_summary:
|
||||
print(err, file=sys.stderr)
|
||||
escaped_path = shlex.quote(str(file))
|
||||
print(f"Run : python correction.py {escaped_path}")
|
||||
print(f"Run : python -m copienator correct {escaped_path}")
|
||||
return ExitCode.PARTIAL if errors_summary else ExitCode.SUCCESS
|
||||
|
||||
|
||||
@@ -341,3 +341,4 @@ def main(argv: Sequence[str] | None = None) -> int:
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
||||
@@ -17,8 +17,8 @@ from copienator import (
|
||||
execute,
|
||||
workspace_from_args,
|
||||
)
|
||||
from platform_utils import WindowsLabelError, validate_windows_labels
|
||||
from utils import compile_to_pdf
|
||||
from copienator.platform import WindowsLabelError, validate_windows_labels
|
||||
from copienator.utils import compile_to_pdf
|
||||
|
||||
|
||||
def fetch_and_save_sub_text(ex_id, indices, label, text_path):
|
||||
@@ -293,3 +293,4 @@ def main(argv: Sequence[str] | None = None) -> int:
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
||||
@@ -3,7 +3,7 @@ import sys
|
||||
from collections.abc import Sequence
|
||||
from pathlib import Path
|
||||
|
||||
from config import EXPORT_DIR
|
||||
from copienator.configuration import EXPORT_DIR
|
||||
from copienator import (
|
||||
EvaluationWorkspace,
|
||||
ExitCode,
|
||||
@@ -11,7 +11,7 @@ from copienator import (
|
||||
execute,
|
||||
workspace_from_args,
|
||||
)
|
||||
from platform_utils import replace_with_link_or_copy
|
||||
from copienator.platform import replace_with_link_or_copy
|
||||
|
||||
ANNOTATION_DIRECTORIES = ("BGnot", "Bnot", "Anot")
|
||||
|
||||
@@ -5,7 +5,7 @@ from collections.abc import Sequence
|
||||
|
||||
from google import genai
|
||||
|
||||
import config
|
||||
from copienator import configuration as config
|
||||
from copienator import (
|
||||
CliError,
|
||||
EvaluationWorkspace,
|
||||
@@ -10,8 +10,8 @@ from google import genai
|
||||
from google.genai import types
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
import config
|
||||
import utils
|
||||
from copienator import configuration as config
|
||||
from copienator import utils
|
||||
from copienator import (
|
||||
CliError,
|
||||
EvaluationWorkspace,
|
||||
@@ -21,8 +21,8 @@ from copienator import (
|
||||
execute,
|
||||
workspace_from_args,
|
||||
)
|
||||
from platform_utils import validate_windows_labels
|
||||
from utils import compile_to_pdf
|
||||
from copienator.platform import validate_windows_labels
|
||||
from copienator.utils import compile_to_pdf
|
||||
|
||||
|
||||
def get_lcp(s1: str, s2: str) -> str:
|
||||
@@ -13,7 +13,7 @@ from google import genai
|
||||
from google.genai import types
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
import config
|
||||
from copienator import configuration as config
|
||||
from copienator import (
|
||||
CliError,
|
||||
EvaluationWorkspace,
|
||||
@@ -24,7 +24,7 @@ from copienator import (
|
||||
target_parser,
|
||||
workspace_from_target,
|
||||
)
|
||||
from utils import natural_key, read_all_labels
|
||||
from copienator.utils import natural_key, read_all_labels
|
||||
|
||||
MODEL_ID = config.MODEL_FOR_LABEL_ID
|
||||
api_key = config.API_KEY
|
||||
@@ -422,7 +422,7 @@ def main(argv: Sequence[str] | None = None) -> int:
|
||||
|
||||
def handle(args: argparse.Namespace) -> ExitCode:
|
||||
workspace, target = workspace_from_target(
|
||||
args, repository=Path(__file__).resolve().parent
|
||||
args, repository=Path(__file__).resolve().parents[2]
|
||||
)
|
||||
targets = [target]
|
||||
for additional in args.additional_targets:
|
||||
@@ -15,7 +15,7 @@ from copienator import (
|
||||
read_json,
|
||||
workspace_from_args,
|
||||
)
|
||||
from platform_utils import replace_with_link_or_copy, safe_filename
|
||||
from copienator.platform import replace_with_link_or_copy, safe_filename
|
||||
|
||||
ANNOTATION_CHOICES = ("BGnot", "Bnot", "Anot")
|
||||
|
||||
@@ -163,3 +163,4 @@ def main(argv: Sequence[str] | None = None) -> int:
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
||||
@@ -219,7 +219,7 @@ def create_jpg(identifier, group_index, group, root_dir):
|
||||
|
||||
print(f"Saved {output_path} with {len(group)} ({os.path.getsize(output_path)/1024/1024:.2f} MB)")
|
||||
|
||||
from utils import natural_key
|
||||
from copienator.utils import natural_key
|
||||
|
||||
|
||||
def process_identifier(identifier, files_info, output_dir):
|
||||
@@ -275,3 +275,4 @@ def main(argv: Sequence[str] | None = None) -> int:
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
||||
@@ -4,7 +4,7 @@ import sys
|
||||
from collections.abc import Sequence
|
||||
from pathlib import Path
|
||||
|
||||
from config import IMPORT_DIR
|
||||
from copienator.configuration import IMPORT_DIR
|
||||
from copienator import (
|
||||
EvaluationWorkspace,
|
||||
ExitCode,
|
||||
@@ -0,0 +1,87 @@
|
||||
import json
|
||||
import sys
|
||||
import concurrent.futures
|
||||
from pathlib import Path
|
||||
from copienator.commands import correction
|
||||
|
||||
def get_missing_tasks():
|
||||
"""
|
||||
Identifies tasks (groups) where NONE of the student IDs in that group
|
||||
appear in the existing results for that label in correction.json.
|
||||
"""
|
||||
missing = []
|
||||
|
||||
# correction.results is already loaded from correction.json during 'from copienator.commands import correction'
|
||||
# correction.tasks is populated with (filepath, label) during 'from copienator.commands import correction'
|
||||
|
||||
for task in correction.tasks:
|
||||
file_path, label = task
|
||||
# Find the group metadata file (Group_X.json) to know which IDs are inside
|
||||
meta_path = Path(file_path).with_suffix('.json')
|
||||
|
||||
if not meta_path.exists():
|
||||
print("Missing meta_path :", meta_path)
|
||||
continue
|
||||
|
||||
with open(meta_path, 'r', encoding="utf-8") as f:
|
||||
# group_data entries: [pid, ymin, ymax, width_ratio]
|
||||
group_data = json.load(f)
|
||||
|
||||
pids_in_group = [str(item[0]) for item in group_data]
|
||||
|
||||
# Check correction.json results for this specific label
|
||||
label_results = correction.results.get(label, [])
|
||||
|
||||
# Collect all student IDs that have already been processed for this label
|
||||
covered_ids = set()
|
||||
for result_list in label_results:
|
||||
for entry in result_list:
|
||||
covered_ids.add(str(entry.get('id')))
|
||||
|
||||
# Logic: Only process if EVERY ID in this group is missing from correction.json
|
||||
if all(pid not in covered_ids for pid in pids_in_group):
|
||||
missing.append(task)
|
||||
|
||||
return missing
|
||||
|
||||
def main(argv=None):
|
||||
if argv:
|
||||
print("missing-correction does not accept command-line arguments.", file=sys.stderr)
|
||||
return 2
|
||||
missing_tasks = get_missing_tasks()
|
||||
print("\n Total nb of tasks : ", len(correction.tasks))
|
||||
|
||||
if not missing_tasks:
|
||||
print("All groups are already present in correction.json. Nothing to do.")
|
||||
return
|
||||
|
||||
print("\nThe following groups are missing from correction.json:")
|
||||
for path, label in missing_tasks:
|
||||
print(f" - [{label}] {path}")
|
||||
|
||||
confirm = input(f"\nFound {len(missing_tasks)} missing groups. Start processing? (y/N): ")
|
||||
if confirm.lower() != 'y':
|
||||
print("Aborted.")
|
||||
return
|
||||
|
||||
print(f"Processing {len(missing_tasks)} tasks with {correction.NB_THREADS} threads...")
|
||||
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=correction.NB_THREADS) as executor:
|
||||
# Map tasks to the processing function defined in correction.py
|
||||
futures = {executor.submit(correction.process_single_task, t): t for t in missing_tasks}
|
||||
|
||||
for future in concurrent.futures.as_completed(futures):
|
||||
try:
|
||||
# Handle potential sub-tasks (like label errors) generated during processing
|
||||
new_generated_tasks = future.result()
|
||||
if new_generated_tasks:
|
||||
for nt in new_generated_tasks:
|
||||
executor.submit(correction.process_single_task, nt)
|
||||
except Exception as e:
|
||||
t = futures[future]
|
||||
print(f"Error processing {t[0]}: {e}")
|
||||
|
||||
print("\nProcessing complete.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -16,7 +16,7 @@ import fitz # PyMuPDF
|
||||
from PIL import Image, ImageDraw, ImageTk
|
||||
from pypdf import PdfReader, PdfWriter
|
||||
|
||||
from config import PAGE_SPLITTER_KB
|
||||
from copienator.configuration import PAGE_SPLITTER_KB
|
||||
from copienator import (
|
||||
CliError,
|
||||
EvaluationWorkspace,
|
||||
@@ -25,7 +25,7 @@ from copienator import (
|
||||
target_parser,
|
||||
workspace_from_target,
|
||||
)
|
||||
from platform_utils import launch_pdf_arranger
|
||||
from copienator.platform import launch_pdf_arranger
|
||||
|
||||
# --- Constants ---
|
||||
# Conversion factor: 1 cm to points (1 inch = 2.54 cm, 72 points = 1 inch)
|
||||
@@ -19,8 +19,8 @@ from copienator import (
|
||||
target_parser,
|
||||
workspace_from_target,
|
||||
)
|
||||
from platform_utils import open_path
|
||||
from utils import natural_key, read_all_labels
|
||||
from copienator.platform import open_path
|
||||
from copienator.utils import natural_key, read_all_labels
|
||||
|
||||
# --- Configuration & Globals ---
|
||||
padding = 60
|
||||
@@ -472,3 +472,4 @@ def main(argv: Sequence[str] | None = None) -> int:
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
||||
@@ -16,7 +16,7 @@ from copienator import (
|
||||
workspace_from_args,
|
||||
)
|
||||
|
||||
WORD_LIST_FILE = Path(__file__).with_name("liste_francais.txt")
|
||||
WORD_LIST_FILE = Path(__file__).resolve().parents[1] / "data" / "liste_francais.txt"
|
||||
ACCENT_PATTERN = re.compile(r"[éèêëàâäîïôöùûüçœÉÈÊËÀÂÄÎÏÔÖÙÛÜÇŒ]")
|
||||
|
||||
|
||||
@@ -9,8 +9,8 @@ import numpy as np
|
||||
from pdf2image import convert_from_path
|
||||
from PIL import Image, ImageChops, ImageDraw, ImageFilter
|
||||
|
||||
import annotating
|
||||
import utils
|
||||
from copienator.commands import annotating
|
||||
from copienator import utils
|
||||
from copienator import (
|
||||
EvaluationWorkspace,
|
||||
ExitCode,
|
||||
@@ -284,3 +284,4 @@ def main(argv: Sequence[str] | None = None) -> int:
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
||||
@@ -9,8 +9,8 @@ from typing import Any
|
||||
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
import annotating
|
||||
import utils
|
||||
from copienator.commands import annotating
|
||||
from copienator import utils
|
||||
from copienator import (
|
||||
EvaluationWorkspace,
|
||||
ExitCode,
|
||||
@@ -23,7 +23,7 @@ from copienator import (
|
||||
from copienator.annotation_actions import apply_checkbox_actions, apply_score_overrides
|
||||
from copienator.annotation_data import AnnotationData, RefaireList, load_annotation_data
|
||||
from copienator.filesystem import staged_files
|
||||
from reading_annotations import (
|
||||
from copienator.commands.reading_annotations import (
|
||||
concatenate,
|
||||
detect_checks_and_notes,
|
||||
has_significant_notes,
|
||||
@@ -398,3 +398,4 @@ def main(argv: Sequence[str] | None = None) -> int:
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
||||
@@ -259,7 +259,7 @@ def resolve_manual(workspace: EvaluationWorkspace) -> ExitCode:
|
||||
if refaire_tasks:
|
||||
print(
|
||||
f"File {workspace.refaire_file.name} generated. Run "
|
||||
f'`python correction.py "{workspace.command_argument()}" --refaire` '
|
||||
f'`python -m copienator correct "{workspace.command_argument()}" --refaire` '
|
||||
"to process updates."
|
||||
)
|
||||
else:
|
||||
@@ -10,7 +10,7 @@ from pathlib import Path
|
||||
import fitz
|
||||
from pypdf import PdfReader, PdfWriter
|
||||
|
||||
import utils
|
||||
from copienator import utils
|
||||
from copienator import (
|
||||
CliError,
|
||||
EvaluationWorkspace,
|
||||
@@ -264,3 +264,4 @@ def main(argv: Sequence[str] | None = None) -> int:
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
||||
@@ -6,7 +6,7 @@ from collections.abc import Sequence
|
||||
from google import genai
|
||||
from google.genai import types
|
||||
|
||||
import config
|
||||
from copienator import configuration as config
|
||||
from copienator import (
|
||||
CliError,
|
||||
EvaluationWorkspace,
|
||||
@@ -6,18 +6,18 @@ from pathlib import Path
|
||||
|
||||
import ezodf
|
||||
|
||||
from config import CURRENT_SCORE_ODS_PATH
|
||||
from utils import read_all_labels
|
||||
from copienator.configuration import CURRENT_SCORE_ODS_PATH
|
||||
from copienator.utils import read_all_labels
|
||||
|
||||
# Configuration
|
||||
ODS_PATH = Path(CURRENT_SCORE_ODS_PATH).expanduser()
|
||||
TARGET_DIR_NAME = "A Rendre"
|
||||
|
||||
def main():
|
||||
def main(argv=None):
|
||||
parser = argparse.ArgumentParser(description="Update ODS with student scores.")
|
||||
parser.add_argument("work_dir", nargs="?", default=os.getcwd(), help="Directory to process")
|
||||
parser.add_argument("--sum", action="store_true", help="Write only the total sum per student")
|
||||
args = parser.parse_args()
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
work_dir = os.path.abspath(args.work_dir)
|
||||
|
||||
@@ -169,4 +169,4 @@ def main():
|
||||
print("Done.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,82 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import re
|
||||
import sys
|
||||
from collections.abc import Sequence
|
||||
from pathlib import Path
|
||||
|
||||
from copienator import (
|
||||
EvaluationWorkspace,
|
||||
ExitCode,
|
||||
evaluation_parser,
|
||||
execute,
|
||||
read_json,
|
||||
workspace_from_args,
|
||||
)
|
||||
|
||||
COPY_PATTERN = re.compile(r"Copie(\d+)")
|
||||
|
||||
|
||||
def build_parser() -> argparse.ArgumentParser:
|
||||
return evaluation_parser("Verify that every answer PDF appears in group metadata.")
|
||||
|
||||
|
||||
def collect_source_pdfs(copies_dir: Path) -> set[tuple[str, str]]:
|
||||
source_pdfs: set[tuple[str, str]] = set()
|
||||
for copy_dir in copies_dir.iterdir():
|
||||
match = COPY_PATTERN.fullmatch(copy_dir.name)
|
||||
if not match or not copy_dir.is_dir():
|
||||
continue
|
||||
copy_id = match.group(1)
|
||||
for pdf_path in copy_dir.glob("*.pdf"):
|
||||
source_pdfs.add((pdf_path.stem, copy_id))
|
||||
return source_pdfs
|
||||
|
||||
|
||||
def collect_grouped_pdfs(groups_dir: Path) -> tuple[set[tuple[str, str]], int]:
|
||||
grouped: set[tuple[str, str]] = set()
|
||||
read_errors = 0
|
||||
for json_path in groups_dir.glob("*/Group_*.json"):
|
||||
try:
|
||||
data = read_json(json_path)
|
||||
if not isinstance(data, list):
|
||||
raise TypeError("expected a JSON array")
|
||||
for entry in data:
|
||||
grouped.add((str(entry[4]), str(entry[0])))
|
||||
except (IndexError, OSError, TypeError, ValueError) as exc:
|
||||
print(f"Error reading {json_path}: {exc}", file=sys.stderr)
|
||||
read_errors += 1
|
||||
return grouped, read_errors
|
||||
|
||||
|
||||
def verify_groups(workspace: EvaluationWorkspace) -> ExitCode:
|
||||
workspace.require_directories("Copies", "Par label")
|
||||
source_pdfs = collect_source_pdfs(workspace.copies_dir)
|
||||
grouped_pdfs, read_errors = collect_grouped_pdfs(workspace.groups_dir)
|
||||
missing = source_pdfs - grouped_pdfs
|
||||
|
||||
if missing:
|
||||
print(f"Verification failed: {len(missing)} files missing from groups:")
|
||||
for label, copy_id in sorted(missing):
|
||||
print(f"Copie{copy_id}/{label}.pdf")
|
||||
return ExitCode.FAILURE
|
||||
if read_errors:
|
||||
print("Verification incomplete because some metadata could not be read.")
|
||||
return ExitCode.PARTIAL
|
||||
print("Verification successful: all files accounted for.")
|
||||
return ExitCode.SUCCESS
|
||||
|
||||
|
||||
def run(workspace: EvaluationWorkspace) -> ExitCode:
|
||||
return verify_groups(workspace)
|
||||
|
||||
|
||||
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__":
|
||||
raise SystemExit(main())
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import os
|
||||
from pathlib import Path
|
||||
from types import ModuleType
|
||||
|
||||
|
||||
def _user_config_path() -> Path | None:
|
||||
explicit = os.environ.get("COPIENATOR_CONFIG", "").strip()
|
||||
if explicit:
|
||||
return Path(explicit).expanduser().resolve()
|
||||
local = Path.cwd() / "config.py"
|
||||
return local.resolve() if local.is_file() else None
|
||||
|
||||
|
||||
def _load_user_config(path: Path) -> ModuleType:
|
||||
if not path.is_file():
|
||||
raise FileNotFoundError(f"Copienator configuration not found: {path}")
|
||||
spec = importlib.util.spec_from_file_location("_copienator_user_config", path)
|
||||
if spec is None or spec.loader is None:
|
||||
raise ImportError(f"Could not load Copienator configuration: {path}")
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
CONFIG_PATH = _user_config_path()
|
||||
if CONFIG_PATH is None:
|
||||
import default_config as _configuration
|
||||
else:
|
||||
_configuration = _load_user_config(CONFIG_PATH)
|
||||
|
||||
for _name in dir(_configuration):
|
||||
if not _name.startswith("_"):
|
||||
globals()[_name] = getattr(_configuration, _name)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,76 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
import sys
|
||||
from collections.abc import Sequence
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class Command:
|
||||
module: str
|
||||
description: str
|
||||
|
||||
|
||||
COMMANDS: dict[str, Command] = {
|
||||
"statement": Command("gemini_for_enonce", "Analyse the exam statement with Gemini"),
|
||||
"statement-personal": Command("enonce_info", "Generate personal statement metadata"),
|
||||
"copies": Command("copies_tools", "Rotate or rename scanned copies"),
|
||||
"page-split": Command("page_splitter", "Split and reorder scanned PDF pages"),
|
||||
"crop-labels": Command("cutleft", "Crop the label margin from copies"),
|
||||
"labels": Command("gemini_for_labels", "Detect question labels with Gemini"),
|
||||
"review-labels": Command("plotting", "Review detected labels interactively"),
|
||||
"split-answers": Command("splitting_int", "Split copies into answers"),
|
||||
"group-answers": Command("grouping", "Group answers by question"),
|
||||
"verify-groups": Command("verify_groups", "Verify grouped answer metadata"),
|
||||
"correct": Command("correction", "Generate or integrate corrections"),
|
||||
"batch-submit": Command("submit_batches", "Submit Gemini batch jobs"),
|
||||
"batch-status": Command("batch_status", "Inspect Gemini batch jobs"),
|
||||
"batch-fetch": Command("fetch_batched_results", "Fetch Gemini batch results"),
|
||||
"post-correction": Command("post_correction", "Clean generated correction text"),
|
||||
"resolve-manual": Command("resolve_manual", "Resolve manual label conflicts"),
|
||||
"annotate-simple": Command("annotating", "Generate simple annotations"),
|
||||
"annotate-checks": Command("annotating_with_checks", "Generate checkable annotations"),
|
||||
"annotate-grouped": Command("annotating_by_label", "Generate grouped annotations"),
|
||||
"export": Command("export", "Export annotations"),
|
||||
"import": Command("import_annotations", "Import handwritten annotations"),
|
||||
"read-annotations": Command("reading_annotations", "Read checkable annotations"),
|
||||
"read-grouped": Command("reading_grouped_annotations", "Read grouped annotations"),
|
||||
"giving-names": Command("giving_names", "Name copies and prepare A Rendre"),
|
||||
"update-ods": Command("update_ods", "Update the configured score spreadsheet"),
|
||||
"add-final-score": Command("add_final_score", "Stamp final scores on copies"),
|
||||
"gui": Command("@gui", "Launch the graphical workflow assistant"),
|
||||
}
|
||||
|
||||
|
||||
def _print_help() -> None:
|
||||
print("Usage: copienator COMMAND [ARGUMENTS]")
|
||||
print(" python -m copienator COMMAND [ARGUMENTS]")
|
||||
print("\nCommands:")
|
||||
width = max(len(name) for name in COMMANDS)
|
||||
for name, command in COMMANDS.items():
|
||||
print(f" {name:<{width}} {command.description}")
|
||||
print("\nUse 'copienator COMMAND --help' for command-specific help.")
|
||||
|
||||
|
||||
def main(argv: Sequence[str] | None = None) -> int:
|
||||
arguments = list(sys.argv[1:] if argv is None else argv)
|
||||
if not arguments or arguments[0] in {"-h", "--help"}:
|
||||
_print_help()
|
||||
return 0
|
||||
|
||||
command_name = arguments.pop(0)
|
||||
command = COMMANDS.get(command_name)
|
||||
if command is None:
|
||||
print(f"Unknown command: {command_name}", file=sys.stderr)
|
||||
print("Use 'copienator --help' to list commands.", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
module_name = (
|
||||
"copienator_gui.__main__"
|
||||
if command.module == "@gui"
|
||||
else f"copienator.commands.{command.module}"
|
||||
)
|
||||
module = importlib.import_module(module_name)
|
||||
result = module.main(arguments)
|
||||
return int(result or 0)
|
||||
@@ -118,3 +118,4 @@ def replace_with_link_or_copy(
|
||||
|
||||
shutil.copy2(source_path, destination_path)
|
||||
return "copy"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from pathlib import Path
|
||||
import io
|
||||
import utils
|
||||
from . import utils
|
||||
|
||||
main_prompt = """I'm giving you an image of several written answers to an exam.
|
||||
|
||||
@@ -86,7 +86,7 @@ Here is a possible correct answer :
|
||||
You are asked to score the question or exercice labeled `<<label>>`,
|
||||
do not score or give feedback to any other question."""
|
||||
|
||||
from utils import get_label_text_content, get_label_sol_content, get_label_persp_content
|
||||
from .utils import get_label_text_content, get_label_sol_content, get_label_persp_content
|
||||
|
||||
def make_prompt(input_dir,full_label):
|
||||
text = get_label_text_content(input_dir, full_label) or ""
|
||||
@@ -298,3 +298,4 @@ Here is a list of all possible labels. You need to answer with a list one of the
|
||||
])]
|
||||
config = types.GenerateContentConfig(temperature=1.0, response_mime_type="application/json")
|
||||
return contents, config
|
||||
|
||||
@@ -2,7 +2,7 @@ import re
|
||||
from pathlib import Path
|
||||
|
||||
from copienator import EvaluationWorkspace
|
||||
from platform_utils import validate_windows_labels
|
||||
from .platform import validate_windows_labels
|
||||
|
||||
def natural_key(text):
|
||||
return [int(c) if c.isdigit() else c.lower() for c in re.split(r'(\d+)', str(text))]
|
||||
@@ -30,7 +30,7 @@ def enonce_total(base_dir):
|
||||
import os
|
||||
import shlex
|
||||
|
||||
from platform_utils import open_path
|
||||
from .platform import open_path
|
||||
|
||||
def edit_file_and_enter(file):
|
||||
editor = os.environ.get("EDITOR")
|
||||
@@ -139,7 +139,7 @@ import tempfile
|
||||
import shutil
|
||||
import subprocess
|
||||
|
||||
from config import LATEX_AFTER, LATEX_BEFORE
|
||||
from .configuration import LATEX_AFTER, LATEX_BEFORE
|
||||
|
||||
|
||||
def compile_to_pdf(text, output_pdf_path):
|
||||
@@ -8,25 +8,23 @@ from copienator_gui.app import CopienatorApp
|
||||
|
||||
def personal_steps_enabled() -> bool:
|
||||
try:
|
||||
from config import SHOW_PERSONAL_STEPS
|
||||
from copienator.configuration import SHOW_PERSONAL_STEPS
|
||||
except (ImportError, AttributeError):
|
||||
try:
|
||||
from default_config import SHOW_PERSONAL_STEPS
|
||||
except (ImportError, AttributeError):
|
||||
return False
|
||||
return False
|
||||
return bool(SHOW_PERSONAL_STEPS)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
def main(argv=None) -> int:
|
||||
parser = argparse.ArgumentParser(description="Interface graphique du workflow Copienator")
|
||||
parser.add_argument("evaluation", nargs="?", type=Path, help="Dossier d’évaluation à ouvrir")
|
||||
args = parser.parse_args()
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
repository = Path(__file__).resolve().parent
|
||||
repository = Path.cwd().resolve()
|
||||
evaluation = args.evaluation.resolve() if args.evaluation else None
|
||||
app = CopienatorApp(repository, personal_steps_enabled(), evaluation)
|
||||
app.mainloop()
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
raise SystemExit(main())
|
||||
@@ -8,7 +8,7 @@ from tkinter import filedialog, messagebox, ttk
|
||||
from typing import Any
|
||||
|
||||
from copienator import ExitCode
|
||||
from platform_utils import WindowsLabelError, open_path, validate_windows_labels
|
||||
from copienator.platform import WindowsLabelError, open_path, validate_windows_labels
|
||||
|
||||
from .diagnostics import collect_diagnostics
|
||||
from .runner import ProcessRunner
|
||||
@@ -71,7 +71,7 @@ def detected_annotation_directories(evaluation: Path) -> tuple[str, ...]:
|
||||
|
||||
def plotting_shortcut_lines() -> list[str]:
|
||||
try:
|
||||
from config import PLOTTING_KB
|
||||
from copienator.configuration import PLOTTING_KB
|
||||
except (ImportError, AttributeError):
|
||||
from default_config import PLOTTING_KB
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import sys
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
from platform_utils import windows_filename_problems
|
||||
from copienator.platform import windows_filename_problems
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -111,7 +111,7 @@ def collect_diagnostics(
|
||||
)
|
||||
)
|
||||
try:
|
||||
from config import CURRENT_SCORE_ODS_PATH, FINAL_SCORE_ODS_PATH
|
||||
from copienator.configuration import CURRENT_SCORE_ODS_PATH, FINAL_SCORE_ODS_PATH
|
||||
|
||||
for label, configured_path in (
|
||||
("ODS courant", CURRENT_SCORE_ODS_PATH),
|
||||
|
||||
+33
-33
@@ -90,8 +90,8 @@ def build_workflow(show_personal_steps: bool) -> list[StepDefinition]:
|
||||
"Analyser l’énoncé",
|
||||
"Détecte les questions, leurs groupes, le corrigé et les indications de barème.",
|
||||
(
|
||||
python("gemini", "Analyse avec Gemini", "gemini_for_enonce.py"),
|
||||
python("personal", "Alternative enonce_info.py", "enonce_info.py"),
|
||||
python("gemini", "Analyse avec Gemini", "statement"),
|
||||
python("personal", "Alternative personnelle", "statement-personal"),
|
||||
),
|
||||
arguments=(
|
||||
arg_target("Dossier de l’évaluation"),
|
||||
@@ -124,7 +124,7 @@ def build_workflow(show_personal_steps: bool) -> list[StepDefinition]:
|
||||
CommandVariant(
|
||||
"rotate",
|
||||
"Rotation",
|
||||
"copies_tools.py",
|
||||
"copies",
|
||||
"python",
|
||||
("rotate",),
|
||||
fixed_args_before_positionals=True,
|
||||
@@ -142,7 +142,7 @@ def build_workflow(show_personal_steps: bool) -> list[StepDefinition]:
|
||||
CommandVariant(
|
||||
"rename",
|
||||
"Renommage",
|
||||
"copies_tools.py",
|
||||
"copies",
|
||||
"python",
|
||||
("rename",),
|
||||
fixed_args_before_positionals=True,
|
||||
@@ -155,7 +155,7 @@ def build_workflow(show_personal_steps: bool) -> list[StepDefinition]:
|
||||
"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_splitter.py"),),
|
||||
(python("default", "Séparation des pages", "page-split"),),
|
||||
arguments=(arg_target(),),
|
||||
artifacts=("Copies", "Copies Originales"),
|
||||
),
|
||||
@@ -164,7 +164,7 @@ def build_workflow(show_personal_steps: bool) -> list[StepDefinition]:
|
||||
"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", "cutleft.py"),),
|
||||
(python("default", "Découpe", "crop-labels"),),
|
||||
arguments=(
|
||||
arg_target(),
|
||||
ArgumentSpec("fullpage", "Toujours utiliser la page entière", "bool", "--fullpage"),
|
||||
@@ -177,7 +177,7 @@ def build_workflow(show_personal_steps: bool) -> list[StepDefinition]:
|
||||
"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", "gemini_for_labels.py"),),
|
||||
(python("default", "Détection des labels", "labels"),),
|
||||
arguments=(
|
||||
arg_target(),
|
||||
ArgumentSpec("overwrite", "Régénérer les résultats", "bool", "--overwrite"),
|
||||
@@ -190,7 +190,7 @@ def build_workflow(show_personal_steps: bool) -> list[StepDefinition]:
|
||||
"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", "plotting.py"),),
|
||||
(python("default", "Vérification", "review-labels"),),
|
||||
arguments=(arg_target(),),
|
||||
requires=("labels", "Cutleft"),
|
||||
artifacts=("Copies/Copie*.json",),
|
||||
@@ -200,7 +200,7 @@ def build_workflow(show_personal_steps: bool) -> list[StepDefinition]:
|
||||
"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", "splitting_int.py"),),
|
||||
(python("default", "Découpage", "split-answers"),),
|
||||
arguments=(arg_target(),),
|
||||
requires=("Copies",),
|
||||
artifacts=("Copies/Copie*/*",),
|
||||
@@ -211,7 +211,7 @@ def build_workflow(show_personal_steps: bool) -> list[StepDefinition]:
|
||||
"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", "grouping.py"),),
|
||||
(python("default", "Regroupement", "group-answers"),),
|
||||
arguments=(arg_target("Dossier de l’évaluation"),),
|
||||
requires=("Copies",),
|
||||
artifacts=("Par label",),
|
||||
@@ -222,7 +222,7 @@ def build_workflow(show_personal_steps: bool) -> list[StepDefinition]:
|
||||
"Labels et regroupement",
|
||||
"Vérifier les groupes produits",
|
||||
"Vérifie que chaque réponse PDF apparaît dans les métadonnées des groupes.",
|
||||
(python("default", "Vérification des groupes", "verify_groups.py"),),
|
||||
(python("default", "Vérification des groupes", "verify-groups"),),
|
||||
arguments=(arg_target("Dossier de l’évaluation"),),
|
||||
optional=True,
|
||||
requires=("Copies", "Par label"),
|
||||
@@ -233,17 +233,17 @@ def build_workflow(show_personal_steps: bool) -> list[StepDefinition]:
|
||||
"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", "correction.py"),
|
||||
python("batch", "Préparer toutes les requêtes batch", "correction.py", fixed_args=("--batch",)),
|
||||
python("hybrid", "Batch à partir d’un label", "correction.py"),
|
||||
python("refaire", "Recorrection depuis refaire.json", "correction.py", fixed_args=("--refaire",)),
|
||||
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",
|
||||
"correction.py",
|
||||
"correct",
|
||||
fixed_args=("--deal-with-batched",),
|
||||
),
|
||||
python("reset", "Réinitialiser les corrections", "correction.py", fixed_args=("--reset",), dangerous=True),
|
||||
python("reset", "Réinitialiser les corrections", "correct", fixed_args=("--reset",), dangerous=True),
|
||||
),
|
||||
arguments=(
|
||||
arg_target("Évaluation ou image Group_X.jpg"),
|
||||
@@ -259,7 +259,7 @@ def build_workflow(show_personal_steps: bool) -> list[StepDefinition]:
|
||||
"Correction",
|
||||
"Envoyer les batchs",
|
||||
"Envoie à Gemini les fichiers JSONL produits par le mode batch.",
|
||||
(python("default", "Envoi", "submit_batches.py"),),
|
||||
(python("default", "Envoi", "batch-submit"),),
|
||||
arguments=(arg_target("Dossier de l’évaluation"),),
|
||||
optional=True,
|
||||
artifacts=("batch_jobs.json",),
|
||||
@@ -270,7 +270,7 @@ def build_workflow(show_personal_steps: bool) -> list[StepDefinition]:
|
||||
"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.py"),),
|
||||
(python("default", "État des batchs", "batch-status"),),
|
||||
arguments=(ArgumentSpec("download", "Télécharger le job", "text", "--download"),),
|
||||
optional=True,
|
||||
skip_for_live_correction=True,
|
||||
@@ -280,7 +280,7 @@ def build_workflow(show_personal_steps: bool) -> list[StepDefinition]:
|
||||
"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", "fetch_batched_results.py"),),
|
||||
(python("default", "Récupération", "batch-fetch"),),
|
||||
arguments=(arg_target("Dossier de l’évaluation"),),
|
||||
optional=True,
|
||||
skip_for_live_correction=True,
|
||||
@@ -290,7 +290,7 @@ def build_workflow(show_personal_steps: bool) -> list[StepDefinition]:
|
||||
"Correction",
|
||||
"Nettoyer la correction",
|
||||
"Corrige certains problèmes d’encodage et prépare le texte pour LaTeX.",
|
||||
(python("default", "Post-correction", "post-correction.py"),),
|
||||
(python("default", "Post-correction", "post-correction"),),
|
||||
arguments=(arg_target("Dossier de l’évaluation"),),
|
||||
requires=("correction.json",),
|
||||
),
|
||||
@@ -299,7 +299,7 @@ def build_workflow(show_personal_steps: bool) -> list[StepDefinition]:
|
||||
"Correction",
|
||||
"Résoudre les conflits manuels",
|
||||
"Étape conditionnelle, uniquement si manual_resolutions.txt contient des conflits à traiter.",
|
||||
(python("default", "Résolution", "resolve_manual.py"),),
|
||||
(python("default", "Résolution", "resolve-manual"),),
|
||||
arguments=(arg_target("Dossier de l’évaluation"),),
|
||||
optional=True,
|
||||
requires=("manual_resolutions.txt", "correction.json"),
|
||||
@@ -311,9 +311,9 @@ def build_workflow(show_personal_steps: bool) -> list[StepDefinition]:
|
||||
"Générer les copies annotées",
|
||||
"Les trois modes sont exclusifs pour un parcours donné.",
|
||||
(
|
||||
python("simple", "Annotations simples (Anot)", "annotating.py"),
|
||||
python("checks", "Annotations avec cases (Bnot)", "annotating_with_checks.py"),
|
||||
python("grouped", "Annotations groupées (BGnot)", "annotating_by_label.py"),
|
||||
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"),
|
||||
@@ -328,7 +328,7 @@ def build_workflow(show_personal_steps: bool) -> list[StepDefinition]:
|
||||
"Génération des annotations",
|
||||
"Exporter",
|
||||
"Exporte les annotations vers le dossier EXPORT_DIR défini dans config.py.",
|
||||
(python("default", "Export", "export.py"),),
|
||||
(python("default", "Export", "export"),),
|
||||
arguments=(
|
||||
arg_target("Dossier de l’évaluation"),
|
||||
ArgumentSpec(
|
||||
@@ -355,7 +355,7 @@ def build_workflow(show_personal_steps: bool) -> list[StepDefinition]:
|
||||
"Correction manuscrite",
|
||||
"Importer les annotations manuscrites",
|
||||
"Copie les PDF présents dans IMPORT_DIR vers l’évaluation.",
|
||||
(python("default", "Import", "import.py"),),
|
||||
(python("default", "Import", "import"),),
|
||||
arguments=(
|
||||
arg_target("Dossier de l’évaluation"),
|
||||
ArgumentSpec(
|
||||
@@ -375,8 +375,8 @@ def build_workflow(show_personal_steps: bool) -> list[StepDefinition]:
|
||||
"Lire les annotations manuscrites",
|
||||
"Le mode doit correspondre au mode choisi lors de la génération des annotations.",
|
||||
(
|
||||
python("standard", "Lecture Bnot", "reading_annotations.py"),
|
||||
python("grouped", "Lecture BGnot", "reading_grouped_annotations.py"),
|
||||
python("standard", "Lecture Bnot", "read-annotations"),
|
||||
python("grouped", "Lecture BGnot", "read-grouped"),
|
||||
),
|
||||
arguments=(
|
||||
arg_target("Dossier de l’évaluation"),
|
||||
@@ -389,7 +389,7 @@ def build_workflow(show_personal_steps: bool) -> list[StepDefinition]:
|
||||
"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.py"),),
|
||||
(python("default", "Attribution des noms", "giving-names"),),
|
||||
arguments=(
|
||||
arg_target("Dossier de l’évaluation"),
|
||||
ArgumentSpec(
|
||||
@@ -424,7 +424,7 @@ def build_workflow(show_personal_steps: bool) -> list[StepDefinition]:
|
||||
"É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.py"),),
|
||||
(python("default", "Mise à jour ODS", "update-ods"),),
|
||||
arguments=(
|
||||
arg_target("Dossier de l’évaluation"),
|
||||
ArgumentSpec("sum", "Écrire seulement la somme", "bool", "--sum"),
|
||||
@@ -452,7 +452,7 @@ def build_workflow(show_personal_steps: bool) -> list[StepDefinition]:
|
||||
"Étapes personnelles",
|
||||
"Ajouter le score final",
|
||||
"Génère les fichiers de diffusion avec le score final.",
|
||||
(python("default", "Score final", "add_final_score.py"),),
|
||||
(python("default", "Score final", "add-final-score"),),
|
||||
arguments=(arg_target("Dossier de l’évaluation"),),
|
||||
personal=True,
|
||||
),
|
||||
@@ -494,7 +494,7 @@ def build_command(
|
||||
|
||||
program_path = repository / variant.program
|
||||
if variant.kind == "python":
|
||||
command = [sys.executable, "-u", str(program_path)]
|
||||
command = [sys.executable, "-u", "-m", "copienator", variant.program]
|
||||
elif variant.kind == "shell":
|
||||
command = [str(program_path)]
|
||||
else:
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
[build-system]
|
||||
requires = ["setuptools>=68"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "copienator"
|
||||
version = "0.1.0"
|
||||
description = "Workflow assistant for preparing and correcting scanned exams"
|
||||
requires-python = ">=3.11"
|
||||
|
||||
[project.scripts]
|
||||
copienator = "copienator.dispatcher:main"
|
||||
copienator-gui = "copienator_gui.__main__:main"
|
||||
|
||||
[tool.setuptools]
|
||||
py-modules = ["default_config"]
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
include = ["copienator*", "copienator_gui*"]
|
||||
|
||||
[tool.setuptools.package-data]
|
||||
copienator = ["data/*.txt"]
|
||||
+50
-27
@@ -1,6 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import importlib
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
@@ -31,6 +31,7 @@ from copienator import (
|
||||
from copienator.annotation_actions import apply_checkbox_actions, apply_score_overrides
|
||||
from copienator.annotation_data import AnnotationLoadResult, load_annotation_data
|
||||
from copienator.filesystem import staged_directory, staged_files
|
||||
from copienator.dispatcher import COMMANDS, main as dispatcher_main
|
||||
from copienator_gui.app import (
|
||||
CopienatorApp,
|
||||
DEFAULT_HTTPS_PROXY,
|
||||
@@ -45,8 +46,8 @@ from copienator_gui.diagnostics import collect_diagnostics
|
||||
from copienator_gui.runner import ProcessRunner
|
||||
from copienator_gui.state import StateStore
|
||||
from copienator_gui.workflow import build_command, build_workflow, evaluation_argument
|
||||
from copies_tools import rename_all, rotate_all
|
||||
from platform_utils import (
|
||||
from copienator.commands.copies_tools import rename_all, rotate_all
|
||||
from copienator.platform import (
|
||||
WindowsLabelError,
|
||||
replace_with_link_or_copy,
|
||||
safe_filename,
|
||||
@@ -57,18 +58,16 @@ from platform_utils import (
|
||||
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)
|
||||
sys.modules[module_name] = module
|
||||
try:
|
||||
spec.loader.exec_module(module)
|
||||
except Exception:
|
||||
sys.modules.pop(module_name, None)
|
||||
raise
|
||||
return module
|
||||
def load_script_module(filename: str, _module_name: str):
|
||||
stem = Path(filename).stem.replace("-", "_")
|
||||
if stem == "import":
|
||||
stem = "import_annotations"
|
||||
return importlib.import_module(f"copienator.commands.{stem}")
|
||||
|
||||
|
||||
def command_arguments(command: list[str]) -> list[str]:
|
||||
assert command[2:4] == ["-m", "copienator"]
|
||||
return command[5:]
|
||||
|
||||
|
||||
class WorkspaceTests(unittest.TestCase):
|
||||
@@ -129,6 +128,24 @@ class WorkspaceTests(unittest.TestCase):
|
||||
self.assertEqual(workspace.names_file(), root / "names")
|
||||
|
||||
|
||||
class DispatcherTests(unittest.TestCase):
|
||||
def test_unknown_command_has_argument_error_code(self) -> None:
|
||||
with redirect_stderr(io.StringIO()):
|
||||
self.assertEqual(dispatcher_main(["does-not-exist"]), 2)
|
||||
|
||||
def test_every_gui_python_command_is_registered(self) -> None:
|
||||
for step in build_workflow(True):
|
||||
for variant in step.variants:
|
||||
if variant.kind == "python":
|
||||
with self.subTest(step=step.id, command=variant.program):
|
||||
self.assertIn(variant.program, COMMANDS)
|
||||
|
||||
def test_root_contains_only_configuration_python_modules(self) -> None:
|
||||
root_modules = {path.name for path in REPOSITORY.glob("*.py")}
|
||||
self.assertIn("default_config.py", root_modules)
|
||||
self.assertLessEqual(root_modules, {"config.py", "default_config.py"})
|
||||
|
||||
|
||||
class AtomicJsonTests(unittest.TestCase):
|
||||
def test_atomic_binary_round_trip(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
@@ -561,7 +578,9 @@ class StandardCliTests(unittest.TestCase):
|
||||
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:])
|
||||
parsed = self.modules[module_name].build_parser().parse_args(
|
||||
command_arguments(command)
|
||||
)
|
||||
parsed_path = getattr(parsed, "evaluation", None) or parsed.target
|
||||
self.assertEqual(str(parsed_path), evaluation)
|
||||
|
||||
@@ -577,7 +596,7 @@ class StandardCliTests(unittest.TestCase):
|
||||
)
|
||||
with self.subTest(script=f"copies_tools:{step_id}"):
|
||||
parsed = self.modules["copies_tools"].build_parser().parse_args(
|
||||
command[3:]
|
||||
command_arguments(command)
|
||||
)
|
||||
self.assertEqual(parsed.operation, step_id)
|
||||
self.assertEqual(str(parsed.evaluation), evaluation)
|
||||
@@ -1521,7 +1540,7 @@ class WorkflowTests(unittest.TestCase):
|
||||
"default",
|
||||
{"target": self.evaluation, "annotation_dir": "Bnot"},
|
||||
)
|
||||
self.assertEqual(command[3:], [self.evaluation, "Bnot"])
|
||||
self.assertEqual(command_arguments(command), [self.evaluation, "Bnot"])
|
||||
|
||||
def test_copy_listing_prefers_processed_copies(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
@@ -1607,27 +1626,31 @@ class WorkflowTests(unittest.TestCase):
|
||||
"live",
|
||||
{"target": self.evaluation, "overwrite": True, "limit": "0"},
|
||||
)
|
||||
self.assertEqual(command[3:], [self.evaluation, "--overwrite", "--limit", "0"])
|
||||
self.assertIn("correction.py", command[2])
|
||||
self.assertEqual(
|
||||
command_arguments(command),
|
||||
[self.evaluation, "--overwrite", "--limit", "0"],
|
||||
)
|
||||
self.assertEqual(command[4], "correct")
|
||||
|
||||
def test_hybrid_correction_arguments(self) -> None:
|
||||
command = self.command(
|
||||
"correction", "hybrid", {"target": self.evaluation, "batch_from": "Ex 4"}
|
||||
)
|
||||
self.assertEqual(command[3:], [self.evaluation, "--batch-from", "Ex 4"])
|
||||
self.assertEqual(
|
||||
command_arguments(command), [self.evaluation, "--batch-from", "Ex 4"]
|
||||
)
|
||||
|
||||
def test_annotation_variants_are_exclusive_commands(self) -> None:
|
||||
command = self.command(
|
||||
"annotation", "grouped", {"target": self.evaluation, "overwrite": True}
|
||||
)
|
||||
self.assertIn("annotating_by_label.py", command[2])
|
||||
self.assertNotIn("annotating.py", command[2])
|
||||
self.assertEqual(command[4], "annotate-grouped")
|
||||
|
||||
def test_copy_preparation_commands_are_python(self) -> None:
|
||||
rotate = self.command("rotate", "rotate", {"target": self.evaluation})
|
||||
rename = self.command("rename", "rename", {"target": self.evaluation})
|
||||
self.assertEqual(rotate[2:], [str(REPOSITORY / "copies_tools.py"), "rotate", self.evaluation])
|
||||
self.assertEqual(rename[2:], [str(REPOSITORY / "copies_tools.py"), "rename", self.evaluation])
|
||||
self.assertEqual(rotate[2:], ["-m", "copienator", "copies", "rotate", self.evaluation])
|
||||
self.assertEqual(rename[2:], ["-m", "copienator", "copies", "rename", self.evaluation])
|
||||
|
||||
|
||||
class CrossPlatformFileTests(unittest.TestCase):
|
||||
@@ -1665,8 +1688,8 @@ class CrossPlatformFileTests(unittest.TestCase):
|
||||
source = root / "source.txt"
|
||||
destination = root / "destination.txt"
|
||||
source.write_text("content", encoding="utf-8")
|
||||
with patch("platform_utils.os.link", side_effect=OSError), patch(
|
||||
"platform_utils.os.symlink", side_effect=OSError
|
||||
with patch("copienator.platform.os.link", side_effect=OSError), patch(
|
||||
"copienator.platform.os.symlink", side_effect=OSError
|
||||
):
|
||||
method = replace_with_link_or_copy(source, destination)
|
||||
self.assertEqual(method, "copy")
|
||||
|
||||
Reference in New Issue
Block a user