Fix mypy type errors in reporting module

This commit is contained in:
2026-01-29 18:06:13 +00:00
parent c016320b71
commit 6830b3158c
8 changed files with 576 additions and 3 deletions

50
.dockerignore Normal file
View File

@@ -0,0 +1,50 @@
# Git
.git
.gitignore
.gitea
# Python
__pycache__
*.py[cod]
*$py.class
*.so
.Python
*.egg-info
.eggs
dist
build
.pytest_cache
.mypy_cache
.ruff_cache
.coverage
htmlcov
# Tests
tests/
pytest.ini
# Documentation
docs/
*.md
!README.md
# Development
.vscode
.idea
*.swp
*.swo
*~
# Environment
.env
.env.*
venv
.venv
# Data (will be created fresh in container)
data/
*.db
*.parquet
# CI/CD
.gitea/

View File

@@ -8,6 +8,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Added
- Fly.io deployment configuration for public demo
- Dockerfile with Python 3.11-slim and WeasyPrint dependencies
- fly.toml with scale-to-zero configuration (London region)
- .dockerignore to exclude tests, docs, and development files
- PDF Report Generation (Sprint 18)
- Professional PDF reports from test results with charts and styling
- `ReportGenerator` class orchestrating data gathering, chart generation, and PDF output

39
Dockerfile Normal file
View File

