Coverage for src / tracekit / reporting / pptx_export.py: 32%
133 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-11 23:04 +0000
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-11 23:04 +0000
1"""PowerPoint presentation export for TraceKit reports.
3This module provides PPTX generation for stakeholder presentations
4with automated slide layouts, embedded plots, and speaker notes.
7Note:
8 This is a stub implementation. For full functionality, install python-pptx:
9 pip install python-pptx
11Example:
12 >>> from tracekit.reporting import export_pptx
13 >>> data = {"title": "Analysis Report", "findings": [...]}
14 >>> export_pptx(data, "presentation.pptx")
15"""
17from __future__ import annotations
19import logging
20from dataclasses import dataclass, field
21from pathlib import Path
22from typing import Any
24logger = logging.getLogger(__name__)
26# Check if python-pptx is available
27try:
28 from pptx import Presentation
29 from pptx.util import Inches
31 PPTX_AVAILABLE = True
32except ImportError:
33 PPTX_AVAILABLE = False
34 logger.warning("python-pptx not available. Install with: pip install python-pptx")
37@dataclass
38class PPTXSlide:
39 """PowerPoint slide configuration.
41 Attributes:
42 title: Slide title
43 content: Slide content (text, bullet points, or image path)
44 layout: Slide layout type
45 notes: Speaker notes
46 chart_data: Optional chart data to embed
47 """
49 title: str
50 content: str | list[str] | Path = ""
51 layout: str = "title_content" # title, title_content, content, blank
52 notes: str = ""
53 chart_data: dict[str, Any] | None = None
56@dataclass
57class PPTXPresentation:
58 """PowerPoint presentation configuration.
60 Attributes:
61 title: Presentation title
62 subtitle: Presentation subtitle
63 author: Author name
64 slides: List of slides
65 template: Optional template file path
67 References:
68 REPORT-023: PowerPoint/PPTX Export
69 """
71 title: str
72 subtitle: str = ""
73 author: str = ""
74 slides: list[PPTXSlide] = field(default_factory=list)
75 template: Path | None = None
77 def add_slide(
78 self,
79 title: str,
80 content: str | list[str] | Path = "",
81 *,
82 layout: str = "title_content",
83 notes: str = "",
84 ) -> PPTXSlide:
85 """Add slide to presentation.
87 Args:
88 title: Slide title
89 content: Slide content
90 layout: Layout type
91 notes: Speaker notes
93 Returns:
94 Created slide object
95 """
96 slide = PPTXSlide(title=title, content=content, layout=layout, notes=notes)
97 self.slides.append(slide)
98 return slide
101def export_pptx(
102 report_data: dict[str, Any],
103 output_path: str | Path,
104 *,
105 title: str = "Analysis Report",
106 subtitle: str = "",
107 author: str = "",
108 template: Path | None = None,
109) -> Path:
110 """Export report data to PowerPoint presentation.
112 Args:
113 report_data: Report data dictionary containing:
114 - 'summary': Executive summary text
115 - 'findings': List of key findings
116 - 'measurements': Measurement results
117 - 'plots': List of plot image paths
118 output_path: Output PPTX file path
119 title: Presentation title
120 subtitle: Presentation subtitle
121 author: Author name
122 template: Optional template PPTX file
124 Returns:
125 Path to generated PPTX file
127 Example:
128 >>> data = {
129 ... 'summary': 'All tests passed',
130 ... 'findings': ['Rise time: 2.3ns', 'Fall time: 2.1ns'],
131 ... 'plots': [Path('plot1.png'), Path('plot2.png')]
132 ... }
133 >>> export_pptx(data, 'report.pptx', title='Signal Analysis')
135 References:
136 REPORT-023: PowerPoint/PPTX Export
137 """
138 output_path = Path(output_path)
140 if not PPTX_AVAILABLE: 140 ↛ 147line 140 didn't jump to line 147 because the condition on line 140 was always true
141 # Create stub file
142 logger.warning("Creating stub PPTX file (python-pptx not installed)")
143 _create_stub_pptx(output_path, title, report_data)
144 return output_path
146 # Create presentation
147 prs = Presentation(str(template)) if template else Presentation()
149 # Title slide
150 _add_title_slide(prs, title, subtitle, author)
152 # Summary slide
153 summary = report_data.get("summary", "")
154 if summary:
155 _add_summary_slide(prs, summary)
157 # Key findings slides
158 findings = report_data.get("findings", [])
159 if findings:
160 _add_findings_slide(prs, findings)
162 # Measurement results slides
163 measurements = report_data.get("measurements", [])
164 if measurements:
165 _add_measurement_slides(prs, measurements)
167 # Plot slides
168 plots = report_data.get("plots", [])
169 for i, plot_path in enumerate(plots, 1):
170 _add_plot_slide(prs, plot_path, f"Figure {i}")
172 # Save presentation
173 prs.save(str(output_path))
174 logger.info("Exported PPTX presentation to %s", output_path)
176 return output_path
179def _add_title_slide(
180 prs: Any,
181 title: str,
182 subtitle: str,
183 author: str,
184) -> None:
185 """Add title slide to presentation."""
186 if not PPTX_AVAILABLE:
187 return
189 slide = prs.slides.add_slide(prs.slide_layouts[0]) # Title slide layout
191 # Set title
192 if slide.shapes.title:
193 slide.shapes.title.text = title
195 # Set subtitle (placeholder 1)
196 if len(slide.placeholders) > 1:
197 subtitle_text = subtitle
198 if author:
199 subtitle_text += f"\n{author}"
200 slide.placeholders[1].text = subtitle_text
203def _add_summary_slide(prs: Any, summary: str) -> None:
204 """Add executive summary slide."""
205 if not PPTX_AVAILABLE:
206 return
208 slide = prs.slides.add_slide(prs.slide_layouts[1]) # Title and content layout
210 if slide.shapes.title:
211 slide.shapes.title.text = "Executive Summary"
213 # Add summary text
214 if len(slide.placeholders) > 1:
215 text_frame = slide.placeholders[1].text_frame
216 text_frame.text = summary
219def _add_findings_slide(prs: Any, findings: list[str]) -> None:
220 """Add key findings slide with bullet points."""
221 if not PPTX_AVAILABLE:
222 return
224 slide = prs.slides.add_slide(prs.slide_layouts[1]) # Title and content layout
226 if slide.shapes.title:
227 slide.shapes.title.text = "Key Findings"
229 # Add bullet points
230 if len(slide.placeholders) > 1:
231 text_frame = slide.placeholders[1].text_frame
232 text_frame.clear()
234 for finding in findings:
235 p = text_frame.add_paragraph()
236 p.text = finding
237 p.level = 0
240def _add_measurement_slides(prs: Any, measurements: list[dict[str, Any]]) -> None:
241 """Add measurement results slides."""
242 if not PPTX_AVAILABLE:
243 return
245 # Group measurements into slides (5 per slide)
246 chunk_size = 5
247 for i in range(0, len(measurements), chunk_size):
248 chunk = measurements[i : i + chunk_size]
250 slide = prs.slides.add_slide(prs.slide_layouts[1])
252 if slide.shapes.title:
253 slide.shapes.title.text = f"Measurement Results ({i + 1}-{i + len(chunk)})"
255 # Add measurements as bullet points
256 if len(slide.placeholders) > 1:
257 text_frame = slide.placeholders[1].text_frame
258 text_frame.clear()
260 for meas in chunk:
261 p = text_frame.add_paragraph()
262 name = meas.get("name", "Unknown")
263 value = meas.get("value", "")
264 unit = meas.get("unit", "")
265 status = meas.get("status", "")
267 p.text = f"{name}: {value} {unit} {status}"
268 p.level = 0
271def _add_plot_slide(prs: Any, plot_path: Path, caption: str) -> None:
272 """Add slide with embedded plot image."""
273 if not PPTX_AVAILABLE:
274 return
276 slide = prs.slides.add_slide(prs.slide_layouts[5]) # Blank layout
278 # Add title
279 if slide.shapes.title:
280 slide.shapes.title.text = caption
282 # Add image (centered)
283 if plot_path.exists():
284 left = Inches(1.5)
285 top = Inches(2)
286 width = Inches(7)
288 slide.shapes.add_picture(str(plot_path), left, top, width=width)
291def _create_stub_pptx(
292 output_path: Path,
293 title: str,
294 report_data: dict[str, Any],
295) -> None:
296 """Create stub PPTX file when python-pptx not available."""
297 # Write text file with .pptx extension as placeholder
298 with output_path.open("w") as f:
299 f.write("PowerPoint Export Stub\n")
300 f.write("======================\n\n")
301 f.write(f"Title: {title}\n\n")
302 f.write("Install python-pptx for full PPTX export:\n")
303 f.write(" pip install python-pptx\n\n")
304 f.write("Report Data Summary:\n")
305 f.write(f" - Summary: {report_data.get('summary', 'N/A')}\n")
306 f.write(f" - Findings: {len(report_data.get('findings', []))} items\n")
307 f.write(f" - Measurements: {len(report_data.get('measurements', []))} items\n")
308 f.write(f" - Plots: {len(report_data.get('plots', []))} items\n")
311def generate_presentation_from_report(
312 report: dict[str, Any],
313 output_path: str | Path,
314 *,
315 presentation_config: PPTXPresentation | None = None,
316) -> Path:
317 """Generate PowerPoint presentation from report structure.
319 Args:
320 report: Report dictionary with standard structure
321 output_path: Output file path
322 presentation_config: Optional presentation configuration
324 Returns:
325 Path to generated presentation
327 References:
328 REPORT-023: PowerPoint/PPTX Export
329 """
330 if presentation_config is None:
331 presentation_config = PPTXPresentation(
332 title=report.get("title", "Analysis Report"),
333 subtitle=report.get("subtitle", ""),
334 author=report.get("author", ""),
335 )
337 # Extract data from report
338 report_data = {
339 "summary": report.get("executive_summary", ""),
340 "findings": report.get("key_findings", []),
341 "measurements": report.get("measurements", []),
342 "plots": report.get("plot_paths", []),
343 }
345 return export_pptx(
346 report_data,
347 output_path,
348 title=presentation_config.title,
349 subtitle=presentation_config.subtitle,
350 author=presentation_config.author,
351 template=presentation_config.template,
352 )
355__all__ = [
356 "PPTXPresentation",
357 "PPTXSlide",
358 "export_pptx",
359 "generate_presentation_from_report",
360]