#!/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"""
{property_data.propertyType} in {property_data.location}
{template_info.get('display_name', 'Unknown')}
{template_info.get('pages', 'N/A')}
{property_data.layout or template_info.get('image_grid', 'Default')}
{property_data.description}
{property_data.locationAdvantages}
{property_data.investmentHighlights}
{property_data.additionalContent}
Template: {template_info.get('display_name', 'Unknown')}
Pages: {template_info.get('pages', 'N/A')} | Layout: {property_data.layout or template_info.get('image_grid', 'Default')}
This is a preview. The final PDF will be generated with professional styling and layouts.