PDF_Generation_and_Automation/python-pdf-generator/api_server.py
2025-08-24 12:01:08 +05:30

672 lines
28 KiB
Python

#!/usr/bin/env python3
"""
FastAPI server for Property PDF Generator
Provides REST API endpoints for generating high-quality property brochures
"""
from fastapi import FastAPI, HTTPException, UploadFile, File, Form
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse, JSONResponse
from pydantic import BaseModel, Field
from typing import List, Optional, Dict, Any
import uvicorn
import os
import tempfile
import json
from datetime import datetime
from property_pdf_generator import PropertyPDFGenerator
# Initialize FastAPI app
app = FastAPI(
title="Property PDF Generator API",
description="High-quality property brochure PDF generation service",
version="1.0.0"
)
# Add CORS middleware
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # In production, restrict to specific domains
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Initialize PDF generator
pdf_generator = PropertyPDFGenerator()
# Store generated PDFs temporarily (in production, use proper file storage)
generated_pdfs = {}
# Cleanup old PDFs periodically
import asyncio
import time
async def cleanup_old_pdfs():
"""Clean up old PDF files to prevent disk space issues"""
while True:
try:
current_time = time.time()
expired_files = []
for filename, filepath in generated_pdfs.items():
# Remove files older than 1 hour
if current_time - os.path.getctime(filepath) > 3600:
try:
if os.path.exists(filepath):
os.unlink(filepath)
expired_files.append(filename)
except Exception:
pass
# Remove expired entries from dictionary
for filename in expired_files:
generated_pdfs.pop(filename, None)
# Run cleanup every 30 minutes
await asyncio.sleep(1800)
except Exception:
# Continue cleanup even if there's an error
await asyncio.sleep(1800)
# Start cleanup task
cleanup_task = None
@app.on_event("startup")
async def startup_event():
"""Start cleanup task on startup"""
global cleanup_task
cleanup_task = asyncio.create_task(cleanup_old_pdfs())
@app.on_event("shutdown")
async def shutdown_event():
"""Stop cleanup task and clean up files on shutdown"""
global cleanup_task
if cleanup_task:
cleanup_task.cancel()
# Clean up all stored PDFs
for filepath in generated_pdfs.values():
try:
if os.path.exists(filepath):
os.unlink(filepath)
except Exception:
pass
generated_pdfs.clear()
# Data models
class PropertyData(BaseModel):
"""Enhanced property information model with market data and analytics"""
# Template & Layout
template: str = Field(..., description="Template name to use")
layout: Optional[str] = Field(None, description="Layout configuration for custom templates")
# Basic Property Information
propertyName: str = Field(..., description="Name of the property")
propertyType: str = Field(..., description="Type of property")
location: str = Field(..., description="Property location")
price: str = Field(..., description="Property price")
bedrooms: str = Field(..., description="Number of bedrooms")
bathrooms: str = Field(..., description="Number of bathrooms")
area: str = Field(..., description="Property area in sq ft")
description: Optional[str] = Field(None, description="Property description")
amenities: List[str] = Field(default=[], description="List of amenities")
images: List[str] = Field(default=[], description="Base64 encoded images")
imageNames: List[str] = Field(default=[], description="Room names for each image")
# Market Data & Analytics
marketTrend: Optional[str] = Field(None, description="Market trend (rising/stable/declining)")
roiPotential: Optional[str] = Field(None, description="Expected ROI percentage")
avgPricePerSqft: Optional[str] = Field(None, description="Average market price per sq ft")
marketDemand: Optional[str] = Field(None, description="Market demand level (high/medium/low)")
locationAdvantages: Optional[str] = Field(None, description="Location benefits and advantages")
# Investment Information
investmentType: Optional[str] = Field(None, description="Investment type (buy-to-live/rent/sell)")
rentalYield: Optional[str] = Field(None, description="Expected rental yield percentage")
investmentHighlights: Optional[str] = Field(None, description="Key investment benefits")
# Content Modules
contentModules: List[str] = Field(default=[], description="Selected content modules")
additionalContent: Optional[str] = Field(None, description="Additional custom content")
# Customization Options
headerStyle: Optional[str] = Field("modern", description="Header style (modern/classic/luxury)")
colorScheme: Optional[str] = Field("blue", description="Color scheme (blue/green/purple/gold)")
fontStyle: Optional[str] = Field("sans-serif", description="Font style (sans-serif/serif/modern)")
class TemplateInfo(BaseModel):
"""Template information model"""
name: str
display_name: str
description: str
category: str
preview_color: str
class GeneratePDFRequest(BaseModel):
"""PDF generation request model"""
property_data: PropertyData
template_name: str
class GeneratePDFResponse(BaseModel):
"""PDF generation response model"""
success: bool
message: str
pdf_url: Optional[str] = None
error: Optional[str] = None
# Available templates
AVAILABLE_TEMPLATES = {
# Professional Templates
"professional-1pager": {
"name": "professional-1pager",
"display_name": "Professional 1-Pager",
"description": "Compact single-page brochure with 2x2 image grid",
"category": "Professional",
"preview_color": "#667eea",
"pages": 1,
"image_grid": "2x2"
},
"professional-3pager": {
"name": "professional-3pager",
"display_name": "Professional 3-Pager",
"description": "Comprehensive three-page brochure with detailed analysis",
"category": "Professional",
"preview_color": "#1e3a8a",
"pages": 3,
"image_grid": "4x4"
},
"professional-5pager": {
"name": "professional-5pager",
"display_name": "Professional 5-Pager",
"description": "Premium five-page brochure with comprehensive analysis",
"category": "Professional",
"preview_color": "#059669",
"pages": 5,
"image_grid": "6x6"
},
# Luxury Templates
"luxury-villa": {
"name": "luxury-villa",
"display_name": "Luxury Villa Brochure",
"description": "Exclusive villa template with premium styling",
"category": "Luxury",
"preview_color": "#2c1810",
"pages": 4,
"image_grid": "5x4"
},
"dubai-penthouse": {
"name": "dubai-penthouse",
"display_name": "Dubai Penthouse",
"description": "Dubai-specific luxury penthouse template",
"category": "Luxury",
"preview_color": "#dc2626",
"pages": 6,
"image_grid": "6x5"
},
# Modern Templates
"modern-apartment": {
"name": "modern-apartment",
"display_name": "Modern Apartment",
"description": "Contemporary apartment template with clean lines",
"category": "Modern",
"preview_color": "#7c3aed",
"pages": 3,
"image_grid": "3x5"
},
# Custom Template
"custom": {
"name": "custom",
"display_name": "Custom Template",
"description": "Build your own template with custom layouts",
"category": "Custom",
"preview_color": "#1f2937",
"pages": "flexible",
"image_grid": "configurable"
}
}
@app.get("/")
async def root():
"""Root endpoint"""
return {
"message": "Property PDF Generator API",
"version": "1.0.0",
"status": "running",
"endpoints": {
"templates": "/api/templates",
"preview": "/api/preview",
"generate": "/api/generate-pdf",
"health": "/api/health"
}
}
@app.get("/api/health")
async def health_check():
"""Health check endpoint"""
return {
"status": "healthy",
"timestamp": datetime.now().isoformat(),
"service": "Property PDF Generator"
}
@app.get("/api/templates", response_model=List[TemplateInfo])
async def get_templates():
"""Get all available templates"""
try:
templates = []
for template_id, template_info in AVAILABLE_TEMPLATES.items():
templates.append(TemplateInfo(**template_info))
return templates
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error fetching templates: {str(e)}")
@app.get("/api/templates/{template_name}")
async def get_template(template_name: str):
"""Get specific template information"""
try:
if template_name not in AVAILABLE_TEMPLATES:
raise HTTPException(status_code=404, detail="Template not found")
return AVAILABLE_TEMPLATES[template_name]
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error fetching template: {str(e)}")
@app.post("/api/preview")
async def generate_preview(property_data: PropertyData):
"""Generate a preview of the property brochure"""
try:
# Validate template
if property_data.template not in AVAILABLE_TEMPLATES:
raise HTTPException(status_code=400, detail="Invalid template selected")
# Generate preview content
preview_content = {
"template": property_data.template,
"template_info": AVAILABLE_TEMPLATES[property_data.template],
"property_data": property_data.dict(),
"preview_html": generate_preview_html(property_data),
"generated_at": datetime.now().isoformat()
}
return {
"success": True,
"preview": preview_content,
"message": "Preview generated successfully"
}
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error generating preview: {str(e)}")
# Add a new endpoint for Salesforce compatibility
@app.post("/api/preview-simple")
async def generate_preview_simple(request_data: dict):
"""Generate a preview using the simpler format sent from Salesforce"""
try:
# Extract template and property data from the request
template = request_data.get('template')
if not template or template not in AVAILABLE_TEMPLATES:
raise HTTPException(status_code=400, detail="Invalid template selected")
# Create a PropertyData object from the simple request
property_data = PropertyData(
template=template,
propertyName=request_data.get('propertyName', 'Unknown Property'),
propertyType=request_data.get('propertyType', 'Unknown'),
location=request_data.get('location', 'Unknown Location'),
price=request_data.get('price', 'Price on Request'),
bedrooms=request_data.get('bedrooms', 'N/A'),
bathrooms=request_data.get('bathrooms', 'N/A'),
area=request_data.get('area', 'N/A'),
description=request_data.get('description', 'No description available')
)
# Generate preview content
preview_content = {
"template": property_data.template,
"template_info": AVAILABLE_TEMPLATES[property_data.template],
"property_data": property_data.dict(),
"preview_html": generate_preview_html(property_data),
"generated_at": datetime.now().isoformat()
}
return {
"success": True,
"preview": preview_content,
"message": "Preview generated successfully"
}
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error generating preview: {str(e)}")
@app.post("/api/generate-pdf", response_model=GeneratePDFResponse)
async def generate_pdf(request: GeneratePDFRequest):
"""Generate a PDF property brochure"""
try:
# Validate template
if request.template_name not in AVAILABLE_TEMPLATES:
return GeneratePDFResponse(
success=False,
message="Invalid template selected",
error="Template not found"
)
# Validate property data
if not request.property_data.propertyName or not request.property_data.propertyType:
return GeneratePDFResponse(
success=False,
message="Missing required property information",
error="Property name and type are required"
)
# Create temporary file for PDF
with tempfile.NamedTemporaryFile(delete=False, suffix='.pdf') as tmp_file:
pdf_path = tmp_file.name
try:
# Generate PDF
result_path = pdf_generator.generate_property_pdf(
request.property_data.dict(),
request.template_name,
pdf_path
)
# Extract filename from the result path
filename = os.path.basename(result_path)
# Store the generated PDF path
generated_pdfs[filename] = pdf_path
# Return the download URL (fix: add /api/ prefix to match the endpoint)
pdf_url = f"/api/download-pdf/{filename}"
return GeneratePDFResponse(
success=True,
message="PDF generated successfully",
pdf_url=pdf_url,
error=None
)
except Exception as e:
# Clean up temporary file on error
if os.path.exists(pdf_path):
os.unlink(pdf_path)
raise e
except Exception as e:
return GeneratePDFResponse(
success=False,
message="Error generating PDF",
error=str(e)
)
@app.get("/api/download-pdf/{filename}")
async def download_pdf(filename: str):
"""Download generated PDF file"""
try:
# Check if the PDF exists in our storage
if filename in generated_pdfs and os.path.exists(generated_pdfs[filename]):
pdf_path = generated_pdfs[filename]
# Return the actual generated PDF
return FileResponse(
pdf_path,
media_type='application/pdf',
filename=f"property_brochure_{datetime.now().strftime('%Y%m%d_%H%M%S')}.pdf"
)
else:
# Fallback: generate a demo PDF
with tempfile.NamedTemporaryFile(delete=False, suffix='.pdf') as tmp_file:
demo_pdf_path = tmp_file.name
# Generate a demo PDF
demo_data = {
"propertyName": "Demo Property",
"propertyType": "Apartment",
"location": "Dubai",
"price": "1,500,000",
"bedrooms": "2",
"bathrooms": "2",
"area": "1,200",
"description": "This is a demo property brochure generated by the Property PDF Generator API.",
"amenities": ['Swimming Pool', 'Gym', 'Parking'],
"images": []
}
pdf_generator.generate_property_pdf(demo_data, "modern", demo_pdf_path)
# Return the demo file
return FileResponse(
demo_pdf_path,
media_type='application/pdf',
filename=f"demo_property_brochure.pdf"
)
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error downloading PDF: {str(e)}")
def generate_preview_html(property_data: PropertyData) -> str:
"""Generate comprehensive HTML preview of the property brochure with all new data"""
template_info = AVAILABLE_TEMPLATES.get(property_data.template, {})
color = template_info.get('preview_color', '#333')
# Market trend styling
trend_color = {
'rising': '#28a745',
'stable': '#ffc107',
'declining': '#dc3545'
}.get(property_data.marketTrend, '#6c757d')
# Investment type styling
investment_color = {
'buy-to-live': '#007bff',
'buy-to-rent': '#28a745',
'buy-to-sell': '#ffc107'
}.get(property_data.investmentType, '#6c757d')
preview_html = f"""
<div style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; max-width: 900px; margin: 0 auto; padding: 20px; background: #f8f9fa; border-radius: 15px;">
<!-- Header Section -->
<div style="text-align: center; margin-bottom: 40px; background: white; padding: 30px; border-radius: 15px; box-shadow: 0 4px 20px rgba(0,0,0,0.1);">
<div style="background: {color}; color: white; padding: 15px; border-radius: 10px; margin-bottom: 20px;">
<h1 style="margin: 0; font-size: 2.5rem; font-weight: 700;">
{property_data.propertyName}
</h1>
<p style="margin: 10px 0 0; font-size: 1.2rem; opacity: 0.9;">
{property_data.propertyType} in {property_data.location}
</p>
</div>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px;">
<div style="background: #e3f2fd; padding: 15px; border-radius: 10px;">
<h3 style="margin: 0; color: #1976d2;">Template</h3>
<p style="margin: 5px 0 0; font-weight: 600;">{template_info.get('display_name', 'Unknown')}</p>
</div>
<div style="background: #e8f5e8; padding: 15px; border-radius: 10px;">
<h3 style="margin: 0; color: #388e3c;">Pages</h3>
<p style="margin: 5px 0 0; font-weight: 600;">{template_info.get('pages', 'N/A')}</p>
</div>
<div style="background: #fce4ec; padding: 15px; border-radius: 10px;">
<h3 style="margin: 0; color: #c2185b;">Layout</h3>
<p style="margin: 5px 0 0; font-weight: 600;">{property_data.layout or template_info.get('image_grid', 'Default')}</p>
</div>
</div>
</div>
<!-- Property Details Grid -->
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 20px; margin-bottom: 30px;">
<div style="background: white; padding: 25px; border-radius: 15px; box-shadow: 0 4px 20px rgba(0,0,0,0.1);">
<h2 style="color: {color}; margin-bottom: 20px; font-size: 1.5rem;">Property Details</h2>
<div style="display: grid; gap: 12px;">
<div style="display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid #eee;">
<strong>Type:</strong> <span>{property_data.propertyType}</span>
</div>
<div style="display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid #eee;">
<strong>Location:</strong> <span>{property_data.location}</span>
</div>
<div style="display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid #eee;">
<strong>Price:</strong> <span style="color: {color}; font-weight: 700;">AED {property_data.price}</span>
</div>
<div style="display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid #eee;">
<strong>Bedrooms:</strong> <span>{property_data.bedrooms}</span>
</div>
<div style="display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid #eee;">
<strong>Bathrooms:</strong> <span>{property_data.bathrooms}</span>
</div>
<div style="display: flex; justify-content: space-between; padding: 8px 0;">
<strong>Area:</strong> <span>{property_data.area} sq ft</span>
</div>
</div>
</div>
{f'''
<div style="background: white; padding: 25px; border-radius: 15px; box-shadow: 0 4px 20px rgba(0,0,0,0.1);">
<h2 style="color: {color}; margin-bottom: 20px; font-size: 1.5rem;">Market Analytics</h2>
<div style="display: grid; gap: 12px;">
<div style="display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid #eee;">
<strong>Market Trend:</strong>
<span style="color: {trend_color}; font-weight: 600;">
{property_data.marketTrend.title() if property_data.marketTrend else 'N/A'}
</span>
</div>
<div style="display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid #eee;">
<strong>ROI Potential:</strong> <span>{property_data.roiPotential}%</span>
</div>
<div style="display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid #eee;">
<strong>Avg Price/sq ft:</strong> <span>AED {property_data.avgPricePerSqft}</span>
</div>
<div style="display: flex; justify-content: space-between; padding: 8px 0;">
<strong>Market Demand:</strong>
<span style="color: {trend_color}; font-weight: 600;">
{property_data.marketDemand.title() if property_data.marketDemand else 'N/A'}
</span>
</div>
</div>
</div>
''' if any([property_data.marketTrend, property_data.roiPotential, property_data.avgPricePerSqft, property_data.marketDemand]) else ''}
{f'''
<div style="background: white; padding: 25px; border-radius: 15px; box-shadow: 0 4px 20px rgba(0,0,0,0.1);">
<h2 style="color: {color}; margin-bottom: 20px; font-size: 1.5rem;">Investment Details</h2>
<div style="display: grid; gap: 12px;">
<div style="display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid #eee;">
<strong>Investment Type:</strong>
<span style="color: {investment_color}; font-weight: 600;">
{property_data.investmentType.replace('-', ' ').title() if property_data.investmentType else 'N/A'}
</span>
</div>
<div style="display: flex; justify-content: space-between; padding: 8px 0;">
<strong>Rental Yield:</strong> <span>{property_data.rentalYield}%</span>
</div>
</div>
</div>
''' if any([property_data.investmentType, property_data.rentalYield]) else ''}
</div>
{f'''
<div style="background: white; padding: 25px; border-radius: 15px; box-shadow: 0 4px 20px rgba(0,0,0,0.1); margin-bottom: 20px;">
<h2 style="color: {color}; margin-bottom: 20px; font-size: 1.5rem;">Property Description</h2>
<p style="line-height: 1.6; color: #555;">{property_data.description}</p>
</div>
''' if property_data.description else ''}
{f'''
<div style="background: white; padding: 25px; border-radius: 15px; box-shadow: 0 4px 20px rgba(0,0,0,0.1); margin-bottom: 20px;">
<h2 style="color: {color}; margin-bottom: 20px; font-size: 1.5rem;">Location Advantages</h2>
<p style="line-height: 1.6; color: #555;">{property_data.locationAdvantages}</p>
</div>
''' if property_data.locationAdvantages else ''}
{f'''
<div style="background: white; padding: 25px; border-radius: 15px; box-shadow: 0 4px 20px rgba(0,0,0,0.1); margin-bottom: 20px;">
<h2 style="color: {color}; margin-bottom: 20px; font-size: 1.5rem;">Investment Highlights</h2>
<p style="line-height: 1.6; color: #555;">{property_data.investmentHighlights}</p>
</div>
''' if property_data.investmentHighlights else ''}
{f'''
<div style="background: white; padding: 25px; border-radius: 15px; box-shadow: 0 4px 20px rgba(0,0,0,0.1); margin-bottom: 20px;">
<h2 style="color: {color}; margin-bottom: 20px; font-size: 1.5rem;">Amenities</h2>
<div style="display: flex; flex-wrap: wrap; gap: 10px;">
{''.join([f'<span style="background: {color}; color: white; padding: 8px 16px; border-radius: 20px; font-size: 14px; font-weight: 500;">{amenity}</span>' for amenity in property_data.amenities])}
</div>
</div>
''' if property_data.amenities else ''}
{f'''
<div style="background: white; padding: 25px; border-radius: 15px; box-shadow: 0 4px 20px rgba(0,0,0,0.1); margin-bottom: 20px;">
<h2 style="color: {color}; margin-bottom: 20px; font-size: 1.5rem;">Content Modules</h2>
<div style="display: flex; flex-wrap: wrap; gap: 10px;">
{''.join([f'<span style="background: #e3f2fd; color: #1976d2; padding: 8px 16px; border-radius: 20px; font-size: 14px; font-weight: 500;">{module.replace("-", " ").title()}</span>' for module in property_data.contentModules])}
</div>
</div>
''' if property_data.contentModules else ''}
{f'''
<div style="background: white; padding: 25px; border-radius: 15px; box-shadow: 0 4px 20px rgba(0,0,0,0.1); margin-bottom: 20px;">
<h2 style="color: {color}; margin-bottom: 20px; font-size: 1.5rem;">Additional Content</h2>
<p style="line-height: 1.6; color: #555;">{property_data.additionalContent}</p>
</div>
''' if property_data.additionalContent else ''}
<!-- Footer -->
<div style="text-align: center; color: #999; font-style: italic; margin-top: 40px; padding: 20px; background: white; border-radius: 15px; box-shadow: 0 4px 20px rgba(0,0,0,0.1);">
<h3 style="color: {color}; margin-bottom: 10px;">Preview Information</h3>
<p style="margin: 5px 0;">Template: {template_info.get('display_name', 'Unknown')}</p>
<p style="margin: 5px 0;">Pages: {template_info.get('pages', 'N/A')} | Layout: {property_data.layout or template_info.get('image_grid', 'Default')}</p>
<p style="margin: 15px 0 5px; font-weight: 600; color: #555;">This is a preview. The final PDF will be generated with professional styling and layouts.</p>
</div>
</div>
"""
return preview_html
@app.post("/api/upload-images")
async def upload_images(files: List[UploadFile] = File(...)):
"""Upload property images"""
try:
uploaded_images = []
for file in files:
if file.content_type.startswith('image/'):
# Read image content
content = await file.read()
# Convert to base64 for storage
import base64
base64_content = base64.b64encode(content).decode('utf-8')
uploaded_images.append({
"filename": file.filename,
"content_type": file.content_type,
"size": len(content),
"base64_data": f"data:{file.content_type};base64,{base64_content}"
})
return {
"success": True,
"message": f"Successfully uploaded {len(uploaded_images)} images",
"images": uploaded_images
}
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error uploading images: {str(e)}")
if __name__ == "__main__":
# Run the server
uvicorn.run(
"api_server:app",
host="0.0.0.0",
port=8000,
reload=True,
log_level="info"
)