"""Micro-service de rendu Quarto. Entrée : .qmd (corps de la requête). Sortie : HTML. POST /render corps = texte du .qmd -> text/html (rendu, autonome) GET /health -> "ok" Le .qmd doit avoir `embed-resources: true` (HTML autonome) — c'est le cas des .qmd produits par le générateur. Le rendu EXÉCUTE le code (Python/geopandas/PySAL), ce que Netlify ne peut pas faire : c'est tout l'intérêt de ce service. """ import os, pathlib, subprocess, tempfile from flask import Flask, request, Response app = Flask(__name__) # Le rendu EXÉCUTE du code : `/render` exécute du Python via Quarto. Il ne doit JAMAIS être public. # - secret partagé RENDER_TOKEN (envoyé par Netlify dans l'en-tête X-Render-Token) ; fail-closed. # - plus de CORS `*` (la réponse est consommée côté serveur par Netlify, pas par un navigateur). # - taille de corps bornée (un .qmd généré reste petit). RENDER_TOKEN = os.environ.get("RENDER_TOKEN", "") app.config["MAX_CONTENT_LENGTH"] = 1 * 1024 * 1024 # 1 Mo : un .qmd légitime est bien en deçà def _authorized() -> bool: # Fail-closed : si RENDER_TOKEN n'est pas configuré, on refuse tout (pas d'ouverture par défaut). return bool(RENDER_TOKEN) and request.headers.get("X-Render-Token", "") == RENDER_TOKEN @app.get("/health") def health(): return "ok" @app.post("/render") def render(): if not _authorized(): return Response("forbidden", status=403, mimetype="text/plain") qmd = request.get_data(as_text=True) if not qmd.strip(): return Response("corps .qmd vide", status=400, mimetype="text/plain") with tempfile.TemporaryDirectory() as d: src = pathlib.Path(d) / "doc.qmd" src.write_text(qmd, encoding="utf-8") proc = subprocess.run( ["quarto", "render", "doc.qmd", "--to", "html", "--output", "doc.html"], cwd=d, capture_output=True, text=True, timeout=600, ) out = pathlib.Path(d) / "doc.html" if proc.returncode != 0 or not out.exists(): return Response("erreur de rendu:\n" + proc.stderr[-4000:], status=500, mimetype="text/plain") html = out.read_text(encoding="utf-8") return Response(html, mimetype="text/html") # pas d'en-tête CORS : consommé côté serveur