From 90982e71c9041755833245137e872fca34f55a28 Mon Sep 17 00:00:00 2001 From: Artemia Date: Sun, 18 Oct 2026 16:38:01 +0200 Subject: [PATCH] =?UTF-8?q?Ajoute=20l'assistant=20de=20g=C3=A9n=C3=A9ratio?= =?UTF-8?q?n=20des=20documents=20(scripts/assistant.py)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Vérifie ou installe Quarto (winget, Homebrew, paquet officiel), prépare le venv des schémas (réinstallé seulement si requirements.txt change), propose un menu (PDF, ODT, HTML ; tous les supports ou un module ; régénération optionnelle des schémas), génère et ouvre le dossier de sortie. Bibliothèque standard uniquement. Co-Authored-By: Claude Opus 5.5 --- scripts/assistant.py | 263 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 263 insertions(+) create mode 100644 scripts/assistant.py diff --git a/scripts/assistant.py b/scripts/assistant.py new file mode 100644 index 0000000..bebe22d --- /dev/null +++ b/scripts/assistant.py @@ -0,0 +1,263 @@ +"""Assistant de génération des supports de la formation (Windows, macOS, Linux). + +Lancé par Generer-documents.bat / Generer-documents.command / generer-documents.sh. +N'utilise que la bibliothèque standard de Python. + +Étapes : vérifier (ou installer) Quarto, préparer l'environnement Python des schémas, +demander quoi générer, lancer Quarto, ouvrir le dossier des documents produits. +""" +import hashlib +import json +import os +import platform +import re +import shutil +import subprocess +import sys +import tempfile +import time +import urllib.request +import webbrowser +from pathlib import Path + +RACINE = Path(__file__).resolve().parent.parent +MODULES = RACINE / "modules" +SORTIE = RACINE / "_output" +VENV = RACINE / ".venv" +EXIGENCES = RACINE / "schemas" / "requirements.txt" +PAGE_QUARTO = "https://quarto.org/docs/get-started/" +SYSTEME = platform.system() # "Windows", "Darwin" (macOS) ou "Linux" + +FORMATS = [ # (libellé, option --to de Quarto, extension produite) + ("PDF", "typst", ".pdf"), + ("LibreOffice (.odt)", "odt", ".odt"), + ("Site web (HTML)", "html", ".html"), +] + + +# --- Dialogue ---------------------------------------------------------------- + +def titre(texte): + print("\n" + "=" * 64 + f"\n {texte}\n" + "=" * 64) + + +def oui_non(question, defaut=True): + suffixe = " [O/n] " if defaut else " [o/N] " + while True: + reponse = input(question + suffixe).strip().lower() + if not reponse: + return defaut + if reponse in ("o", "oui", "y", "yes"): + return True + if reponse in ("n", "non", "no"): + return False + print(" Répondez par o (oui) ou n (non).") + + +def choisir(question, options, defaut=1): + """Affiche une liste numérotée et renvoie l'index (0-based) choisi.""" + print(f"\n{question}") + for i, option in enumerate(options, 1): + print(f" {i:>2}. {option}" + (" (par défaut)" if i == defaut else "")) + while True: + reponse = input(f"Votre choix [{defaut}] : ").strip() + if not reponse: + return defaut - 1 + if reponse.isdigit() and 1 <= int(reponse) <= len(options): + return int(reponse) - 1 + print(f" Tapez un nombre entre 1 et {len(options)}.") + + +def attendre(message="Appuyez sur Entrée pour continuer..."): + try: + input(message) + except EOFError: + pass + + +# --- Quarto -------------------------------------------------------------------- + +def trouver_quarto(): + trouve = shutil.which("quarto") + if trouve: + return trouve + candidats = { + "Windows": [Path(os.environ.get("ProgramFiles", r"C:\Program Files")) / "Quarto/bin/quarto.exe", + Path(os.environ.get("LOCALAPPDATA", "")) / "Programs/Quarto/bin/quarto.exe"], + "Darwin": [Path("/Applications/quarto/bin/quarto"), Path("/usr/local/bin/quarto"), + Path("/opt/homebrew/bin/quarto")], + "Linux": [Path("/opt/quarto/bin/quarto"), Path("/usr/local/bin/quarto"), + Path.home() / ".local/bin/quarto"], + }.get(SYSTEME, []) + for chemin in candidats: + if chemin.is_file(): + return str(chemin) + return None + + +def telecharger_quarto(motif): + """Télécharge le fichier d'installation de la dernière version de Quarto dont le nom contient `motif`.""" + with urllib.request.urlopen("https://api.github.com/repos/quarto-dev/quarto-cli/releases/latest", + timeout=30) as reponse: + version = json.load(reponse) + fichier = next(a for a in version["assets"] if a["name"].endswith(motif)) + destination = Path(tempfile.gettempdir()) / fichier["name"] + print(f" Téléchargement de {fichier['name']} ({fichier['size'] // 1_000_000} Mo)...") + urllib.request.urlretrieve(fichier["browser_download_url"], destination) + return destination + + +def installer_quarto(): + try: + if SYSTEME == "Windows": + if shutil.which("winget"): + subprocess.run(["winget", "install", "-e", "--id", "Posit.Quarto", + "--accept-source-agreements", "--accept-package-agreements"]) + else: + subprocess.run(["msiexec", "/i", str(telecharger_quarto("win.msi"))]) + elif SYSTEME == "Darwin": + if shutil.which("brew"): + subprocess.run(["brew", "install", "--cask", "quarto"]) + else: + print(" L'installateur va s'ouvrir : suivez ses étapes puis revenez ici.") + subprocess.run(["open", "-W", str(telecharger_quarto("macos.pkg"))]) + elif SYSTEME == "Linux" and shutil.which("apt-get"): + arch = "arm64" if platform.machine().lower() in ("aarch64", "arm64") else "amd64" + paquet = telecharger_quarto(f"linux-{arch}.deb") + print(" Votre mot de passe peut être demandé (installation système).") + subprocess.run(["sudo", "apt-get", "install", "-y", str(paquet)]) + else: + return False + except Exception as erreur: # réseau indisponible, fichier introuvable… + print(f" L'installation automatique a échoué : {erreur}") + return False + return True + + +def verifier_quarto(): + titre("1/3 Quarto (outil de mise en page)") + quarto = trouver_quarto() + if quarto: + print(f" Quarto est installé : {quarto}") + return quarto + print(" Quarto n'est pas installé. Il est indispensable pour produire les documents.") + if oui_non(" L'installer automatiquement maintenant ?"): + installer_quarto() + quarto = trouver_quarto() + while not quarto: + print(f"\n Installez Quarto depuis la page qui va s'ouvrir : {PAGE_QUARTO}") + webbrowser.open(PAGE_QUARTO) + attendre(" Une fois l'installation terminée, appuyez sur Entrée...") + quarto = trouver_quarto() + if not quarto and not oui_non(" Quarto reste introuvable. Réessayer ?"): + raise SystemExit(1) + print(f" Quarto est prêt : {quarto}") + return quarto + + +# --- Environnement Python des schémas ---------------------------------------------- + +def python_du_venv(): + return VENV / ("Scripts/python.exe" if SYSTEME == "Windows" else "bin/python") + + +def preparer_venv(): + titre("2/3 Environnement Python (schémas)") + empreinte = hashlib.sha256(EXIGENCES.read_bytes()).hexdigest() + temoin = VENV / ".empreinte-requirements" + if python_du_venv().exists() and temoin.exists() and temoin.read_text().strip() == empreinte: + print(" Déjà prêt.") + return True + try: + if not python_du_venv().exists(): + print(" Création de l'environnement virtuel .venv ...") + subprocess.run([sys.executable, "-m", "venv", str(VENV)], check=True) + print(" Installation des bibliothèques (la première fois, cela peut prendre quelques minutes)...") + subprocess.run([str(python_du_venv()), "-m", "pip", "install", "--disable-pip-version-check", + "-q", "-r", str(EXIGENCES)], check=True) + except subprocess.CalledProcessError: + print(" Impossible de préparer l'environnement Python (connexion Internet ?).") + print(" Ce n'est pas bloquant : les documents peuvent être produits avec les schémas existants.") + return False + temoin.write_text(empreinte) + print(" Environnement prêt.") + return True + + +# --- Génération -------------------------------------------------------------------- + +def lister_supports(): + supports = [] + for qmd in sorted(MODULES.glob("*.qmd")): + m = re.search(r'^title:\s*"?(.*?)"?\s*$', qmd.read_text(encoding="utf-8"), re.M) + supports.append((qmd, m.group(1) if m else qmd.stem)) + return supports + + +def ouvrir_dossier(dossier): + try: + if SYSTEME == "Windows": + os.startfile(dossier) + elif SYSTEME == "Darwin": + subprocess.run(["open", str(dossier)]) + else: + subprocess.run(["xdg-open", str(dossier)]) + except Exception: + pass + + +def generer(quarto, venv_ok): + titre("3/3 Génération des documents") + i_format = choisir("Quel format ?", [f[0] for f in FORMATS]) + libelle, cible, extension = FORMATS[i_format] + supports = lister_supports() + i_portee = choisir("Quels supports ?", ["Tous les supports"] + [t for _, t in supports]) + if venv_ok and oui_non("\nRégénérer aussi les schémas (utile seulement si vous les avez modifiés) ?", + defaut=False): + subprocess.run([str(python_du_venv()), str(RACINE / "schemas" / "generer.py")], cwd=RACINE) + + debut = time.time() + if i_portee == 0: + print(f"\nProduction de tous les supports en {libelle} : patientez...\n") + commandes = [[quarto, "render", "--to", cible]] + else: + qmd = supports[i_portee - 1][0] + print(f"\nProduction de « {supports[i_portee - 1][1]} » en {libelle}...\n") + commandes = [[quarto, "render", str(qmd.relative_to(RACINE)), "--to", cible]] + for commande in commandes: + if subprocess.run(commande, cwd=RACINE).returncode != 0: + print("\n*** La génération a échoué. Le message d'erreur se trouve juste au-dessus. ***") + return False + + produits = sorted(p for p in SORTIE.rglob(f"*{extension}") if p.stat().st_mtime >= debut - 1) + titre("Terminé") + for p in produits: + print(f" {p.relative_to(RACINE)}") + print(f"\n {len(produits)} document(s) dans le dossier : {SORTIE}") + ouvrir_dossier(SORTIE / "modules" if (SORTIE / "modules").is_dir() else SORTIE) + return True + + +def main(): + print("Assistant de génération — Formation ESP32-S3 avec Arduino Framework") + if sys.version_info < (3, 9): + print("Python 3.9 ou plus récent est nécessaire.") + raise SystemExit(1) + quarto = verifier_quarto() + venv_ok = preparer_venv() + while True: + generer(quarto, venv_ok) + if not oui_non("\nGénérer d'autres documents ?", defaut=False): + break + + +if __name__ == "__main__": + try: + main() + except KeyboardInterrupt: + print("\nInterrompu.") + except SystemExit: + raise + except Exception as erreur: + print(f"\nErreur inattendue : {erreur}") + raise SystemExit(1)