From 5405ceec7fa63cbd92442b4921198bad5b7b91b2 Mon Sep 17 00:00:00 2001 From: Kai Chappell Date: Thu, 29 Jan 2026 17:58:02 +0000 Subject: [PATCH] Implement HTML renderer with Jinja2 --- .../reporting/renderers/__init__.py | 5 + src/py_dvt_ate/reporting/renderers/html.py | 100 ++++++++++++++++++ 2 files changed, 105 insertions(+) create mode 100644 src/py_dvt_ate/reporting/renderers/__init__.py create mode 100644 src/py_dvt_ate/reporting/renderers/html.py diff --git a/src/py_dvt_ate/reporting/renderers/__init__.py b/src/py_dvt_ate/reporting/renderers/__init__.py new file mode 100644 index 0000000..5e585f6 --- /dev/null +++ b/src/py_dvt_ate/reporting/renderers/__init__.py @@ -0,0 +1,5 @@ +"""Report renderers for HTML and PDF output.""" + +from py_dvt_ate.reporting.renderers.html import HTMLRenderer + +__all__ = ["HTMLRenderer"] diff --git a/src/py_dvt_ate/reporting/renderers/html.py b/src/py_dvt_ate/reporting/renderers/html.py new file mode 100644 index 0000000..bff2930 --- /dev/null +++ b/src/py_dvt_ate/reporting/renderers/html.py @@ -0,0 +1,100 @@ +"""HTML renderer using Jinja2 templates. + +This module provides HTML rendering for test reports using Jinja2 templating. +Templates are loaded from the package's templates directory. +""" + +import base64 +import json +from datetime import datetime +from importlib import resources +from pathlib import Path + +from jinja2 import Environment, PackageLoader, select_autoescape + +from py_dvt_ate import __version__ +from py_dvt_ate.reporting.exceptions import TemplateRenderError +from py_dvt_ate.reporting.models import ReportData + + +class HTMLRenderer: + """Renders HTML reports from ReportData using Jinja2 templates. + + The renderer loads templates from the py_dvt_ate.reporting.templates package + and provides methods for rendering complete HTML reports. + """ + + def __init__(self) -> None: + """Initialise the HTML renderer with Jinja2 environment.""" + self._env = Environment( + loader=PackageLoader("py_dvt_ate.reporting", "templates"), + autoescape=select_autoescape(["html", "xml"]), + ) + self._css_content: str | None = None + + def _load_css(self) -> str: + """Load CSS content from the templates directory.""" + if self._css_content is None: + templates_pkg = resources.files("py_dvt_ate.reporting.templates") + css_file = templates_pkg.joinpath("styles.css") + self._css_content = css_file.read_text() + return self._css_content + + def _load_logo(self, logo_path: Path | None) -> str | None: + """Load and encode logo image as base64. + + Args: + logo_path: Path to logo image file. + + Returns: + Base64-encoded image data, or None if no logo or file not found. + """ + if logo_path is None or not logo_path.exists(): + return None + + try: + with logo_path.open("rb") as f: + return base64.b64encode(f.read()).decode("utf-8") + except OSError: + return None + + def render(self, data: ReportData) -> str: + """Render a test report to HTML. + + Args: + data: Report data containing test run, results, and charts. + + Returns: + Complete HTML document as a string. + + Raises: + TemplateRenderError: If template rendering fails. + """ + try: + template = self._env.get_template("test_report.html") + + # Format config JSON for display + config_formatted = "" + if data.run.config_json: + try: + config_dict = json.loads(data.run.config_json) + config_formatted = json.dumps(config_dict, indent=2) + except json.JSONDecodeError: + config_formatted = data.run.config_json + + # Prepare template context + context = { + "data": data, + "css_content": self._load_css(), + "logo_base64": self._load_logo(data.config.logo_path), + "company_name": data.config.company_name, + "version": __version__, + "generated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), + "config_formatted": config_formatted, + } + + return template.render(**context) + + except Exception as e: + msg = f"Failed to render HTML template: {e}" + raise TemplateRenderError(msg) from e