lustat-qmd-render / server.py
hdjebar's picture
sync render-service depuis 1ee41f3
28d455b verified
Raw
History Blame Contribute Delete
2.29 kB
"""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