Spaces:
Running
Running
| """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 | |
| def health(): | |
| return "ok" | |
| 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 | |