Spaces:
Running
Running
| """ | |
| Report Export Service. | |
| Generates PDF and Word documents from research report data. | |
| Uses ReportLab for PDF and python-docx for Word. | |
| """ | |
| from __future__ import annotations | |
| import io | |
| import logging | |
| from datetime import datetime | |
| from typing import Any, Dict | |
| from reportlab.lib.pagesizes import A4 | |
| from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle | |
| from reportlab.lib.colors import HexColor | |
| from reportlab.platypus import ( | |
| SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, HRFlowable | |
| ) | |
| from reportlab.lib.units import inch | |
| from reportlab.lib.enums import TA_CENTER, TA_LEFT | |
| from docx import Document | |
| from docx.shared import Inches, Pt, Cm, RGBColor | |
| from docx.enum.text import WD_ALIGN_PARAGRAPH | |
| logger = logging.getLogger(__name__) | |
| ACCENT = HexColor("#005241") | |
| ACCENT_LIGHT = HexColor("#e8f5e9") | |
| def generate_pdf(report: Dict[str, Any]) -> io.BytesIO: | |
| """Generate a PDF report from report data.""" | |
| buffer = io.BytesIO() | |
| doc = SimpleDocTemplate( | |
| buffer, pagesize=A4, | |
| topMargin=0.75 * inch, bottomMargin=0.75 * inch, | |
| leftMargin=0.85 * inch, rightMargin=0.85 * inch, | |
| ) | |
| styles = getSampleStyleSheet() | |
| # Custom styles | |
| title_style = ParagraphStyle( | |
| "ReportTitle", parent=styles["Title"], | |
| fontSize=22, spaceAfter=6, textColor=ACCENT, | |
| fontName="Helvetica-Bold", | |
| ) | |
| subtitle_style = ParagraphStyle( | |
| "ReportSubtitle", parent=styles["Normal"], | |
| fontSize=10, textColor=HexColor("#6b7280"), spaceAfter=20, | |
| ) | |
| heading_style = ParagraphStyle( | |
| "ReportHeading", parent=styles["Heading2"], | |
| fontSize=14, textColor=ACCENT, spaceBefore=16, spaceAfter=8, | |
| fontName="Helvetica-Bold", | |
| ) | |
| body_style = ParagraphStyle( | |
| "ReportBody", parent=styles["Normal"], | |
| fontSize=10, leading=15, spaceAfter=8, | |
| ) | |
| elements = [] | |
| # Title | |
| title = report.get("title", "Research Report") | |
| elements.append(Paragraph(title, title_style)) | |
| # Metadata | |
| timestamp = report.get("created_at", datetime.utcnow().isoformat()) | |
| insight_type = report.get("insight_type", "general") | |
| meta = f"Generated: {timestamp[:19]} · Type: {insight_type.replace('_', ' ').title()}" | |
| elements.append(Paragraph(meta, subtitle_style)) | |
| # Divider | |
| elements.append(HRFlowable( | |
| width="100%", thickness=1.5, color=ACCENT, spaceBefore=4, spaceAfter=12, | |
| )) | |
| # Summary | |
| summary = report.get("summary", "") | |
| if summary: | |
| elements.append(Paragraph("Executive Summary", heading_style)) | |
| elements.append(Paragraph(summary, body_style)) | |
| elements.append(Spacer(1, 8)) | |
| # Content | |
| content = report.get("content", "") | |
| if content: | |
| elements.append(Paragraph("Detailed Analysis", heading_style)) | |
| for paragraph in content.split("\n\n"): | |
| paragraph = paragraph.strip() | |
| if not paragraph: | |
| continue | |
| if paragraph.startswith("##"): | |
| elements.append(Paragraph(paragraph.lstrip("#").strip(), heading_style)) | |
| elif paragraph.startswith("- ") or paragraph.startswith("* "): | |
| for line in paragraph.split("\n"): | |
| bullet_text = line.lstrip("-* ").strip() | |
| if bullet_text: | |
| elements.append(Paragraph(f"• {bullet_text}", body_style)) | |
| else: | |
| elements.append(Paragraph(paragraph, body_style)) | |
| elements.append(Spacer(1, 8)) | |
| # Tickers | |
| tickers = report.get("tickers_json", "") | |
| if tickers and tickers != "[]": | |
| import json | |
| try: | |
| ticker_list = json.loads(tickers) if isinstance(tickers, str) else tickers | |
| if ticker_list: | |
| elements.append(Paragraph("Tickers Analyzed", heading_style)) | |
| elements.append(Paragraph(", ".join(ticker_list), body_style)) | |
| elements.append(Spacer(1, 8)) | |
| except Exception: | |
| pass | |
| # Footer | |
| elements.append(Spacer(1, 20)) | |
| elements.append(HRFlowable( | |
| width="100%", thickness=0.5, color=HexColor("#d1d5db"), spaceBefore=8, spaceAfter=8, | |
| )) | |
| footer_style = ParagraphStyle( | |
| "Footer", parent=styles["Normal"], | |
| fontSize=8, textColor=HexColor("#9ca3af"), alignment=TA_CENTER, | |
| ) | |
| elements.append(Paragraph( | |
| f"QuantHedge Research Report · Generated {datetime.utcnow().strftime('%Y-%m-%d %H:%M UTC')}", | |
| footer_style, | |
| )) | |
| doc.build(elements) | |
| buffer.seek(0) | |
| return buffer | |
| def generate_docx(report: Dict[str, Any]) -> io.BytesIO: | |
| """Generate a Word document from report data.""" | |
| doc = Document() | |
| # Set default font | |
| style = doc.styles["Normal"] | |
| font = style.font | |
| font.name = "Calibri" | |
| font.size = Pt(10) | |
| # Title | |
| title = report.get("title", "Research Report") | |
| heading = doc.add_heading(title, level=0) | |
| for run in heading.runs: | |
| run.font.color.rgb = RGBColor(0, 82, 65) | |
| # Metadata | |
| timestamp = report.get("created_at", datetime.utcnow().isoformat()) | |
| insight_type = report.get("insight_type", "general") | |
| meta = doc.add_paragraph() | |
| meta_run = meta.add_run(f"Generated: {timestamp[:19]} · Type: {insight_type.replace('_', ' ').title()}") | |
| meta_run.font.size = Pt(9) | |
| meta_run.font.color.rgb = RGBColor(107, 114, 128) | |
| doc.add_paragraph("_" * 60) | |
| # Summary | |
| summary = report.get("summary", "") | |
| if summary: | |
| h = doc.add_heading("Executive Summary", level=1) | |
| for run in h.runs: | |
| run.font.color.rgb = RGBColor(0, 82, 65) | |
| doc.add_paragraph(summary) | |
| # Content | |
| content = report.get("content", "") | |
| if content: | |
| h = doc.add_heading("Detailed Analysis", level=1) | |
| for run in h.runs: | |
| run.font.color.rgb = RGBColor(0, 82, 65) | |
| for paragraph in content.split("\n\n"): | |
| paragraph = paragraph.strip() | |
| if not paragraph: | |
| continue | |
| if paragraph.startswith("##"): | |
| h = doc.add_heading(paragraph.lstrip("#").strip(), level=2) | |
| for run in h.runs: | |
| run.font.color.rgb = RGBColor(0, 82, 65) | |
| elif paragraph.startswith("- ") or paragraph.startswith("* "): | |
| for line in paragraph.split("\n"): | |
| bullet_text = line.lstrip("-* ").strip() | |
| if bullet_text: | |
| doc.add_paragraph(bullet_text, style="List Bullet") | |
| else: | |
| doc.add_paragraph(paragraph) | |
| # Tickers | |
| tickers = report.get("tickers_json", "") | |
| if tickers and tickers != "[]": | |
| import json | |
| try: | |
| ticker_list = json.loads(tickers) if isinstance(tickers, str) else tickers | |
| if ticker_list: | |
| h = doc.add_heading("Tickers Analyzed", level=1) | |
| for run in h.runs: | |
| run.font.color.rgb = RGBColor(0, 82, 65) | |
| doc.add_paragraph(", ".join(ticker_list)) | |
| except Exception: | |
| pass | |
| # Footer | |
| doc.add_paragraph("_" * 60) | |
| footer = doc.add_paragraph() | |
| footer.alignment = WD_ALIGN_PARAGRAPH.CENTER | |
| run = footer.add_run(f"QuantHedge Research Report · Generated {datetime.utcnow().strftime('%Y-%m-%d %H:%M UTC')}") | |
| run.font.size = Pt(8) | |
| run.font.color.rgb = RGBColor(156, 163, 175) | |
| buffer = io.BytesIO() | |
| doc.save(buffer) | |
| buffer.seek(0) | |
| return buffer | |