Executor / pdf_report_generation_helper.py
Soumik Bose
go
d54e4f5
Raw
History Blame Contribute Delete
5.96 kB
"""
PDF Report Generation Helper - Production Ready
Handles PDF generation, upload to Supabase, and cleanup
"""
import os
import uuid
import logging
from typing import List, Dict, Any
from starlette.concurrency import run_in_threadpool
# The Service file defines the Generator class
from pdf_report_generation_service import Q4ReportGenerator
logger = logging.getLogger(__name__)
def _upload_to_supabase_sync(file_path: str, file_name: str, chat_id: str) -> str:
"""
Synchronous wrapper for Supabase upload.
Import inside function to avoid potential circular dependencies if supabase_service changes.
"""
from supabase_service import upload_file_to_supabase
return upload_file_to_supabase(
file_path=file_path,
file_name=file_name,
chat_id=chat_id
)
def _generate_pdf_sync(
generator: Q4ReportGenerator,
config_content: List[Dict[str, Any]],
local_file_path: str,
output_dir: str
) -> bool:
"""Synchronous PDF generation wrapper"""
return generator.generate(
sections=config_content,
filename=os.path.basename(local_file_path),
output_dir=output_dir,
cover_page=True
)
def _cleanup_files(file_paths: List[str]):
"""Clean up temporary files"""
for file_path in file_paths:
if file_path and os.path.exists(file_path):
try:
os.remove(file_path)
logger.info(f"Cleaned up: {file_path}")
except Exception as e:
logger.warning(f"Failed to delete {file_path}: {e}")
def _cleanup_downloaded_images(generator: Q4ReportGenerator, output_dir: str):
"""Clean up images downloaded during PDF generation"""
if not hasattr(generator, 'downloaded_images'):
return
for img_uri in generator.downloaded_images:
try:
# Convert file:// URI to path
if img_uri.startswith('file://'):
# Handle both Unix and Windows paths
if os.name == 'nt' and img_uri.startswith('file:///'):
img_path = img_uri[8:] # Windows: file:///C:/...
else:
img_path = img_uri[7:] # Unix: file:///path/...
if os.path.exists(img_path):
os.remove(img_path)
logger.info(f"Deleted temp image: {img_path}")
except Exception as e:
logger.warning(f"Failed to delete image {img_uri}: {e}")
# Try to remove the images directory if empty
images_dir = os.path.join(output_dir, 'images')
if os.path.exists(images_dir):
try:
if not os.listdir(images_dir): # Check if empty
os.rmdir(images_dir)
logger.info(f"Removed empty images directory: {images_dir}")
except Exception as e:
logger.debug(f"Could not remove images directory: {e}")
async def generate_and_upload_report(
config_content: List[Dict[str, Any]],
file_name: str,
chat_id: str,
title: str = "Analytics Report",
subtitle: str = "Generated Report",
author: str = "AI Assistant",
department: str = "Data Analytics",
output_dir: str = "temp_reports",
confidential: bool = True
) -> str:
"""
Generates a PDF report, uploads it to Supabase, and returns the public URL.
"""
local_file_path = None
try:
# 1. Setup paths
os.makedirs(output_dir, exist_ok=True)
if not file_name.endswith('.pdf'):
file_name += '.pdf'
unique_id = str(uuid.uuid4())[:8]
local_filename = f"{os.path.splitext(file_name)[0]}_{unique_id}.pdf"
local_file_path = os.path.join(output_dir, local_filename)
logger.info(f"Starting PDF generation: {local_filename}")
# 2. Initialize Generator
generator = Q4ReportGenerator(
title=title,
subtitle=subtitle,
author=author,
department=department,
confidential=confidential
)
# 3. Generate PDF (Run in thread pool to avoid blocking)
success = await run_in_threadpool(
_generate_pdf_sync,
generator=generator,
config_content=config_content,
local_file_path=local_file_path,
output_dir=output_dir
)
if not success:
raise Exception("PDF generation failed - generator returned False")
if not os.path.exists(local_file_path):
raise Exception(f"PDF file not found after generation: {local_file_path}")
# 4. Upload to Supabase (Run in thread pool)
cloud_filename = f"{os.path.splitext(file_name)[0]}_{unique_id}.pdf"
logger.info(f"Uploading {local_filename} to Supabase as {cloud_filename}")
public_url = await run_in_threadpool(
_upload_to_supabase_sync,
file_path=local_file_path,
file_name=cloud_filename,
chat_id=chat_id
)
if not public_url:
raise Exception("Upload failed - no URL returned")
logger.info(f"Upload successful: {public_url}")
return public_url
except Exception as e:
logger.error(f"Report generation/upload failed: {e}", exc_info=True)
raise Exception(f"Failed to generate and upload report: {str(e)}")
finally:
# 5. Cleanup
try:
if local_file_path:
_cleanup_files([local_file_path])
if 'generator' in locals():
await run_in_threadpool(
_cleanup_downloaded_images,
generator=generator,
output_dir=output_dir
)
except Exception as cleanup_error:
logger.warning(f"Cleanup error (non-fatal): {cleanup_error}")