JairoDanielMT commited on
Commit
598559d
Β·
1 Parent(s): 7d0c16f

probando si funciona el json -> pdf

Browse files
Dockerfile ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ ENV DEBIAN_FRONTEND=noninteractive
4
+
5
+ WORKDIR /app
6
+
7
+ RUN apt-get update && \
8
+ apt-get install -y --no-install-recommends \
9
+ wkhtmltopdf \
10
+ fonts-dejavu-core \
11
+ && rm -rf /var/lib/apt/lists/*
12
+
13
+ COPY requirements.txt .
14
+ RUN pip install --no-cache-dir -r requirements.txt
15
+
16
+ COPY app/ app/
17
+
18
+ ENV PYTHONUNBUFFERED=1
19
+
20
+ EXPOSE 7860
21
+
22
+ CMD ["sh", "-c", "uvicorn app.main:app --host 0.0.0.0 --port ${PORT:-7860}"]
app/__init__.py/config.py ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ import os
5
+ from pathlib import Path
6
+ from typing import Optional
7
+
8
+ import pdfkit
9
+
10
+ # Carpeta de este mΓ³dulo (app/)
11
+ BASE_DIR = Path(__file__).parent
12
+
13
+ # Cache de configuraciΓ³n de pdfkit
14
+ # False => aΓΊn no inicializado
15
+ # None => se intentΓ³ inicializar pero no se encontrΓ³ wkhtmltopdf
16
+ # config => configuraciΓ³n vΓ‘lida
17
+ _PDFKIT_CONFIG: Optional[pdfkit.configuration] | bool = False
18
+
19
+
20
+ def _detect_wkhtmltopdf() -> Optional[str]:
21
+ """
22
+ Para entorno Docker (Linux):
23
+
24
+ 1) Si hay WKHTMLTOPDF_PATH y apunta a un archivo, usarlo.
25
+ 2) Si no, devolver None y dejar que pdfkit use lo que haya en PATH.
26
+
27
+ No nos preocupamos por .exe ni por Windows aquΓ­.
28
+ """
29
+ env_path = os.getenv("WKHTMLTOPDF_PATH")
30
+ if env_path and Path(env_path).is_file():
31
+ return env_path
32
+
33
+ # None = que pdfkit use wkhtmltopdf del PATH (/usr/bin/wkhtmltopdf)
34
+ return None
35
+
36
+
37
+ def get_pdfkit_config() -> Optional[pdfkit.configuration]:
38
+ """
39
+ Devuelve una instancia de pdfkit.configuration o None si
40
+ no se pudo inicializar wkhtmltopdf.
41
+ """
42
+ global _PDFKIT_CONFIG
43
+
44
+ if _PDFKIT_CONFIG is not False:
45
+ return _PDFKIT_CONFIG # type: ignore[return-value]
46
+
47
+ wkhtml_path = _detect_wkhtmltopdf()
48
+
49
+ try:
50
+ if wkhtml_path:
51
+ _PDFKIT_CONFIG = pdfkit.configuration(wkhtmltopdf=wkhtml_path)
52
+ logging.info("wkhtmltopdf detectado en WKHTMLTOPDF_PATH: %s", wkhtml_path)
53
+ else:
54
+ # Usar binario en PATH (caso normal en Docker: /usr/bin/wkhtmltopdf)
55
+ _PDFKIT_CONFIG = pdfkit.configuration()
56
+ logging.info("wkhtmltopdf detectado en PATH.")
57
+ except OSError as exc:
58
+ logging.error("No se pudo inicializar pdfkit: %s", exc)
59
+ _PDFKIT_CONFIG = None
60
+
61
+ return _PDFKIT_CONFIG # type: ignore[return-value]
app/__init__.py/main.py ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import io
4
+ from typing import Any, Dict, Literal, Optional
5
+
6
+ from fastapi import FastAPI, HTTPException
7
+ from fastapi.responses import HTMLResponse, StreamingResponse
8
+ from pydantic import BaseModel, Field
9
+
10
+ from .renderer import render_html, render_pdf_bytes
11
+
12
+ app = FastAPI(
13
+ title="Doc Compiler Service",
14
+ version="1.0.0",
15
+ description=(
16
+ "Servicio de compilaciΓ³n que recibe JSON de documentaciΓ³n, "
17
+ "renderiza plantillas Jinja2 y devuelve HTML o PDF."
18
+ ),
19
+ )
20
+
21
+
22
+ class CompileRequest(BaseModel):
23
+ doc: Dict[str, Any] = Field(
24
+ ...,
25
+ description="JSON de documentaciΓ³n generado por la IA (incluyendo metadata.script_type).",
26
+ )
27
+ job_id: Optional[str] = Field(
28
+ None,
29
+ description="Identificador opcional para el archivo (por ejemplo, nombre del script o job).",
30
+ )
31
+ output: Literal["html", "pdf"] = Field(
32
+ "pdf",
33
+ description="Tipo de salida deseada. En estos endpoints se usa como referencia.",
34
+ )
35
+
36
+
37
+ @app.get("/health")
38
+ def health_check():
39
+ """
40
+ Endpoint simple de healthcheck para monitoreo.
41
+ """
42
+ return {"status": "ok"}
43
+
44
+
45
+ @app.post("/compile/html")
46
+ def compile_html(req: CompileRequest):
47
+ """
48
+ Recibe JSON y devuelve el HTML renderizado (ΓΊtil para depuraciΓ³n o previsualizaciΓ³n).
49
+ """
50
+ if not req.doc:
51
+ raise HTTPException(status_code=400, detail="Campo 'doc' es obligatorio")
52
+
53
+ try:
54
+ html = render_html(req.doc)
55
+ except Exception as exc:
56
+ raise HTTPException(status_code=500, detail=f"Error al renderizar HTML: {exc}")
57
+
58
+ return HTMLResponse(content=html)
59
+
60
+
61
+ @app.post("/compile/pdf")
62
+ def compile_pdf(req: CompileRequest):
63
+ """
64
+ Recibe JSON y devuelve el PDF compilado usando wkhtmltopdf.
65
+ """
66
+ if not req.doc:
67
+ raise HTTPException(status_code=400, detail="Campo 'doc' es obligatorio")
68
+
69
+ job_id = req.job_id or "document"
70
+
71
+ try:
72
+ pdf_bytes = render_pdf_bytes(req.doc)
73
+ except RuntimeError as exc:
74
+ raise HTTPException(status_code=500, detail=str(exc))
75
+ except Exception as exc:
76
+ raise HTTPException(status_code=500, detail=f"Error al generar PDF: {exc}")
77
+
78
+ filename = f"{job_id}.pdf"
79
+
80
+ return StreamingResponse(
81
+ io.BytesIO(pdf_bytes),
82
+ media_type="application/pdf",
83
+ headers={"Content-Disposition": f'attachment; filename="{filename}"'},
84
+ )
app/__init__.py/renderer.py ADDED
@@ -0,0 +1,95 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from datetime import datetime
4
+ from typing import Any, Dict
5
+
6
+ import pdfkit
7
+ from jinja2 import Environment, FileSystemLoader, select_autoescape
8
+
9
+ from .config import BASE_DIR, get_pdfkit_config
10
+
11
+ # Carpeta de plantillas Jinja2
12
+ TEMPLATES_DIR = BASE_DIR / "templates"
13
+
14
+ # Mapeo script_type -> plantilla
15
+ TEMPLATE_MAP: Dict[str, str] = {
16
+ "RESTlet": "RESTlet.html.j2",
17
+ "ClientScript": "ClientScript.html.j2",
18
+ "Map/Reduce": "MapReduce.html.j2",
19
+ "MapReduce": "MapReduce.html.j2",
20
+ "Scheduled": "ScheduledScript.html.j2",
21
+ "ScheduledScript": "ScheduledScript.html.j2",
22
+ "Suitelet": "Suitelet.html.j2",
23
+ "UserEvent": "UserEventScript.html.j2",
24
+ "UserEventScript": "UserEventScript.html.j2",
25
+ "UserModule.js": "InternalModule.html.j2",
26
+ "InternalModule": "InternalModule.html.j2",
27
+ }
28
+
29
+ # Entorno Jinja
30
+ env = Environment(
31
+ loader=FileSystemLoader(str(TEMPLATES_DIR)),
32
+ autoescape=select_autoescape(["html", "xml"]),
33
+ trim_blocks=True,
34
+ lstrip_blocks=True,
35
+ )
36
+
37
+
38
+ def _resolve_template(doc: Dict[str, Any]) -> str:
39
+ """
40
+ A partir del JSON determina quΓ© plantilla Jinja usar
41
+ leyendo metadata.script_type o metadata.type.
42
+ """
43
+ metadata = doc.get("metadata") or {}
44
+ script_type = (
45
+ metadata.get("script_type") or metadata.get("type") or "InternalModule"
46
+ )
47
+ return TEMPLATE_MAP.get(script_type, "InternalModule.html.j2")
48
+
49
+
50
+ def render_html(doc: Dict[str, Any]) -> str:
51
+ """
52
+ Renderiza el HTML final a partir del JSON y la plantilla Jinja2 apropiada.
53
+ """
54
+ template_name = _resolve_template(doc)
55
+ template = env.get_template(template_name)
56
+
57
+ html = template.render(
58
+ doc=doc,
59
+ generated_at=datetime.utcnow().strftime("%Y-%m-%d %H:%M UTC"),
60
+ )
61
+ return html
62
+
63
+
64
+ def _wkhtml_options() -> Dict[str, str]:
65
+ """
66
+ Opciones por defecto para wkhtmltopdf.
67
+ """
68
+ return {
69
+ "page-size": "A4",
70
+ "encoding": "UTF-8",
71
+ "margin-top": "20mm",
72
+ "margin-right": "20mm",
73
+ "margin-bottom": "20mm",
74
+ "margin-left": "20mm",
75
+ }
76
+
77
+
78
+ def render_pdf_bytes(doc: Dict[str, Any]) -> bytes:
79
+ """
80
+ Toma el JSON, renderiza el HTML y lo compila a PDF (bytes en memoria).
81
+ Lanza RuntimeError si wkhtmltopdf no estΓ‘ disponible.
82
+ """
83
+ html = render_html(doc)
84
+ config = get_pdfkit_config()
85
+
86
+ if config is None:
87
+ raise RuntimeError("wkhtmltopdf no disponible en el servicio doc-compiler.")
88
+
89
+ pdf_bytes = pdfkit.from_string(
90
+ html,
91
+ False, # False => devuelve bytes en vez de escribir archivo
92
+ options=_wkhtml_options(),
93
+ configuration=config,
94
+ )
95
+ return pdf_bytes
{templates β†’ app/templates}/ClientScript.html.j2 RENAMED
File without changes
{templates β†’ app/templates}/IndexDocument.html.j2 RENAMED
File without changes
{templates β†’ app/templates}/InternalModule.html.j2 RENAMED
File without changes
{templates β†’ app/templates}/MapReduce.html.j2 RENAMED
File without changes
{templates β†’ app/templates}/RESTlet.html.j2 RENAMED
File without changes
{templates β†’ app/templates}/ScheduledScript.html.j2 RENAMED
File without changes
{templates β†’ app/templates}/Suitelet.html.j2 RENAMED
File without changes
{templates β†’ app/templates}/UserEventScript.html.j2 RENAMED
File without changes
{templates β†’ app/templates}/base.html.j2 RENAMED
File without changes
{templates β†’ app/templates}/components.html.j2 RENAMED
File without changes
requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ fastapi
2
+ uvicorn[standard]
3
+ jinja2
4
+ pdfkit
5
+ pydantic