"""Operation management - formerly Session (renamed for new architecture). An Operation represents a single feed generation and/or delivery task. This is the renamed version of the original Session concept, preserving all existing functionality while fitting into the new Session → Operation hierarchy. """ from datetime import datetime from pathlib import Path from typing import Dict, List, Optional, Any def create_operation_id() -> str: """Generate unique operation ID based on timestamp.""" return datetime.now().strftime("%Y%m%d_%H%M%S_%f")[:19] # Microsecond precision def create_operation( run_config: Dict[str, Any], output_directory: str, files: List[str], operation_type: str = "generation+delivery", zip_file_path: Optional[str] = None ) -> Dict[str, Any]: """Create a new operation record. Args: run_config: Configuration used for feed generation output_directory: Directory where files were generated files: List of generated file paths operation_type: Type of operation ('generation', 'delivery', 'generation+delivery') zip_file_path: Optional path to zip archive file (if generated) Returns: Operation dictionary """ operation_id = create_operation_id() return { "id": operation_id, "created_at": datetime.now().isoformat(), "operation_type": operation_type, "run_config": run_config, "output_directory": output_directory, "files": files, "zip_file_path": zip_file_path, "status": "queued", # queued, running, completed, failed "delivery_info": None, "execution_start": None, "execution_end": None, "error": None, } def get_operation_status_emoji(status: str) -> str: """Get emoji for operation status. Args: status: Operation status Returns: Emoji string """ status_emoji = { 'queued': '⏳', 'running': '🔄', 'completed': '✅', 'failed': '❌', 'generated': '📝', 'delivered': '✅', } return status_emoji.get(status, '❓') def format_operation_display(operation: Dict[str, Any]) -> str: """Format operation for display in UI. Args: operation: Operation dictionary Returns: Formatted string for display """ created = operation.get('created_at', 'Unknown') status = operation.get('status', 'unknown') file_count = len(operation.get('files', [])) op_type = operation.get('operation_type', 'unknown') # Parse datetime for better display try: dt = datetime.fromisoformat(created) created_str = dt.strftime("%Y-%m-%d %H:%M") except (ValueError, TypeError): created_str = created status_emoji = get_operation_status_emoji(status) # Abbreviate operation type type_abbrev = { 'generation': 'Gen', 'delivery': 'Del', 'generation+delivery': 'Gen+Del', }.get(op_type, op_type) return f"{status_emoji} {created_str} [{type_abbrev}] ({file_count} files)" def get_operation_files_absolute(operation: Dict[str, Any]) -> List[Path]: """Get absolute paths to operation files. Args: operation: Operation dictionary Returns: List of absolute file paths that exist """ files = operation.get('files', []) # Convert to absolute paths and verify existence abs_files = [] for file in files: file_path = Path(file) # If path is already absolute, use it directly if file_path.is_absolute(): if file_path.exists(): abs_files.append(file_path) else: # Try relative to current directory first if file_path.exists(): abs_files.append(file_path.absolute()) else: # Try relative to output directory output_dir = Path(operation.get('output_directory', '')) combined = output_dir / file_path if combined.exists(): abs_files.append(combined.absolute()) return abs_files def update_operation_status( operation: Dict[str, Any], status: str, delivery_info: Optional[Dict[str, Any]] = None, error: Optional[str] = None ) -> Dict[str, Any]: """Update operation status and optionally add delivery info or error. Args: operation: Operation to update status: New status ('queued', 'running', 'completed', 'failed') delivery_info: Optional delivery information to attach error: Optional error message if failed Returns: Updated operation dictionary """ operation['status'] = status operation['updated_at'] = datetime.now().isoformat() if status == 'running' and not operation.get('execution_start'): operation['execution_start'] = datetime.now().isoformat() if status in ['completed', 'failed']: operation['execution_end'] = datetime.now().isoformat() if delivery_info: operation['delivery_info'] = delivery_info if error: operation['error'] = error return operation