""" PDF Generator for TradingAgents Analysis Reports This module generates PDF reports from analysis results using WeasyPrint. """ import os import logging from pathlib import Path from typing import Dict, Any, Optional from datetime import datetime try: from playwright.sync_api import sync_playwright PLAYWRIGHT_AVAILABLE = True except (ImportError, OSError) as e: PLAYWRIGHT_AVAILABLE = False logging.warning(f"Playwright not available: {str(e)}. Trying WeasyPrint fallback.") try: from weasyprint import HTML, CSS from weasyprint.text.fonts import FontConfiguration WEASYPRINT_AVAILABLE = True except (ImportError, OSError) as e: WEASYPRINT_AVAILABLE = False HTML = None CSS = None FontConfiguration = None if not PLAYWRIGHT_AVAILABLE: logging.warning(f"WeasyPrint also not available: {str(e)}. PDF generation will be disabled.") from ..converters.html_converter import RichToHTMLConverter from ..formatters.report_formatter import ReportFormatter class TradingReportPDFGenerator: """Generates PDF reports from TradingAgents analysis results.""" def __init__(self, config: Optional[Dict[str, Any]] = None): """ Initialize PDF generator. Args: config: Configuration dictionary for PDF generation """ self.config = config or self._default_config() self.html_converter = RichToHTMLConverter() self.report_formatter = ReportFormatter() self.logger = logging.getLogger(__name__) if not WEASYPRINT_AVAILABLE: self.logger.warning("WeasyPrint not available. PDF generation disabled.") def _default_config(self) -> Dict[str, Any]: """Get default PDF generation configuration.""" return { "enabled": True, "output_dir": "results", "page_format": "A4", "margin": "2cm", "font_family": "Arial, sans-serif", "font_size": "12pt" } def generate_pdf(self, analysis_results: Dict[str, Any], ticker: str, date: str, output_path: Optional[str] = None) -> Optional[str]: """ Generate PDF report from analysis results. Args: analysis_results: Complete analysis results dictionary ticker: Stock ticker symbol date: Analysis date output_path: Optional custom output path Returns: Path to generated PDF file, or None if generation failed """ if not self.is_available(): self.logger.error("No PDF generation method available.") return None if not self.config.get("enabled", True): self.logger.info("PDF generation disabled in configuration.") return None try: # Generate output path if not provided if not output_path: output_path = self._generate_output_path(ticker, date) # Ensure output directory exists os.makedirs(os.path.dirname(output_path), exist_ok=True) # Format the report content html_content = self._create_html_report(analysis_results, ticker, date) # Generate PDF using available method success = self._html_to_pdf(html_content, output_path) if success: # Also save HTML backup html_path = output_path.replace('.pdf', '.html') with open(html_path, 'w', encoding='utf-8') as f: f.write(html_content) self.logger.info(f"PDF report generated: {output_path}") return output_path else: return None except Exception as e: self.logger.error(f"Failed to generate PDF report: {str(e)}") return None def _generate_output_path(self, ticker: str, date: str) -> str: """Generate output path for PDF file.""" output_dir = Path(self.config["output_dir"]) / ticker / date return str(output_dir / "analysis_report.pdf") def _create_html_report(self, analysis_results: Dict[str, Any], ticker: str, date: str) -> str: """ Create complete HTML report from analysis results. Args: analysis_results: Analysis results dictionary ticker: Stock ticker symbol date: Analysis date Returns: Complete HTML content for PDF generation """ # Format the main report content report_content = self.report_formatter.format_complete_report( analysis_results, ticker, date ) # Wrap in complete HTML document with styles html_template = f""" TradingAgents Analysis: {ticker} {self._get_pdf_styles()} {report_content} """ return html_template def _get_pdf_styles(self) -> str: """Get CSS styles for PDF generation.""" return """ """ def _html_to_pdf(self, html_content: str, output_path: str) -> bool: """ Convert HTML content to PDF using available method. Args: html_content: HTML content to convert output_path: Output PDF file path Returns: True if successful, False otherwise """ # Try Playwright first (preferred method) if PLAYWRIGHT_AVAILABLE: try: return self._html_to_pdf_playwright(html_content, output_path) except Exception as e: self.logger.warning(f"Playwright PDF generation failed: {str(e)}. Trying WeasyPrint fallback.") # Fallback to WeasyPrint if WEASYPRINT_AVAILABLE: try: return self._html_to_pdf_weasyprint(html_content, output_path) except Exception as e: self.logger.error(f"WeasyPrint PDF generation also failed: {str(e)}") return False def _html_to_pdf_playwright(self, html_content: str, output_path: str) -> bool: """ Convert HTML content to PDF using Playwright. Args: html_content: HTML content to convert output_path: Output PDF file path Returns: True if successful, False otherwise """ try: with sync_playwright() as p: browser = p.chromium.launch(headless=True) page = browser.new_page() # Set content and wait for it to load page.set_content(html_content, wait_until='networkidle') # Generate PDF with options page.pdf( path=output_path, format='A4', margin={ 'top': '2cm', 'right': '2cm', 'bottom': '2cm', 'left': '2cm' }, print_background=True, display_header_footer=True, header_template='
TradingAgents Analysis Report
', footer_template='
/
' ) browser.close() self.logger.info("PDF generated successfully using Playwright") return True except Exception as e: self.logger.error(f"Playwright PDF generation failed: {str(e)}") return False def _html_to_pdf_weasyprint(self, html_content: str, output_path: str) -> bool: """ Convert HTML content to PDF using WeasyPrint. Args: html_content: HTML content to convert output_path: Output PDF file path Returns: True if successful, False otherwise """ try: # Create font configuration font_config = FontConfiguration() # Generate PDF html_doc = HTML(string=html_content) html_doc.write_pdf( output_path, font_config=font_config, optimize_images=True ) self.logger.info("PDF generated successfully using WeasyPrint") return True except Exception as e: self.logger.error(f"WeasyPrint conversion failed: {str(e)}") # Try without font configuration as fallback try: html_doc = HTML(string=html_content) html_doc.write_pdf(output_path) self.logger.info("PDF generated successfully using WeasyPrint (fallback mode)") return True except Exception as fallback_error: self.logger.error(f"WeasyPrint fallback also failed: {str(fallback_error)}") return False def is_available(self) -> bool: """Check if PDF generation is available.""" return (PLAYWRIGHT_AVAILABLE or WEASYPRINT_AVAILABLE) and self.config.get("enabled", True)