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

1"""PowerPoint presentation export for TraceKit reports. 

2 

3This module provides PPTX generation for stakeholder presentations 

4with automated slide layouts, embedded plots, and speaker notes. 

5 

6 

7Note: 

8 This is a stub implementation. For full functionality, install python-pptx: 

9 pip install python-pptx 

10 

11Example: 

12 >>> from tracekit.reporting import export_pptx 

13 >>> data = {"title": "Analysis Report", "findings": [...]} 

14 >>> export_pptx(data, "presentation.pptx") 

15""" 

16 

17from __future__ import annotations 

18 

19import logging 

20from dataclasses import dataclass, field 

21from pathlib import Path 

22from typing import Any 

23 

24logger = logging.getLogger(__name__) 

25 

26# Check if python-pptx is available 

27try: 

28 from pptx import Presentation 

29 from pptx.util import Inches 

30 

31 PPTX_AVAILABLE = True 

32except ImportError: 

33 PPTX_AVAILABLE = False 

34 logger.warning("python-pptx not available. Install with: pip install python-pptx") 

35 

36 

37@dataclass 

38class PPTXSlide: 

39 """PowerPoint slide configuration. 

40 

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 """ 

48 

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 

54 

55 

56@dataclass 

57class PPTXPresentation: 

58 """PowerPoint presentation configuration. 

59 

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 

66 

67 References: 

68 REPORT-023: PowerPoint/PPTX Export 

69 """ 

70 

71 title: str 

72 subtitle: str = "" 

73 author: str = "" 

74 slides: list[PPTXSlide] = field(default_factory=list) 

75 template: Path | None = None 

76 

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. 

86 

87 Args: 

88 title: Slide title 

89 content: Slide content 

90 layout: Layout type 

91 notes: Speaker notes 

92 

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 

99 

100 

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. 

111 

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 

123 

124 Returns: 

125 Path to generated PPTX file 

126 

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') 

134 

135 References: 

136 REPORT-023: PowerPoint/PPTX Export 

137 """ 

138 output_path = Path(output_path) 

139 

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 

145 

146 # Create presentation 

147 prs = Presentation(str(template)) if template else Presentation() 

148 

149 # Title slide 

150 _add_title_slide(prs, title, subtitle, author) 

151 

152 # Summary slide 

153 summary = report_data.get("summary", "") 

154 if summary: 

155 _add_summary_slide(prs, summary) 

156 

157 # Key findings slides 

158 findings = report_data.get("findings", []) 

159 if findings: 

160 _add_findings_slide(prs, findings) 

161 

162 # Measurement results slides 

163 measurements = report_data.get("measurements", []) 

164 if measurements: 

165 _add_measurement_slides(prs, measurements) 

166 

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}") 

171 

172 # Save presentation 

173 prs.save(str(output_path)) 

174 logger.info("Exported PPTX presentation to %s", output_path) 

175 

176 return output_path 

177 

178 

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 

188 

189 slide = prs.slides.add_slide(prs.slide_layouts[0]) # Title slide layout 

190 

191 # Set title 

192 if slide.shapes.title: 

193 slide.shapes.title.text = title 

194 

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 

201 

202 

203def _add_summary_slide(prs: Any, summary: str) -> None: 

204 """Add executive summary slide.""" 

205 if not PPTX_AVAILABLE: 

206 return 

207 

208 slide = prs.slides.add_slide(prs.slide_layouts[1]) # Title and content layout 

209 

210 if slide.shapes.title: 

211 slide.shapes.title.text = "Executive Summary" 

212 

213 # Add summary text 

214 if len(slide.placeholders) > 1: 

215 text_frame = slide.placeholders[1].text_frame 

216 text_frame.text = summary 

217 

218 

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 

223 

224 slide = prs.slides.add_slide(prs.slide_layouts[1]) # Title and content layout 

225 

226 if slide.shapes.title: 

227 slide.shapes.title.text = "Key Findings" 

228 

229 # Add bullet points 

230 if len(slide.placeholders) > 1: 

231 text_frame = slide.placeholders[1].text_frame 

232 text_frame.clear() 

233 

234 for finding in findings: 

235 p = text_frame.add_paragraph() 

236 p.text = finding 

237 p.level = 0 

238 

239 

240def _add_measurement_slides(prs: Any, measurements: list[dict[str, Any]]) -> None: 

241 """Add measurement results slides.""" 

242 if not PPTX_AVAILABLE: 

243 return 

244 

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] 

249 

250 slide = prs.slides.add_slide(prs.slide_layouts[1]) 

251 

252 if slide.shapes.title: 

253 slide.shapes.title.text = f"Measurement Results ({i + 1}-{i + len(chunk)})" 

254 

255 # Add measurements as bullet points 

256 if len(slide.placeholders) > 1: 

257 text_frame = slide.placeholders[1].text_frame 

258 text_frame.clear() 

259 

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", "") 

266 

267 p.text = f"{name}: {value} {unit} {status}" 

268 p.level = 0 

269 

270 

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 

275 

276 slide = prs.slides.add_slide(prs.slide_layouts[5]) # Blank layout 

277 

278 # Add title 

279 if slide.shapes.title: 

280 slide.shapes.title.text = caption 

281 

282 # Add image (centered) 

283 if plot_path.exists(): 

284 left = Inches(1.5) 

285 top = Inches(2) 

286 width = Inches(7) 

287 

288 slide.shapes.add_picture(str(plot_path), left, top, width=width) 

289 

290 

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") 

309 

310 

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. 

318 

319 Args: 

320 report: Report dictionary with standard structure 

321 output_path: Output file path 

322 presentation_config: Optional presentation configuration 

323 

324 Returns: 

325 Path to generated presentation 

326 

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 ) 

336 

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 } 

344 

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 ) 

353 

354 

355__all__ = [ 

356 "PPTXPresentation", 

357 "PPTXSlide", 

358 "export_pptx", 

359 "generate_presentation_from_report", 

360]