""" Export server for processing Google Sheets export jobs. This module handles the actual processing of export jobs queued through the FastStream message broker, including data retrieval, formatting, and Google Sheets API integration. """ import logging from typing import Dict, Any, List, Optional from datetime import datetime from adapters.sqlite.database import get_db logger = logging.getLogger(__name__) class ExportServer: """Server for processing Google Sheets export operations.""" def __init__(self): """Initialize ExportServer with database and Google Sheets client.""" self.db = get_db # TODO: Initialize Google Sheets client when implemented # self.sheets_client = GoogleSheetsClient() async def process_export_job(self, job_data: Dict[str, Any]) -> None: """ Process a Google Sheets export job. Args: job_data: Dictionary containing job_id, configuration_id, and metadata """ job_id = job_data["job_id"] configuration_id = job_data["configuration_id"] try: logger.info(f"Starting export job {job_id} for configuration {configuration_id}") # Update job status to running await self._update_job_status(job_id, "running", started_at=datetime.utcnow()) # Get export configuration config = await self._get_export_configuration(configuration_id) if not config: raise ValueError(f"Export configuration {configuration_id} not found") # Process each enabled entity total_records = 0 processed_entities = {} for entity_type, mapping in config["entity_mappings"].items(): if mapping.get("is_enabled", False): logger.info(f"Exporting {entity_type} data") records_count = await self._export_entity( entity_type, config["sheet_id"], mapping, config.get("date_range_start"), config.get("date_range_end") ) total_records += records_count processed_entities[entity_type] = { "processed": records_count, "total": records_count, "status": "completed" } logger.info(f"Exported {records_count} {entity_type} records") # Update job as completed await self._update_job_status( job_id, "completed", completed_at=datetime.utcnow(), records_processed=total_records, total_records=total_records, entities_processed=processed_entities ) logger.info(f"Export job {job_id} completed successfully with {total_records} records") except Exception as e: logger.error(f"Export job {job_id} failed: {str(e)}") await self._update_job_status( job_id, "failed", completed_at=datetime.utcnow(), error_message=str(e) ) raise async def _export_entity( self, entity_type: str, sheet_id: str, mapping: Dict[str, Any], date_range_start: Optional[str] = None, date_range_end: Optional[str] = None ) -> int: """ Export specific entity type to Google Sheets. Args: entity_type: Type of entity to export (deals, contacts, companies, etc.) sheet_id: Google Sheets document ID mapping: Field mapping configuration for this entity date_range_start: Optional start date filter date_range_end: Optional end date filter Returns: Number of records exported """ try: # Get entity data from database data = await self._get_entity_data(entity_type, date_range_start, date_range_end) if not data: logger.warning(f"No data found for {entity_type}") return 0 # Format data according to mapping formatted_data = await self._format_data_for_export(data, mapping) # Export to Google Sheets await self._write_to_google_sheets(sheet_id, mapping["sheet_name"], formatted_data) return len(data) except Exception as e: logger.error(f"Failed to export {entity_type}: {str(e)}") raise async def _get_entity_data( self, entity_type: str, date_range_start: Optional[str] = None, date_range_end: Optional[str] = None ) -> List[Dict[str, Any]]: """ Retrieve entity data from the database. Args: entity_type: Type of entity to retrieve date_range_start: Optional start date filter date_range_end: Optional end date filter Returns: List of entity records """ # TODO: Implement database query based on entity type and date range # This would query the appropriate table (amo_deals, amo_contacts, etc.) # and join with custom fields if needed logger.info(f"Retrieving {entity_type} data (date range: {date_range_start} to {date_range_end})") # Placeholder implementation return [] async def _format_data_for_export( self, data: List[Dict[str, Any]], mapping: Dict[str, Any] ) -> List[List[Any]]: """ Format data according to the export mapping configuration. Args: data: Raw entity data from database mapping: Field mapping configuration Returns: Formatted data ready for Google Sheets export """ field_mapping = mapping.get("field_mapping", []) # Sort field mapping by order sorted_fields = sorted(field_mapping, key=lambda x: x.get("order", 0)) # Create header row headers = [field["field_name"] for field in sorted_fields] formatted_data = [headers] # Format data rows for record in data: row = [] for field in sorted_fields: field_name = field["field_name"] value = record.get(field_name, "") # Handle different data types and formatting if isinstance(value, datetime): value = value.isoformat() elif value is None: value = "" row.append(value) formatted_data.append(row) logger.info(f"Formatted {len(data)} records with {len(headers)} columns") return formatted_data async def _write_to_google_sheets( self, sheet_id: str, sheet_name: str, data: List[List[Any]] ) -> None: """ Write formatted data to Google Sheets. Args: sheet_id: Google Sheets document ID sheet_name: Name of the sheet tab data: Formatted data to write """ # TODO: Implement Google Sheets API integration # This would use the Google Sheets client to write data logger.info(f"Writing {len(data)} rows to sheet '{sheet_name}' in document {sheet_id}") # Placeholder - would actually write to Google Sheets logger.info(f"Successfully wrote data to Google Sheets (placeholder)") async def _get_export_configuration(self, configuration_id: int) -> Optional[Dict[str, Any]]: """ Get export configuration from database. Args: configuration_id: ID of the export configuration Returns: Export configuration data or None if not found """ # TODO: Implement database query to get export configuration # This would query the export_configuration and export_entity_mappings tables logger.info(f"Retrieving export configuration {configuration_id}") # Placeholder implementation return { "id": configuration_id, "sheet_id": "placeholder_sheet_id", "entity_mappings": { "deals": { "sheet_name": "Deals", "is_enabled": True, "field_mapping": [ {"field_name": "name", "column": "A", "order": 1}, {"field_name": "price", "column": "B", "order": 2}, {"field_name": "created_at", "column": "C", "order": 3} ] } } } async def _update_job_status( self, job_id: str, status: str, **kwargs ) -> None: """ Update job status in the database. Args: job_id: Unique identifier of the job status: New status (pending, running, completed, failed) **kwargs: Additional fields to update """ # TODO: Implement database update # This would update the export_jobs table logger.info(f"Updating job {job_id} status to {status}") update_data = {"status": status, **kwargs} logger.debug(f"Job update data: {update_data}") # Placeholder - would actually update database pass