@@ -0,0 +1,39 @@
FROM python:3.11-slim
WORKDIR /app
# Install system dependencies for WeasyPrint (PDF generation)
RUN apt-get update && apt-get install -y --no-install-recommends \
libpango-1.0-0 \
libpangocairo-1.0-0 \
libgdk-pixbuf2.0-0 \
libffi-dev \
shared-mime-info \
&& rm -rf /var/lib/apt/lists/*
# Copy project files
COPY pyproject.toml .
COPY src/ src/
COPY config/ config/
# Install package with reports dependencies (for PDF export)
RUN pip install --no-cache-dir -e ".[reports]"
# Create data directory for SQLite and measurements
RUN mkdir -p /app/data/measurements /app/data/reports
# Streamlit configuration
RUN mkdir -p ~/.streamlit
RUN echo '[server]\n\
headless = true\n\
address = "0.0.0.0"\n\
port = 8080\n\
enableXsrfProtection = false\n\
enableCORS = false\n\
\n\
[browser]\n\
gatherUsageStats = false\n' > ~/.streamlit/config.toml
EXPOSE 8080
CMD ["streamlit", "run", "src/py_dvt_ate/app/dashboard/app.py"]

View File

@@ -0,0 +1,460 @@
# Sprint 18: PDF Report Generation
| Document ID | DEV-002 |
|-------------|---------|
| Version | 1.0.0 |
| Status | Draft |
| Author | Kai Chappell |
| Created | 2026-01-29 |
| Last Updated | 2026-01-29 |
---
## Purpose
This document defines **Sprint 18** of py-dvt-ate development: automated PDF report generation from test results. Reports are designed to be professional and well-presented for recruiters/clients evaluating the simulation platform.
For project context, see:
- `01_requirements.md` - What the system must do
- `02_technical_specification.md` - How to implement
- `03_architecture_decisions.md` - Why decisions were made
- `04_development_plan.md` - Phase 1 MVP sprints (1-17)
---
## Feature Overview
Add automated PDF report generation with:
- Professional, well-presented layout suitable for external stakeholders
- Clean UX with easy download from CLI and dashboard
- Test metadata, results table with pass/fail status, and measurement charts
- Configurable company branding
---
## Design Principles
Following existing project patterns:
1. **Small, Focused Commits** - Each task = 1 commit, ~50-150 lines changed
2. **Stubs First** - Define interfaces/types before implementation
3. **Test Alongside** - Write tests immediately after implementation
4. **UK English** - characterisation, behaviour, colour
5. **Minimal Context** - Each task completable with knowledge of 1-3 files
---
## Task Breakdown
### Task 18.1: Add reporting dependencies to pyproject.toml
- Add `matplotlib>=3.8` to reports optional dependency group
- Verify jinja2 and weasyprint already present
- **Files:** `pyproject.toml`
- **Commit:** "Add matplotlib to reports dependencies"
---
### Task 18.2: Create report data models
- Create `src/py_dvt_ate/reporting/models.py`
- Define `ReportConfig` dataclass:
- `company_name: str` - Company name for header
- `logo_path: Path | None` - Optional logo image path
- `include_charts: bool` - Whether to include charts
- `chart_dpi: int` - Chart resolution
- Define `ReportData` dataclass:
- `run: TestRun` - Test run metadata
- `results: list[TestResult]` - Scalar results with limits
- `measurements: pd.DataFrame | None` - Time-series data
- `charts: dict[str, str]` - Chart name to base64 PNG
- **Files:** `src/py_dvt_ate/reporting/models.py`
- **Commit:** "Add report data models"
---
### Task 18.3: Create reporting exceptions
- Create `src/py_dvt_ate/reporting/exceptions.py`
- Define exception hierarchy:
- `ReportingError` - Base exception
- `ReportGenerationError` - General generation failure
- `TemplateRenderError` - HTML rendering failure
- `PDFConversionError` - HTML to PDF conversion failure
- `ChartGenerationError` - Chart generation failure
- **Files:** `src/py_dvt_ate/reporting/exceptions.py`
- **Commit:** "Add reporting exception classes"
---
### Task 18.4: Create CSS stylesheet for reports
- Create `src/py_dvt_ate/reporting/templates/styles.css`
- Professional styling:
- A4 page setup with margins
- Header with company branding
- Footer with page numbers
- Data tables with borders
- Status badges (pass=green, fail=red, info=blue)
- Summary cards with colour coding
- Chart containers
- Print-optimised with page breaks
- **Files:** `src/py_dvt_ate/reporting/templates/styles.css`
- **Commit:** "Add professional CSS stylesheet for reports"
---
### Task 18.5: Create base HTML template
- Create `src/py_dvt_ate/reporting/templates/base.html`
- Jinja2 base template with:
- `<head>` with CSS include
- Header block with company name, logo, report metadata
- Content block (for child templates)
- Footer block with confidentiality notice, page numbers
- WeasyPrint `@page` rules for PDF pagination
- **Files:** `src/py_dvt_ate/reporting/templates/base.html`
- **Commit:** "Add base HTML report template"
---
### Task 18.6: Create test report template
- Create `src/py_dvt_ate/reporting/templates/test_report.html`
- Extends `base.html` with sections:
- **Test Overview**: name, description, status, timestamps, duration, operator
- **Results Summary**: total/pass/fail cards with counts
- **Results Table**: parameter, value, unit, limits, pass/fail badge
- **Charts**: voltage vs temperature (if available)
- **Configuration**: test config JSON (optional)
- Jinja2 filters for formatting (floats, dates)
- **Files:** `src/py_dvt_ate/reporting/templates/test_report.html`
- **Commit:** "Add test report HTML template"
---
### Task 18.7: Implement HTML renderer
- Create `src/py_dvt_ate/reporting/renderers/__init__.py`
- Create `src/py_dvt_ate/reporting/renderers/html.py`
- `HTMLRenderer` class:
- Constructor takes `ReportConfig`
- Uses `jinja2.Environment` with `PackageLoader`
- `render(report_data: ReportData) -> str` method
- Custom filters for number formatting
- Template loading from `py_dvt_ate.reporting.templates` package
- **Files:** `src/py_dvt_ate/reporting/renderers/html.py`, `src/py_dvt_ate/reporting/renderers/__init__.py`
- **Commit:** "Implement HTML renderer with Jinja2"
---
### Task 18.8: Implement PDF renderer
- Create `src/py_dvt_ate/reporting/renderers/pdf.py`
- `PDFRenderer` class:
- `render_to_file(html: str, output_path: Path) -> None`
- `render_to_bytes(html: str) -> bytes`
- Use WeasyPrint `HTML(string=html).write_pdf()`
- Handle WeasyPrint warnings gracefully
- **Files:** `src/py_dvt_ate/reporting/renderers/pdf.py`
- **Commit:** "Implement PDF renderer with WeasyPrint"
---
### Task 18.9: Implement chart generator
- Create `src/py_dvt_ate/reporting/charts/__init__.py`
- Create `src/py_dvt_ate/reporting/charts/matplotlib_charts.py`
- `ChartGenerator` class:
- Constructor takes `ReportConfig` (for DPI)
- `_setup_style()` - Configure matplotlib for professional appearance
- `generate_voltage_vs_temperature(measurements: DataFrame) -> str`
- Scatter plot with trend line
- Calculate and display slope (ppm/C)
- Return base64-encoded PNG
- `generate_all(run, results, measurements) -> dict[str, str]`
- Dispatch to appropriate chart methods based on test type
- Use `matplotlib.use('Agg')` for non-interactive backend
- **Files:** `src/py_dvt_ate/reporting/charts/matplotlib_charts.py`, `src/py_dvt_ate/reporting/charts/__init__.py`
- **Commit:** "Implement matplotlib chart generator"
---
### Task 18.10: Implement ReportGenerator class
- Create `src/py_dvt_ate/reporting/generator.py`
- `IReportGenerator` Protocol:
- `generate(run_id: UUID, output_path: Path | None) -> Path`
- `generate_bytes(run_id: UUID) -> bytes`
- `ReportGenerator` class:
- Constructor: `repository`, `config`, `output_dir`
- Private: `_html_renderer`, `_pdf_renderer`, `_chart_generator`
- `_gather_data(run_id: UUID) -> ReportData`
- Fetch run, results, measurements from repository
- Generate charts if measurements available
- `_generate_output_path(run: TestRun) -> Path`
- Format: `{test_name}_{run_id_short}_{timestamp}.pdf`
- Error handling with appropriate exception types
- **Files:** `src/py_dvt_ate/reporting/generator.py`
- **Commit:** "Implement ReportGenerator class"
---
### Task 18.11: Update reporting module exports
- Update `src/py_dvt_ate/reporting/__init__.py`
- Export public API:
- `ReportGenerator`, `IReportGenerator`
- `ReportConfig`, `ReportData`
- All exception classes
- Add module docstring with usage example
- Lazy imports to handle missing optional dependencies
- **Files:** `src/py_dvt_ate/reporting/__init__.py`
- **Commit:** "Update reporting module public API"
---
### Task 18.12: Add ReportingConfig to app config
- Update `src/py_dvt_ate/app/config.py`
- Add `ReportingConfig` Pydantic model:
- `company_name: str = "DVT Engineering"`
- `logo_path: str | None = None`
- `include_charts: bool = True`
- `chart_dpi: int = 150`
- Add `reporting: ReportingConfig` to `AppConfig`
- **Files:** `src/py_dvt_ate/app/config.py`
- **Commit:** "Add ReportingConfig to application config"
---
### Task 18.13: Add reporting section to default.yaml
- Update `config/default.yaml`
- Add `reporting:` section with all options
- Document each option with comments
- **Files:** `config/default.yaml`
- **Commit:** "Add reporting configuration to default.yaml"
---
### Task 18.14: Add list-runs CLI command
- Update `src/py_dvt_ate/app/cli.py`
- Add `list-runs` command:
- `--limit` option (default 10)
- `--config` option for config file
- Output format: `{id:8} {test_name:15} {status:8} {timestamp}`
- Load repository from config
- **Files:** `src/py_dvt_ate/app/cli.py`
- **Commit:** "Add list-runs CLI command"
---
### Task 18.15: Add export-report CLI command
- Update `src/py_dvt_ate/app/cli.py`
- Add `export-report` command:
- `run_id` argument (required)
- `--output` / `-o` option for output path
- `--company` option for company name override
- `--config` option for config file
- Support short (8-char) and full UUID lookup
- Display progress and result path
- **Files:** `src/py_dvt_ate/app/cli.py`
- **Commit:** "Add export-report CLI command"
---
### Task 18.16: Add PDF download to dashboard
- Update `src/py_dvt_ate/app/dashboard/app.py`
- In results viewer page, add:
- "Generate PDF Report" button (primary)
- `st.download_button` for PDF download
- Progress spinner during generation
- Error handling for missing dependencies
- Store generated PDF in `st.session_state`
- **Files:** `src/py_dvt_ate/app/dashboard/app.py`
- **Commit:** "Add PDF download button to dashboard"
---
### Task 18.17: Add reporting unit tests
- Create `tests/unit/reporting/__init__.py`
- Create `tests/unit/reporting/test_models.py`
- Test ReportConfig and ReportData creation
- Test default values
- Create `tests/unit/reporting/test_html_renderer.py`
- Test template rendering with mock data
- Test custom filters
- Create `tests/unit/reporting/test_chart_generator.py`
- Test chart generation produces valid base64
- Test with sample DataFrame
- **Files:** `tests/unit/reporting/`
- **Commit:** "Add reporting unit tests"
---
### Task 18.18: Add reporting integration test
- Create `tests/integration/test_report_generation.py`
- End-to-end test:
- Create test run with sample results in repository
- Generate PDF report
- Verify PDF file created and non-empty
- Optionally verify PDF structure (page count)
- Use pytest fixtures for repository setup
- **Files:** `tests/integration/test_report_generation.py`
- **Commit:** "Add report generation integration test"
---
### Task 18.19: Update CHANGELOG
- Update `CHANGELOG.md`
- Add `## [Unreleased]` section if not present
- Document:
- New `export-report` CLI command
- New `list-runs` CLI command
- Dashboard PDF download feature
- Reporting module with PDF/HTML generation
- **Files:** `CHANGELOG.md`
- **Commit:** "Update CHANGELOG with report generation feature"
---
## File Structure (New Files)
```
src/py_dvt_ate/reporting/
├── __init__.py # Task 18.11 - Public API
├── models.py # Task 18.2 - ReportConfig, ReportData
├── exceptions.py # Task 18.3 - Exception hierarchy
├── generator.py # Task 18.10 - ReportGenerator
├── renderers/
│ ├── __init__.py # Task 18.7
│ ├── html.py # Task 18.7 - HTMLRenderer
│ └── pdf.py # Task 18.8 - PDFRenderer
├── charts/
│ ├── __init__.py # Task 18.9
│ └── matplotlib_charts.py # Task 18.9 - ChartGenerator
└── templates/
├── styles.css # Task 18.4 - CSS
├── base.html # Task 18.5 - Base template
└── test_report.html # Task 18.6 - Report template
tests/unit/reporting/
├── __init__.py # Task 18.17
├── test_models.py # Task 18.17
├── test_html_renderer.py # Task 18.17
└── test_chart_generator.py # Task 18.17
tests/integration/
└── test_report_generation.py # Task 18.18
```
---
## Files to Modify
| File | Tasks | Changes |
|------|-------|---------|
| `pyproject.toml` | 18.1 | Add matplotlib to reports deps |
| `src/py_dvt_ate/app/config.py` | 18.12 | Add ReportingConfig |
| `config/default.yaml` | 18.13 | Add reporting section |
| `src/py_dvt_ate/app/cli.py` | 18.14, 18.15 | Add list-runs, export-report |
| `src/py_dvt_ate/app/dashboard/app.py` | 18.16 | Add PDF download |
| `CHANGELOG.md` | 18.19 | Document new features |
---
## Dependencies
| Package | Version | Purpose |
|---------|---------|---------|
| jinja2 | >=3.1 | HTML template rendering |
| weasyprint | >=60.0 | HTML to PDF conversion |
| matplotlib | >=3.8 | Chart generation |
All in `[project.optional-dependencies] reports` group.
Install with: `pip install py-dvt-ate[reports]`
---
## Verification
### CLI Verification
```bash
# List recent test runs
py-dvt-ate list-runs
# Generate PDF report
py-dvt-ate export-report <run_id>
# With options
py-dvt-ate export-report <run_id> -o ./my_report.pdf --company "Acme Corp"
# View generated PDF
xdg-open ./data/reports/*.pdf
```
### Dashboard Verification
```bash
# Start dashboard
streamlit run src/py_dvt_ate/app/dashboard/app.py
# In browser:
# 1. Navigate to Results Viewer
# 2. Select a test run
# 3. Click "Generate PDF Report"
# 4. Click "Download PDF Report"
# 5. Open downloaded PDF
```
### Test Verification
```bash
# Run unit tests
pytest tests/unit/reporting/ -v
# Run integration test
pytest tests/integration/test_report_generation.py -v
# Check coverage
pytest tests/unit/reporting/ --cov=py_dvt_ate.reporting --cov-report=term-missing
```
---
## Task Progress
| Task | Status | Description |
|------|--------|-------------|
| 18.1 | pending | Add matplotlib dependency |
| 18.2 | pending | Report data models |
| 18.3 | pending | Reporting exceptions |
| 18.4 | pending | CSS stylesheet |
| 18.5 | pending | Base HTML template |
| 18.6 | pending | Test report template |
| 18.7 | pending | HTML renderer |
| 18.8 | pending | PDF renderer |
| 18.9 | pending | Chart generator |
| 18.10 | pending | ReportGenerator class |
| 18.11 | pending | Module exports |
| 18.12 | pending | App config update |
| 18.13 | pending | default.yaml update |
| 18.14 | pending | list-runs CLI |
| 18.15 | pending | export-report CLI |
| 18.16 | pending | Dashboard download |
| 18.17 | pending | Unit tests |
| 18.18 | pending | Integration test |
| 18.19 | pending | CHANGELOG update |
---
**End of Sprint 18 Plan**

16
fly.toml Normal file
View File

@@ -0,0 +1,16 @@
app = "py-dvt-ate"
primary_region = "lhr"
[build]
[http_service]
internal_port = 8080
force_https = true
auto_stop_machines = "stop"
auto_start_machines = true
min_machines_running = 0
[[vm]]
cpu_kind = "shared"
cpus = 1
memory_mb = 512

View File

@@ -84,6 +84,8 @@ disallow_incomplete_defs = true
module = [
"streamlit.*",
"plotly.*",
"weasyprint.*",
"matplotlib.*",
]
ignore_missing_imports = true

View File

@@ -6,6 +6,7 @@ Charts are rendered to base64-encoded PNG images for embedding in HTML/PDF.
import base64
from io import BytesIO
from typing import Any
import pandas as pd
@@ -27,9 +28,9 @@ class ChartGenerator:
dpi: Resolution for chart images (dots per inch).
"""
self.dpi = dpi
self._plt: type | None = None
self._plt: tuple[Any, Any] | None = None
def _get_matplotlib(self) -> tuple:
def _get_matplotlib(self) -> tuple[Any, Any]:
"""Lazy-load matplotlib to avoid import errors when not installed.
Returns:

View File

@@ -75,7 +75,8 @@ class PDFRenderer:
"""
try:
HTML = self._get_weasyprint()
return HTML(string=html).write_pdf()
result: bytes = HTML(string=html).write_pdf()
return result
except PDFConversionError:
raise
except Exception as e: