diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..2945bb6 --- /dev/null +++ b/.dockerignore @@ -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/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e16f73..622705f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..965c1e5 --- /dev/null +++ b/Dockerfile @@ -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"] diff --git a/docs/05_sprint_18_report_generation.md b/docs/05_sprint_18_report_generation.md new file mode 100644 index 0000000..edb83cc --- /dev/null +++ b/docs/05_sprint_18_report_generation.md @@ -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: + - `` 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 + +# With options +py-dvt-ate export-report -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** diff --git a/fly.toml b/fly.toml new file mode 100644 index 0000000..348c30f --- /dev/null +++ b/fly.toml @@ -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 diff --git a/pyproject.toml b/pyproject.toml index 43d001a..bd7726c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -84,6 +84,8 @@ disallow_incomplete_defs = true module = [ "streamlit.*", "plotly.*", + "weasyprint.*", + "matplotlib.*", ] ignore_missing_imports = true diff --git a/src/py_dvt_ate/reporting/charts/matplotlib_charts.py b/src/py_dvt_ate/reporting/charts/matplotlib_charts.py index 31cadd3..6a4ba21 100644 --- a/src/py_dvt_ate/reporting/charts/matplotlib_charts.py +++ b/src/py_dvt_ate/reporting/charts/matplotlib_charts.py @@ -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: diff --git a/src/py_dvt_ate/reporting/renderers/pdf.py b/src/py_dvt_ate/reporting/renderers/pdf.py index e95e6a9..a6dc9f3 100644 --- a/src/py_dvt_ate/reporting/renderers/pdf.py +++ b/src/py_dvt_ate/reporting/renderers/pdf.py @@ -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: