quanthedge / backend /app /services /export /report_export.py
jashdoshi77's picture
QuantHedge: Full deployment with Docker + nginx + uvicorn
9d29748
"""
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