Soumik Bose commited on
Commit ·
d54e4f5
1
Parent(s): 1539a3f
go
Browse files- pdf_report_generation_helper.py +4 -25
- pdf_report_generation_service.py +3 -208
pdf_report_generation_helper.py
CHANGED
|
@@ -6,9 +6,9 @@ import os
|
|
| 6 |
import uuid
|
| 7 |
import logging
|
| 8 |
from typing import List, Dict, Any
|
| 9 |
-
from pathlib import Path
|
| 10 |
from starlette.concurrency import run_in_threadpool
|
| 11 |
|
|
|
|
| 12 |
from pdf_report_generation_service import Q4ReportGenerator
|
| 13 |
|
| 14 |
logger = logging.getLogger(__name__)
|
|
@@ -16,7 +16,7 @@ logger = logging.getLogger(__name__)
|
|
| 16 |
def _upload_to_supabase_sync(file_path: str, file_name: str, chat_id: str) -> str:
|
| 17 |
"""
|
| 18 |
Synchronous wrapper for Supabase upload.
|
| 19 |
-
Import
|
| 20 |
"""
|
| 21 |
from supabase_service import upload_file_to_supabase
|
| 22 |
return upload_file_to_supabase(
|
|
@@ -92,24 +92,7 @@ async def generate_and_upload_report(
|
|
| 92 |
confidential: bool = True
|
| 93 |
) -> str:
|
| 94 |
"""
|
| 95 |
-
Generates a PDF report
|
| 96 |
-
and returns the public URL. Cleans up temporary files afterwards.
|
| 97 |
-
|
| 98 |
-
Args:
|
| 99 |
-
config_content: List of section dictionaries for the report
|
| 100 |
-
file_name: Desired filename (without or with .pdf extension)
|
| 101 |
-
chat_id: Chat ID for Supabase storage organization
|
| 102 |
-
title: Report title
|
| 103 |
-
subtitle: Report subtitle
|
| 104 |
-
author: Report author
|
| 105 |
-
department: Department name
|
| 106 |
-
output_dir: Directory for temporary file storage
|
| 107 |
-
|
| 108 |
-
Returns:
|
| 109 |
-
str: Public URL of the uploaded PDF
|
| 110 |
-
|
| 111 |
-
Raises:
|
| 112 |
-
Exception: If PDF generation or upload fails
|
| 113 |
"""
|
| 114 |
|
| 115 |
local_file_path = None
|
|
@@ -118,11 +101,9 @@ async def generate_and_upload_report(
|
|
| 118 |
# 1. Setup paths
|
| 119 |
os.makedirs(output_dir, exist_ok=True)
|
| 120 |
|
| 121 |
-
# Ensure filename ends with .pdf
|
| 122 |
if not file_name.endswith('.pdf'):
|
| 123 |
file_name += '.pdf'
|
| 124 |
|
| 125 |
-
# Create unique filename to prevent collisions
|
| 126 |
unique_id = str(uuid.uuid4())[:8]
|
| 127 |
local_filename = f"{os.path.splitext(file_name)[0]}_{unique_id}.pdf"
|
| 128 |
local_file_path = os.path.join(output_dir, local_filename)
|
|
@@ -177,13 +158,11 @@ async def generate_and_upload_report(
|
|
| 177 |
raise Exception(f"Failed to generate and upload report: {str(e)}")
|
| 178 |
|
| 179 |
finally:
|
| 180 |
-
# 5. Cleanup
|
| 181 |
try:
|
| 182 |
-
# Delete local PDF
|
| 183 |
if local_file_path:
|
| 184 |
_cleanup_files([local_file_path])
|
| 185 |
|
| 186 |
-
# Delete downloaded images
|
| 187 |
if 'generator' in locals():
|
| 188 |
await run_in_threadpool(
|
| 189 |
_cleanup_downloaded_images,
|
|
|
|
| 6 |
import uuid
|
| 7 |
import logging
|
| 8 |
from typing import List, Dict, Any
|
|
|
|
| 9 |
from starlette.concurrency import run_in_threadpool
|
| 10 |
|
| 11 |
+
# The Service file defines the Generator class
|
| 12 |
from pdf_report_generation_service import Q4ReportGenerator
|
| 13 |
|
| 14 |
logger = logging.getLogger(__name__)
|
|
|
|
| 16 |
def _upload_to_supabase_sync(file_path: str, file_name: str, chat_id: str) -> str:
|
| 17 |
"""
|
| 18 |
Synchronous wrapper for Supabase upload.
|
| 19 |
+
Import inside function to avoid potential circular dependencies if supabase_service changes.
|
| 20 |
"""
|
| 21 |
from supabase_service import upload_file_to_supabase
|
| 22 |
return upload_file_to_supabase(
|
|
|
|
| 92 |
confidential: bool = True
|
| 93 |
) -> str:
|
| 94 |
"""
|
| 95 |
+
Generates a PDF report, uploads it to Supabase, and returns the public URL.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 96 |
"""
|
| 97 |
|
| 98 |
local_file_path = None
|
|
|
|
| 101 |
# 1. Setup paths
|
| 102 |
os.makedirs(output_dir, exist_ok=True)
|
| 103 |
|
|
|
|
| 104 |
if not file_name.endswith('.pdf'):
|
| 105 |
file_name += '.pdf'
|
| 106 |
|
|
|
|
| 107 |
unique_id = str(uuid.uuid4())[:8]
|
| 108 |
local_filename = f"{os.path.splitext(file_name)[0]}_{unique_id}.pdf"
|
| 109 |
local_file_path = os.path.join(output_dir, local_filename)
|
|
|
|
| 158 |
raise Exception(f"Failed to generate and upload report: {str(e)}")
|
| 159 |
|
| 160 |
finally:
|
| 161 |
+
# 5. Cleanup
|
| 162 |
try:
|
|
|
|
| 163 |
if local_file_path:
|
| 164 |
_cleanup_files([local_file_path])
|
| 165 |
|
|
|
|
| 166 |
if 'generator' in locals():
|
| 167 |
await run_in_threadpool(
|
| 168 |
_cleanup_downloaded_images,
|
pdf_report_generation_service.py
CHANGED
|
@@ -275,13 +275,6 @@ strong {
|
|
| 275 |
def fetch_image(url: str, output_dir: str) -> Optional[str]:
|
| 276 |
"""
|
| 277 |
Download image from URL to persistent file for PDF rendering.
|
| 278 |
-
|
| 279 |
-
Args:
|
| 280 |
-
url: The URL of the image
|
| 281 |
-
output_dir: Directory to save downloaded images
|
| 282 |
-
|
| 283 |
-
Returns:
|
| 284 |
-
File URI string or None if failed
|
| 285 |
"""
|
| 286 |
if not url.startswith(('http://', 'https://')):
|
| 287 |
if os.path.exists(url):
|
|
@@ -299,7 +292,6 @@ def fetch_image(url: str, output_dir: str) -> Optional[str]:
|
|
| 299 |
response = requests.get(url, headers=headers, timeout=15)
|
| 300 |
response.raise_for_status()
|
| 301 |
|
| 302 |
-
# Determine file extension
|
| 303 |
content_type = response.headers.get('content-type', '')
|
| 304 |
ext = '.jpg'
|
| 305 |
if 'png' in content_type:
|
|
@@ -454,206 +446,9 @@ class Q4ReportGenerator:
|
|
| 454 |
|
| 455 |
html_doc.write_pdf(output_path, stylesheets=[css_doc])
|
| 456 |
|
| 457 |
-
|
| 458 |
-
file_size = os.path.getsize(output_path) / 1024
|
| 459 |
-
|
| 460 |
-
print(f"\n{'='*70}")
|
| 461 |
-
print(f"✅ PDF GENERATED SUCCESSFULLY!")
|
| 462 |
-
print(f"{'='*70}")
|
| 463 |
-
print(f"📁 Location: {abs_path}")
|
| 464 |
-
print(f"📊 Size: {file_size:.2f} KB")
|
| 465 |
-
print(f"🖼️ Images: {len(self.downloaded_images)} embedded")
|
| 466 |
-
print(f"{'='*70}\n")
|
| 467 |
-
|
| 468 |
return True
|
| 469 |
|
| 470 |
except Exception as e:
|
| 471 |
-
logger.error(f"
|
| 472 |
-
return False
|
| 473 |
-
|
| 474 |
-
# ---------------------------------------------------------
|
| 475 |
-
# COMPLETE TEST CASE WITH FULL Q4 REPORT
|
| 476 |
-
# ---------------------------------------------------------
|
| 477 |
-
# if __name__ == "__main__":
|
| 478 |
-
|
| 479 |
-
# q4_sections = [
|
| 480 |
-
# {
|
| 481 |
-
# "content": """
|
| 482 |
-
# # Executive Summary
|
| 483 |
-
|
| 484 |
-
# This report presents the comprehensive Q4 2025 performance analysis for our customer analytics division. The quarter demonstrated strong growth across key metrics with notable improvements in customer engagement and retention.
|
| 485 |
-
|
| 486 |
-
# <div class="key-metric-box">
|
| 487 |
-
|
| 488 |
-
# ### Key Q4 Highlights
|
| 489 |
-
|
| 490 |
-
# - **Revenue Growth**: 23.4% increase over Q3 2025
|
| 491 |
-
# - **Customer Acquisition**: 8,547 new customers (+18% QoQ)
|
| 492 |
-
# - **Retention Rate**: 94.2% (industry-leading performance)
|
| 493 |
-
# - **Data Quality Score**: 98.7% completeness
|
| 494 |
-
|
| 495 |
-
# </div>
|
| 496 |
-
|
| 497 |
-
# > "Q4 2025 marks our strongest quarter to date, with record-breaking performance across all major KPIs and successful implementation of our new customer engagement strategy."
|
| 498 |
-
|
| 499 |
-
# ## Quarter at a Glance
|
| 500 |
-
|
| 501 |
-
# The analytics team processed over 2.3 million customer interactions, generating actionable insights that drove strategic decisions across the organization.
|
| 502 |
-
# """
|
| 503 |
-
# },
|
| 504 |
-
# {
|
| 505 |
-
# "content": """
|
| 506 |
-
# # Performance Metrics
|
| 507 |
-
|
| 508 |
-
# ## Overall Q4 Performance
|
| 509 |
-
|
| 510 |
-
# | Metric | Q4 2025 | Q3 2025 | Change | Target |
|
| 511 |
-
# |--------|---------|---------|--------|--------|
|
| 512 |
-
# | Total Revenue | $4.2M | $3.4M | +23.4% | $4.0M |
|
| 513 |
-
# | New Customers | 8,547 | 7,243 | +18.0% | 8,000 |
|
| 514 |
-
# | Active Users | 45,892 | 42,156 | +8.9% | 44,000 |
|
| 515 |
-
# | Retention Rate | 94.2% | 92.8% | +1.4pp | 93.0% |
|
| 516 |
-
# | Avg. Order Value | $127 | $118 | +7.6% | $120 |
|
| 517 |
-
|
| 518 |
-
# <div class="highlight-box">
|
| 519 |
-
|
| 520 |
-
# **Performance Assessment**: All key metrics exceeded quarterly targets, demonstrating the effectiveness of our data-driven strategy and customer-focused initiatives.
|
| 521 |
-
|
| 522 |
-
# </div>
|
| 523 |
-
|
| 524 |
-
# ## Channel Performance Analysis
|
| 525 |
-
|
| 526 |
-
# The following visualization shows the distribution of customer orders across different channels in Q4 2025.
|
| 527 |
-
# """,
|
| 528 |
-
# "images": [
|
| 529 |
-
# {
|
| 530 |
-
# "url": "https://quickchart.io/chart?c={type:'bar',data:{labels:['Mobile App','Desktop Web','Mobile Web'],datasets:[{label:'Q4 Orders (%)',data:[52,35,13],backgroundColor:['rgba(0,51,102,0.8)','rgba(0,85,165,0.8)','rgba(52,152,219,0.8)']}]},options:{title:{display:true,text:'Q4 2025 Channel Distribution'},scales:{y:{beginAtZero:true,max:60}},plugins:{legend:{display:true}}}}",
|
| 531 |
-
# "caption": "Figure 1: Customer Orders by Channel - Q4 2025"
|
| 532 |
-
# }
|
| 533 |
-
# ],
|
| 534 |
-
# "page_break": True
|
| 535 |
-
# },
|
| 536 |
-
# {
|
| 537 |
-
# "content": """
|
| 538 |
-
# # Customer Analytics Deep Dive
|
| 539 |
-
|
| 540 |
-
# ## Demographic Insights
|
| 541 |
-
|
| 542 |
-
# Our Q4 analysis reveals significant shifts in customer demographics and behavior patterns.
|
| 543 |
-
|
| 544 |
-
# ### Age Distribution
|
| 545 |
-
|
| 546 |
-
# | Age Group | Customers | % of Total | Avg. Spend | Growth (QoQ) |
|
| 547 |
-
# |-----------|-----------|------------|------------|--------------|
|
| 548 |
-
# | 18-24 | 8,234 | 17.9% | $89 | +22% |
|
| 549 |
-
# | 25-34 | 15,678 | 34.2% | $142 | +15% |
|
| 550 |
-
# | 35-44 | 12,456 | 27.1% | $156 | +11% |
|
| 551 |
-
# | 45-54 | 6,789 | 14.8% | $178 | +8% |
|
| 552 |
-
# | 55+ | 2,735 | 6.0% | $203 | +5% |
|
| 553 |
-
|
| 554 |
-
# ## Revenue Trend Analysis
|
| 555 |
-
|
| 556 |
-
# Below is the revenue trend across Q4 months showing consistent growth:
|
| 557 |
-
# """,
|
| 558 |
-
# "images": [
|
| 559 |
-
# {
|
| 560 |
-
# "url": "https://quickchart.io/chart?c={type:'line',data:{labels:['October','November','December'],datasets:[{label:'Revenue ($M)',data:[1.2,1.5,1.5],borderColor:'rgb(0,51,102)',backgroundColor:'rgba(0,51,102,0.1)',fill:true}]},options:{title:{display:true,text:'Q4 2025 Monthly Revenue Trend'},scales:{y:{beginAtZero:true,max:2}}}}",
|
| 561 |
-
# "caption": "Figure 2: Monthly Revenue Trend - Q4 2025"
|
| 562 |
-
# }
|
| 563 |
-
# ],
|
| 564 |
-
# "page_break": True
|
| 565 |
-
# },
|
| 566 |
-
# {
|
| 567 |
-
# "content": """
|
| 568 |
-
# # Recommendations & 2026 Strategy
|
| 569 |
-
|
| 570 |
-
# ## Key Recommendations
|
| 571 |
-
|
| 572 |
-
# ### Immediate Actions (Q1 2026)
|
| 573 |
-
|
| 574 |
-
# 1. **Expand Mobile Capabilities**
|
| 575 |
-
# - Invest in mobile app feature development
|
| 576 |
-
# - Optimize mobile checkout experience
|
| 577 |
-
# - Target: 60% mobile order share by Q2 2026
|
| 578 |
-
|
| 579 |
-
# 2. **Scale Personalization Engine**
|
| 580 |
-
# - Deploy to all customer touchpoints
|
| 581 |
-
# - Expected: 35% increase in engagement
|
| 582 |
-
|
| 583 |
-
# 3. **Enhance Data Infrastructure**
|
| 584 |
-
# - Implement advanced data governance framework
|
| 585 |
-
# - Prepare for 3x data volume growth
|
| 586 |
-
|
| 587 |
-
# <div class="executive-summary">
|
| 588 |
-
|
| 589 |
-
# **2026 Vision**: Position our analytics capabilities as a competitive differentiator through advanced AI, real-time insights, and predictive decision-making.
|
| 590 |
-
|
| 591 |
-
# </div>
|
| 592 |
-
|
| 593 |
-
# ## Investment Requirements
|
| 594 |
-
|
| 595 |
-
# | Initiative | Investment | Expected ROI | Timeline |
|
| 596 |
-
# |------------|------------|--------------|----------|
|
| 597 |
-
# | Mobile Expansion | $450K | 280% | Q1-Q2 2026 |
|
| 598 |
-
# | AI/ML Platform | $780K | 420% | Q1-Q4 2026 |
|
| 599 |
-
# | Infrastructure | $320K | 195% | Q1-Q3 2026 |
|
| 600 |
-
# | **Total** | **$1.55M** | **310%** | **2026** |
|
| 601 |
-
# """
|
| 602 |
-
# },
|
| 603 |
-
# {
|
| 604 |
-
# "content": f"""
|
| 605 |
-
# # Conclusion
|
| 606 |
-
|
| 607 |
-
# ## Quarter Summary
|
| 608 |
-
|
| 609 |
-
# Q4 2025 represents a milestone quarter for our analytics organization. We exceeded all primary objectives, delivered exceptional data quality, and laid the foundation for continued growth.
|
| 610 |
-
|
| 611 |
-
# ### Key Achievements
|
| 612 |
-
|
| 613 |
-
# - ✓ **23.4% revenue growth** exceeding target by 5%
|
| 614 |
-
# - ✓ **8,547 new customers** acquired, 7% above goal
|
| 615 |
-
# - ✓ **94.2% retention rate**, best in company history
|
| 616 |
-
# - ✓ **98.7% data quality score**, industry-leading
|
| 617 |
-
|
| 618 |
-
# <div class="highlight-box">
|
| 619 |
-
|
| 620 |
-
# **Strategic Position**: Our analytics capabilities represent a significant competitive advantage for 2026 and beyond.
|
| 621 |
-
|
| 622 |
-
# </div>
|
| 623 |
-
|
| 624 |
-
# ---
|
| 625 |
-
|
| 626 |
-
# **Prepared by**: Data Analytics Team
|
| 627 |
-
# **Report Date**: {datetime.now().strftime("%B %d, %Y")}
|
| 628 |
-
# **Classification**: Confidential - Internal Use Only
|
| 629 |
-
# """
|
| 630 |
-
# }
|
| 631 |
-
# ]
|
| 632 |
-
|
| 633 |
-
# # Generate the complete Q4 report
|
| 634 |
-
# generator = Q4ReportGenerator(
|
| 635 |
-
# title="Q4 2025 Analytics Performance Report",
|
| 636 |
-
# subtitle="Fourth Quarter Results & 2026 Strategy",
|
| 637 |
-
# author="Senior Data Analyst",
|
| 638 |
-
# department="Data Analytics Division",
|
| 639 |
-
# confidential=True
|
| 640 |
-
# )
|
| 641 |
-
|
| 642 |
-
# success = generator.generate(
|
| 643 |
-
# sections=q4_sections,
|
| 644 |
-
# filename="Q4_2025_Complete_Report.pdf",
|
| 645 |
-
# output_dir="./output",
|
| 646 |
-
# cover_page=True
|
| 647 |
-
# )
|
| 648 |
-
|
| 649 |
-
# if success:
|
| 650 |
-
# print("\n📋 Report Features:")
|
| 651 |
-
# print(" ✓ Executive cover page")
|
| 652 |
-
# print(" ✓ Performance metrics with tables")
|
| 653 |
-
# print(" ✓ Embedded charts and visualizations")
|
| 654 |
-
# print(" ✓ Customer analytics insights")
|
| 655 |
-
# print(" ✓ Strategic recommendations")
|
| 656 |
-
# print(" ✓ Professional styling and formatting")
|
| 657 |
-
# print("\n💡 Check the 'output' folder for your PDF!")
|
| 658 |
-
# else:
|
| 659 |
-
# print("\n❌ Report generation failed. Check logs for details.")
|
|
|
|
| 275 |
def fetch_image(url: str, output_dir: str) -> Optional[str]:
|
| 276 |
"""
|
| 277 |
Download image from URL to persistent file for PDF rendering.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 278 |
"""
|
| 279 |
if not url.startswith(('http://', 'https://')):
|
| 280 |
if os.path.exists(url):
|
|
|
|
| 292 |
response = requests.get(url, headers=headers, timeout=15)
|
| 293 |
response.raise_for_status()
|
| 294 |
|
|
|
|
| 295 |
content_type = response.headers.get('content-type', '')
|
| 296 |
ext = '.jpg'
|
| 297 |
if 'png' in content_type:
|
|
|
|
| 446 |
|
| 447 |
html_doc.write_pdf(output_path, stylesheets=[css_doc])
|
| 448 |
|
| 449 |
+
logger.info(f"PDF Generated: {output_path}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 450 |
return True
|
| 451 |
|
| 452 |
except Exception as e:
|
| 453 |
+
logger.error(f"Error generating report: {e}", exc_info=True)
|
| 454 |
+
return False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|