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 <noreply@anthropic.com>
264 lines
10 KiB
Python
264 lines
10 KiB
Python
"""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)
|