Windows compatibility
This commit is contained in:
@@ -9,6 +9,9 @@ from pathlib import Path
|
||||
from tkinter import filedialog, messagebox, ttk
|
||||
from typing import Any
|
||||
|
||||
from platform_utils import WindowsLabelError, validate_windows_labels
|
||||
|
||||
from .diagnostics import collect_diagnostics
|
||||
from .runner import ProcessRunner
|
||||
from .state import StateStore
|
||||
from .workflow import (
|
||||
@@ -42,6 +45,7 @@ class CopienatorApp(tk.Tk):
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.repository = repository.resolve()
|
||||
self.show_personal_steps = show_personal_steps
|
||||
self.steps = build_workflow(show_personal_steps)
|
||||
self.step_by_id = {step.id: step for step in self.steps}
|
||||
self.state_store = StateStore()
|
||||
@@ -93,6 +97,7 @@ class CopienatorApp(tk.Tk):
|
||||
path_entry.bind("<Return>", lambda _event: self._load_evaluation())
|
||||
ttk.Button(top, text="Parcourir…", command=self._browse_evaluation).grid(row=0, column=2, padx=6)
|
||||
ttk.Button(top, text="Charger", command=self._load_evaluation).grid(row=0, column=3)
|
||||
ttk.Button(top, text="Diagnostic…", command=self._show_diagnostics).grid(row=0, column=4, padx=(6, 0))
|
||||
|
||||
ttk.Label(top, text="Clé Gemini").grid(row=1, column=0, sticky="w", padx=(0, 8), pady=(7, 0))
|
||||
ttk.Entry(top, textvariable=self.api_key_var, show="•", width=32).grid(
|
||||
@@ -194,6 +199,47 @@ class CopienatorApp(tk.Tk):
|
||||
status = ttk.Label(self, textvariable=self.info_var, anchor="w", relief="sunken", padding=(6, 3))
|
||||
status.grid(row=3, column=0, sticky="ew")
|
||||
|
||||
def _show_diagnostics(self) -> None:
|
||||
checks = collect_diagnostics(
|
||||
self.show_personal_steps, self.api_key_var.get(), self.evaluation
|
||||
)
|
||||
dialog = tk.Toplevel(self)
|
||||
dialog.title("Diagnostic Copienator")
|
||||
dialog.geometry("760x480")
|
||||
dialog.transient(self)
|
||||
dialog.columnconfigure(0, weight=1)
|
||||
dialog.rowconfigure(1, weight=1)
|
||||
|
||||
required_missing = sum(1 for check in checks if check.required and not check.ok)
|
||||
summary = (
|
||||
"Tous les prérequis obligatoires sont disponibles."
|
||||
if required_missing == 0
|
||||
else f"{required_missing} prérequis obligatoire(s) manquant(s)."
|
||||
)
|
||||
ttk.Label(dialog, text=summary, padding=10, font=("TkDefaultFont", 11, "bold")).grid(
|
||||
row=0, column=0, sticky="w"
|
||||
)
|
||||
|
||||
tree = ttk.Treeview(dialog, columns=("state", "need", "detail"), show="headings")
|
||||
tree.heading("state", text="État")
|
||||
tree.heading("need", text="Niveau")
|
||||
tree.heading("detail", text="Composant et détail")
|
||||
tree.column("state", width=85, anchor="center")
|
||||
tree.column("need", width=100, anchor="center")
|
||||
tree.column("detail", width=530)
|
||||
for check in checks:
|
||||
tree.insert(
|
||||
"",
|
||||
"end",
|
||||
values=(
|
||||
"OK" if check.ok else "Manquant",
|
||||
"Obligatoire" if check.required else "Optionnel",
|
||||
f"{check.name} — {check.detail}",
|
||||
),
|
||||
)
|
||||
tree.grid(row=1, column=0, sticky="nsew", padx=10)
|
||||
ttk.Button(dialog, text="Fermer", command=dialog.destroy).grid(row=2, column=0, pady=10)
|
||||
|
||||
def _browse_evaluation(self) -> None:
|
||||
selected = filedialog.askdirectory(initialdir=self.evaluation_var.get() or self.repository)
|
||||
if selected:
|
||||
@@ -482,6 +528,19 @@ class CopienatorApp(tk.Tk):
|
||||
if step.is_manual:
|
||||
self._mark_step("success")
|
||||
return
|
||||
if os.name == "nt" and step.id != "statement":
|
||||
labels_path = evaluation / "labels"
|
||||
if labels_path.is_file():
|
||||
labels = [
|
||||
line.strip()
|
||||
for line in labels_path.read_text(encoding="utf-8").splitlines()
|
||||
if line.strip()
|
||||
]
|
||||
try:
|
||||
validate_windows_labels(labels)
|
||||
except WindowsLabelError as exc:
|
||||
messagebox.showerror("Labels incompatibles avec Windows", str(exc))
|
||||
return
|
||||
if not self._validate_arguments():
|
||||
return
|
||||
missing = self._missing_requirements(step)
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
from platform_utils import windows_filename_problems
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class DiagnosticCheck:
|
||||
name: str
|
||||
ok: bool
|
||||
detail: str
|
||||
required: bool = True
|
||||
|
||||
|
||||
def module_available(module: str) -> bool:
|
||||
try:
|
||||
return importlib.util.find_spec(module) is not None
|
||||
except (ImportError, ModuleNotFoundError, ValueError):
|
||||
return False
|
||||
|
||||
|
||||
def collect_diagnostics(
|
||||
show_personal_steps: bool,
|
||||
api_key_override: str = "",
|
||||
evaluation: Path | None = None,
|
||||
) -> list[DiagnosticCheck]:
|
||||
checks = [
|
||||
DiagnosticCheck(
|
||||
"Système",
|
||||
os.name == "nt" or sys.platform.startswith("linux"),
|
||||
f"{sys.platform} — Linux et Windows sont pris en charge",
|
||||
),
|
||||
DiagnosticCheck("Python", True, sys.executable),
|
||||
]
|
||||
|
||||
modules = {
|
||||
"numpy": "numpy",
|
||||
"pandas": "pandas",
|
||||
"matplotlib": "matplotlib",
|
||||
"Pillow": "PIL",
|
||||
"pydantic": "pydantic",
|
||||
"pypdf": "pypdf",
|
||||
"pdf2image": "pdf2image",
|
||||
"reportlab": "reportlab",
|
||||
"img2pdf": "img2pdf",
|
||||
"PyMuPDF": "fitz",
|
||||
"ftfy": "ftfy",
|
||||
"ezodf": "ezodf",
|
||||
"Google GenAI": "google.genai",
|
||||
}
|
||||
checks.extend(
|
||||
DiagnosticCheck(label, module_available(module), f"Module Python : {module}")
|
||||
for label, module in modules.items()
|
||||
)
|
||||
|
||||
programs = (
|
||||
("Poppler", ("pdftoppm", "pdftocairo"), True),
|
||||
("LaTeX", ("pdflatex",), True),
|
||||
("PDF Arranger", ("pdf-arranger", "pdfarranger"), False),
|
||||
)
|
||||
for label, candidates, required in programs:
|
||||
executable = next((shutil.which(candidate) for candidate in candidates if shutil.which(candidate)), None)
|
||||
detail = executable or "Introuvable dans PATH"
|
||||
checks.append(DiagnosticCheck(label, executable is not None, detail, required))
|
||||
|
||||
api_key = api_key_override.strip() or os.environ.get("GEMINI_API_KEY")
|
||||
checks.append(
|
||||
DiagnosticCheck(
|
||||
"Clé Gemini",
|
||||
bool(api_key),
|
||||
"GEMINI_API_KEY est définie" if api_key else "À saisir dans le GUI ou dans l’environnement",
|
||||
)
|
||||
)
|
||||
|
||||
if evaluation and (evaluation / "labels").is_file():
|
||||
labels = [
|
||||
line.strip()
|
||||
for line in (evaluation / "labels").read_text(encoding="utf-8").splitlines()
|
||||
if line.strip()
|
||||
]
|
||||
unsafe = [label for label in labels if windows_filename_problems(label)]
|
||||
detail = (
|
||||
"Tous les labels sont utilisables comme noms de fichiers"
|
||||
if not unsafe
|
||||
else "Caractères Windows interdits : " + ", ".join(unsafe[:3])
|
||||
)
|
||||
if len(unsafe) > 3:
|
||||
detail += f" (+{len(unsafe) - 3})"
|
||||
checks.append(
|
||||
DiagnosticCheck(
|
||||
"Labels compatibles Windows",
|
||||
not unsafe,
|
||||
detail,
|
||||
os.name == "nt",
|
||||
)
|
||||
)
|
||||
|
||||
if show_personal_steps:
|
||||
checks.append(
|
||||
DiagnosticCheck(
|
||||
"gestion_classe",
|
||||
shutil.which("gestion_classe") is not None,
|
||||
shutil.which("gestion_classe") or "Introuvable dans PATH",
|
||||
False,
|
||||
)
|
||||
)
|
||||
try:
|
||||
from config import CURRENT_SCORE_ODS_PATH, FINAL_SCORE_ODS_PATH
|
||||
|
||||
for label, configured_path in (
|
||||
("ODS courant", CURRENT_SCORE_ODS_PATH),
|
||||
("ODS final", FINAL_SCORE_ODS_PATH),
|
||||
):
|
||||
path = Path(configured_path).expanduser()
|
||||
checks.append(DiagnosticCheck(label, path.is_file(), str(path), False))
|
||||
except ImportError:
|
||||
checks.append(DiagnosticCheck("Configuration personnelle", False, "Variables absentes", False))
|
||||
|
||||
return checks
|
||||
@@ -30,6 +30,7 @@ class CommandVariant:
|
||||
program: str | None
|
||||
kind: str = "python" # python, shell, external, manual
|
||||
fixed_args: tuple[str, ...] = ()
|
||||
fixed_args_before_positionals: bool = False
|
||||
dangerous: bool = False
|
||||
|
||||
|
||||
@@ -116,7 +117,16 @@ def build_workflow(show_personal_steps: bool) -> list[StepDefinition]:
|
||||
"Prétraitement des copies",
|
||||
"Retourner toutes les copies",
|
||||
"Rotation facultative de 180° lorsque les scans sont à l’envers.",
|
||||
(CommandVariant("rotate", "Rotation", "rotate_all.sh", "shell"),),
|
||||
(
|
||||
CommandVariant(
|
||||
"rotate",
|
||||
"Rotation",
|
||||
"copies_tools.py",
|
||||
"python",
|
||||
("rotate",),
|
||||
fixed_args_before_positionals=True,
|
||||
),
|
||||
),
|
||||
arguments=(arg_target("Dossier de l’évaluation"),),
|
||||
optional=True,
|
||||
),
|
||||
@@ -125,7 +135,16 @@ def build_workflow(show_personal_steps: bool) -> list[StepDefinition]:
|
||||
"Prétraitement des copies",
|
||||
"Renommer les PDF en CopieXX.pdf",
|
||||
"Renomme les PDF du dossier d’évaluation dans leur ordre courant.",
|
||||
(CommandVariant("rename", "Renommage", "rename_to_copie.sh", "shell"),),
|
||||
(
|
||||
CommandVariant(
|
||||
"rename",
|
||||
"Renommage",
|
||||
"copies_tools.py",
|
||||
"python",
|
||||
("rename",),
|
||||
fixed_args_before_positionals=True,
|
||||
),
|
||||
),
|
||||
arguments=(arg_target("Dossier de l’évaluation"),),
|
||||
),
|
||||
StepDefinition(
|
||||
@@ -464,8 +483,11 @@ def build_command(
|
||||
elif spec.flag:
|
||||
options.extend((spec.flag, rendered))
|
||||
|
||||
if variant.fixed_args_before_positionals:
|
||||
command.extend(variant.fixed_args)
|
||||
command.extend(positionals)
|
||||
command.extend(variant.fixed_args)
|
||||
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"))
|
||||
|
||||
Reference in New Issue
Block a user