Implement ReportGenerator class

This commit is contained in:
2026-01-29 17:59:27 +00:00
parent 50432eaa3d
commit d76e610070

View File

@@ -0,0 +1,199 @@
"""Report generator orchestrating the full report generation pipeline.
This module provides the main ReportGenerator class that coordinates
data gathering, chart generation, HTML rendering, and PDF conversion.
"""
from datetime import datetime
from pathlib import Path
from typing import Protocol
from uuid import UUID
from py_dvt_ate.data.repository import ITestRepository
from py_dvt_ate.reporting.charts import ChartGenerator
from py_dvt_ate.reporting.exceptions import ReportGenerationError
from py_dvt_ate.reporting.models import ReportConfig, ReportData
from py_dvt_ate.reporting.renderers import HTMLRenderer, PDFRenderer
class IReportGenerator(Protocol):
"""Protocol for report generators."""
def generate(self, run_id: UUID, output_path: Path | None = None) -> Path:
"""Generate a PDF report for a test run.
Args:
run_id: UUID of the test run.
output_path: Optional output path. If None, uses default location.
Returns:
Path to the generated PDF file.
"""
...
def generate_bytes(self, run_id: UUID) -> bytes:
"""Generate a PDF report and return as bytes.
Args:
run_id: UUID of the test run.
Returns:
PDF document as bytes.
"""
...
class ReportGenerator:
"""Generates PDF reports from test run data.
This class orchestrates the full report generation pipeline:
1. Fetch test run data from repository
2. Generate charts from measurements
3. Render HTML from templates
4. Convert HTML to PDF
Example:
>>> from py_dvt_ate.data.repository import SQLiteRepository
>>> from py_dvt_ate.reporting import ReportGenerator, ReportConfig
>>>
>>> repo = SQLiteRepository("./data/py_dvt_ate.db")
>>> config = ReportConfig(company_name="My Company")
>>> generator = ReportGenerator(repo, config)
>>> pdf_path = generator.generate(run_id)
"""
def __init__(
self,
repository: ITestRepository,
config: ReportConfig | None = None,
reports_dir: Path | None = None,
) -> None:
"""Initialise the report generator.
Args:
repository: Test data repository for fetching run data.
config: Report configuration. Uses defaults if not provided.
reports_dir: Directory for generated reports. Defaults to ./data/reports.
"""
self.repository = repository
self.config = config or ReportConfig()
self.reports_dir = reports_dir or Path("./data/reports")
self._html_renderer = HTMLRenderer()
self._pdf_renderer = PDFRenderer()
self._chart_generator = ChartGenerator(dpi=self.config.chart_dpi)
def _gather_data(self, run_id: UUID) -> ReportData:
"""Gather all data needed for the report.
Args:
run_id: UUID of the test run.
Returns:
ReportData containing run, results, measurements, and charts.
Raises:
ReportGenerationError: If data gathering fails.
"""
try:
run = self.repository.get_run(run_id)
results = self.repository.get_results(run_id)
measurements = self.repository.get_measurements_dataframe(run_id)
# Generate charts if enabled
charts: dict[str, str] = {}
if self.config.include_charts:
charts = self._chart_generator.generate_all(run, results, measurements)
return ReportData(
run=run,
results=results,
measurements=measurements,
charts=charts,
config=self.config,
)
except ValueError as e:
msg = f"Failed to gather data for run {run_id}: {e}"
raise ReportGenerationError(msg) from e
def _generate_filename(self, run_id: UUID, test_name: str) -> str:
"""Generate a filename for the report.
Args:
run_id: UUID of the test run.
test_name: Name of the test.
Returns:
Filename string with timestamp.
"""
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
safe_name = test_name.replace(" ", "_").replace("/", "_")
return f"{safe_name}_{str(run_id)[:8]}_{timestamp}.pdf"
def generate(self, run_id: UUID, output_path: Path | None = None) -> Path:
"""Generate a PDF report for a test run.
Args:
run_id: UUID of the test run.
output_path: Optional output path. If None, uses default location.
Returns:
Path to the generated PDF file.
Raises:
ReportGenerationError: If report generation fails.
"""
try:
# Gather data
data = self._gather_data(run_id)
# Determine output path
if output_path is None:
self.reports_dir.mkdir(parents=True, exist_ok=True)
filename = self._generate_filename(run_id, data.run.test_name)
output_path = self.reports_dir / filename
# Render HTML
html = self._html_renderer.render(data)
# Convert to PDF
self._pdf_renderer.render_to_file(html, output_path)
return output_path
except ReportGenerationError:
raise
except Exception as e:
msg = f"Failed to generate report for run {run_id}: {e}"
raise ReportGenerationError(msg) from e
def generate_bytes(self, run_id: UUID) -> bytes:
"""Generate a PDF report and return as bytes.
Useful for streaming downloads without writing to disk.
Args:
run_id: UUID of the test run.
Returns:
PDF document as bytes.
Raises:
ReportGenerationError: If report generation fails.
"""
try:
# Gather data
data = self._gather_data(run_id)
# Render HTML
html = self._html_renderer.render(data)
# Convert to PDF bytes
return self._pdf_renderer.render_to_bytes(html)
except ReportGenerationError:
raise
except Exception as e:
msg = f"Failed to generate report bytes for run {run_id}: {e}"
raise ReportGenerationError(msg) from e