Files
Aegis/backend/app/services/report_engine.py
T
kitos 443a04befb
Aegis CI / lint-and-test (push) Has been cancelled
Snyk Security Scan / Python vulnerabilities (backend) (push) Has been cancelled
Snyk Security Scan / npm vulnerabilities (frontend) (push) Has been cancelled
Snyk Security Scan / Docker image vulnerabilities (backend) (push) Has been cancelled
fix(report_engine): lazy-init output dir to fix CI PermissionError on /app
2026-06-12 14:13:41 +02:00

158 lines
5.6 KiB
Python

"""Report engine — renders Jinja2 HTML templates to PDF, DOCX, and HTML.
Uses WeasyPrint for PDF generation and docxtpl for DOCX.
"""
# Import logging
import logging
# Import os
import os
# Import uuid
import uuid
# Import datetime from datetime
from datetime import datetime
# Import Environment, FileSystemLoader from jinja2
from jinja2 import Environment, FileSystemLoader
# Import settings from app.config
from app.config import settings
# Assign logger = logging.getLogger(__name__)
logger = logging.getLogger(__name__)
# Define class ReportEngine
class ReportEngine:
"""Template-based report generator supporting PDF, DOCX, and HTML output."""
# Define function __init__
def __init__(self) -> None:
"""Initialise the Jinja2 environment."""
# Assign self.jinja_env = Environment(
self.jinja_env = Environment(
# Keyword argument: loader
loader=FileSystemLoader(settings.REPORT_TEMPLATES_DIR),
# Keyword argument: autoescape
autoescape=True,
)
def _ensure_output_dir(self) -> None:
os.makedirs(settings.REPORT_OUTPUT_DIR, exist_ok=True)
# Define function render_html
def render_html(self, template_name: str, context: dict) -> str:
"""Render a Jinja2 template to an HTML string."""
# Assign template = self.jinja_env.get_template(f"{template_name}.html")
template = self.jinja_env.get_template(f"{template_name}.html")
# Call context.setdefault()
context.setdefault("company_name", settings.COMPANY_NAME)
# Call context.setdefault()
context.setdefault("generated_at", datetime.utcnow().strftime("%B %d, %Y %H:%M UTC"))
# Return template.render(context)
return template.render(context)
# Define function generate_pdf
def generate_pdf(self, template_name: str, context: dict) -> str:
"""Render HTML and convert to PDF with WeasyPrint."""
self._ensure_output_dir()
# Import CSS, HTML from weasyprint
from weasyprint import CSS, HTML
# Assign html_content = self.render_html(template_name, context)
html_content = self.render_html(template_name, context)
# Assign css_path = os.path.join(settings.REPORT_TEMPLATES_DIR, "styles", "report.css")
css_path = os.path.join(settings.REPORT_TEMPLATES_DIR, "styles", "report.css")
# Assign output_path = os.path.join(
output_path = os.path.join(
settings.REPORT_OUTPUT_DIR,
f"{template_name}_{uuid.uuid4().hex[:8]}.pdf",
)
# Assign stylesheets = []
stylesheets = []
# Check: os.path.exists(css_path)
if os.path.exists(css_path):
# Call stylesheets.append()
stylesheets.append(CSS(filename=css_path))
# Call HTML()
HTML(
# Keyword argument: string
string=html_content,
# Keyword argument: base_url
base_url=settings.REPORT_TEMPLATES_DIR,
).write_pdf(output_path, stylesheets=stylesheets)
# Log info: "PDF generated: %s", output_path
logger.info("PDF generated: %s", output_path)
# Return output_path
return output_path
# Define function generate_docx
def generate_docx(self, template_name: str, context: dict) -> str:
"""Render a .docx template with docxtpl."""
self._ensure_output_dir()
# Import DocxTemplate from docxtpl
from docxtpl import DocxTemplate
# Assign template_path = os.path.join(
template_path = os.path.join(
settings.REPORT_TEMPLATES_DIR, f"{template_name}.docx"
)
# Assign output_path = os.path.join(
output_path = os.path.join(
settings.REPORT_OUTPUT_DIR,
f"{template_name}_{uuid.uuid4().hex[:8]}.docx",
)
# Assign doc = DocxTemplate(template_path)
doc = DocxTemplate(template_path)
# Call context.setdefault()
context.setdefault("company_name", settings.COMPANY_NAME)
# Call context.setdefault()
context.setdefault("generated_at", datetime.utcnow().strftime("%B %d, %Y"))
# Call doc.render()
doc.render(context)
# Call doc.save()
doc.save(output_path)
# Log info: "DOCX generated: %s", output_path
logger.info("DOCX generated: %s", output_path)
# Return output_path
return output_path
# Define function generate_html
def generate_html(self, template_name: str, context: dict) -> str:
"""Render and save a standalone HTML report (alias for spec compatibility)."""
# Return self.generate_html_file(template_name, context)
return self.generate_html_file(template_name, context)
# Define function generate_html_file
def generate_html_file(self, template_name: str, context: dict) -> str:
"""Render and save a standalone HTML report."""
self._ensure_output_dir()
# Assign html_content = self.render_html(template_name, context)
html_content = self.render_html(template_name, context)
# Assign output_path = os.path.join(
output_path = os.path.join(
settings.REPORT_OUTPUT_DIR,
f"{template_name}_{uuid.uuid4().hex[:8]}.html",
)
# Open context manager
with open(output_path, "w", encoding="utf-8") as f:
# Call f.write()
f.write(html_content)
# Log info: "HTML report generated: %s", output_path
logger.info("HTML report generated: %s", output_path)
# Return output_path
return output_path
# Assign report_engine = ReportEngine()
report_engine = ReportEngine()