"""Warstwa RENDER — usługa składająca raport PDF (PRE-24). OSOBNA usługa, nie część prezentacji, z tego samego powodu co silnik swisseph (LOG-27): TeX Live waży setki megabajtów. Trzymanie go w obrazie produktu spowalniałoby każdy build i deploy, a tak aktualizuje się niezależnie i awaria renderu nie kładzie aplikacji. Rozmawia TYLKO z prezentacją, po szyfrowanym łączu (własny, trzeci klucz — dostaje cały raport: dane urodzeniowe i opisy z baz). W klastrze wystawiona jako ClusterIP, bez wejścia z zewnątrz. Świadomie nie zapisuje niczego na dysk poza katalogiem tymczasowym, który sprząta po sobie — raport nie może zostawiać śladu w kontenerze. """ from __future__ import annotations import logging from fastapi import FastAPI, HTTPException from fastapi.responses import Response from app import link_crypto, security from app.compile import RenderError, to_pdf, toolchain_ready log = logging.getLogger("astrololo.render") app = FastAPI(title="astrololo · usługa render (PDF)") security.install(app, "render") # token międzywarstwowy (LOG-32) # Szyfrowanie łącza od prezentacji. PO security.install, żeby także odmowa # tokenowa wracała zaszyfrowana — inaczej klient nie umiałby jej odczytać. link_crypto.install(app, link_crypto.ENV_PRESENTATION_RENDER, "render") @app.post("/pdf") def build_pdf(report: dict) -> Response: """Raport (JSON) → PDF. Wejście: person, data, wheel_svg, natal, predictions. Zwracamy surowe bajty PDF-a, nie base64 — łącze i tak jest szyfrowane, więc kodowanie tylko rozdmuchałoby odpowiedź o trzecią część. """ if not isinstance(report, dict): raise HTTPException(422, "Oczekiwano obiektu JSON z raportem.") try: pdf = to_pdf(report) except RenderError as e: raise HTTPException(500, str(e)) except FileNotFoundError as e: # brak xelatex/rsvg w obrazie raise HTTPException(503, f"Brak narzędzia w obrazie usługi render: {e}") return Response( content=pdf, media_type="application/pdf", headers={"Content-Disposition": 'attachment; filename="raport.pdf"'}, ) @app.get("/health") def health() -> dict: """Raportuje też obecność narzędzi — dzięki temu zepsuty obraz widać od razu, a nie dopiero przy pierwszym raporcie.""" tools = toolchain_ready() return {"status": "ok" if all(tools.values()) else "degraded", "layer": "render", "tools": tools}