"""Session management for orchestrating multiple Operations. A Session is a container that manages multiple Operations (feed generation/delivery tasks). This allows users to queue and batch execute multiple operations within a single session. Architecture: - Session: Container for multiple Operations - Operation: Single generation/delivery task (formerly called Session) """ import json import uuid from datetime import datetime from pathlib import Path from typing import Dict, List, Optional, Any import operation_manager # Session storage location SESSIONS_FILE = Path("tmp/streamlit_sessions_v2.json") def create_session_id() -> str: """Generate unique session ID using UUID. Returns: 8-character unique identifier """ return str(uuid.uuid4())[:8] def create_session( name: Optional[str] = None, description: Optional[str] = None ) -> Dict[str, Any]: """Create a new session to hold multiple operations. Args: name: Optional name for the session description: Optional description Returns: Session dictionary """ session_id = create_session_id() return { "id": session_id, "created_at": datetime.now().isoformat(), "name": name or f"Session {session_id}", "description": description, "operations": [], # List of operation IDs or full operation dicts "status": "active", # active, completed, archived "updated_at": datetime.now().isoformat(), } def add_operation_to_session( session: Dict[str, Any], operation: Dict[str, Any] ) -> Dict[str, Any]: """Add an operation to a session's queue. Args: session: Session to add to operation: Operation to add Returns: Updated session """ if 'operations' not in session: session['operations'] = [] session['operations'].append(operation) session['updated_at'] = datetime.now().isoformat() return session def execute_operation( operation: Dict[str, Any], execute_callback: callable ) -> Dict[str, Any]: """Execute a single operation. Args: operation: Operation to execute execute_callback: Callback function to perform actual work Returns: Updated operation with execution results """ operation = operation_manager.update_operation_status(operation, 'running') try: # Execute the operation via callback result = execute_callback(operation) if result.get('success'): operation = operation_manager.update_operation_status( operation, 'completed', delivery_info=result.get('delivery_info') ) else: operation = operation_manager.update_operation_status( operation, 'failed', error=result.get('error', 'Unknown error') ) except Exception as e: operation = operation_manager.update_operation_status( operation, 'failed', error=str(e) ) return operation def execute_session_queue( session: Dict[str, Any], execute_callback: callable, operation_filter: Optional[callable] = None ) -> Dict[str, Any]: """Execute all queued operations in a session. Args: session: Session containing operations execute_callback: Callback to execute each operation operation_filter: Optional filter to select operations to run Returns: Updated session with execution results """ operations = session.get('operations', []) for i, operation in enumerate(operations): # Apply filter if provided if operation_filter and not operation_filter(operation): continue # Execute operation operations[i] = execute_operation(operation, execute_callback) session['operations'] = operations session['updated_at'] = datetime.now().isoformat() # Update session status based on operations all_completed = all( op.get('status') in ['completed', 'failed'] for op in operations ) if all_completed: session['status'] = 'completed' return session def get_session_summary(session: Dict[str, Any]) -> Dict[str, Any]: """Get summary statistics for a session. Args: session: Session to summarize Returns: Summary dictionary """ operations = session.get('operations', []) summary = { 'total_operations': len(operations), 'queued': 0, 'running': 0, 'completed': 0, 'failed': 0, 'total_files': 0, } for op in operations: status = op.get('status', 'queued') summary[status] = summary.get(status, 0) + 1 summary['total_files'] += len(op.get('files', [])) return summary def load_sessions() -> List[Dict[str, Any]]: """Load all sessions from disk, removing duplicates. Returns: List of session dictionaries, newest first, deduplicated by ID """ if not SESSIONS_FILE.exists(): return [] try: with open(SESSIONS_FILE, 'r') as f: sessions = json.load(f) # Deduplicate by ID, keeping the most recently updated seen_ids: Dict[str, Dict[str, Any]] = {} for s in sessions: sid = s.get('id') if sid: existing = seen_ids.get(sid) if existing: # Keep the one with more recent updated_at existing_time = existing.get('updated_at', '') new_time = s.get('updated_at', '') if new_time > existing_time: seen_ids[sid] = s else: seen_ids[sid] = s unique_sessions = list(seen_ids.values()) unique_sessions.sort( key=lambda x: x.get('created_at', ''), reverse=True ) return unique_sessions except (json.JSONDecodeError, IOError) as e: print(f"Error loading sessions: {e}") return [] def save_sessions(sessions: List[Dict[str, Any]]) -> None: """Save sessions to disk. Args: sessions: List of session dictionaries to save """ SESSIONS_FILE.parent.mkdir(parents=True, exist_ok=True) try: with open(SESSIONS_FILE, 'w') as f: json.dump(sessions, f, indent=2) except IOError as e: print(f"Error saving sessions: {e}") def add_session( session: Dict[str, Any], sessions: List[Dict[str, Any]] ) -> List[Dict[str, Any]]: """Add or update a session in the list and save. If a session with the same ID exists, it will be updated. Otherwise, the session is added to the front of the list. Args: session: Session to add or update sessions: Existing sessions list Returns: Updated sessions list """ session_id = session.get('id') session['updated_at'] = datetime.now().isoformat() # Check if session already exists existing_idx = None for i, s in enumerate(sessions): if s.get('id') == session_id: existing_idx = i break if existing_idx is not None: # Update existing session sessions[existing_idx] = session else: # Add new session to front sessions.insert(0, session) save_sessions(sessions) return sessions def get_session( session_id: str, sessions: List[Dict[str, Any]] ) -> Optional[Dict[str, Any]]: """Get a specific session by ID. Args: session_id: Session ID to find sessions: List of sessions to search Returns: Session dict if found, None otherwise """ for session in sessions: if session.get('id') == session_id: return session return None def update_session( session_id: str, sessions: List[Dict[str, Any]], updates: Dict[str, Any] ) -> List[Dict[str, Any]]: """Update a session with new data. Args: session_id: Session to update sessions: Existing sessions list updates: Dictionary of fields to update Returns: Updated sessions list """ for session in sessions: if session.get('id') == session_id: session.update(updates) session['updated_at'] = datetime.now().isoformat() break save_sessions(sessions) return sessions def delete_session( session_id: str, sessions: List[Dict[str, Any]] ) -> List[Dict[str, Any]]: """Delete a session. Args: session_id: Session to delete sessions: Existing sessions list Returns: Updated sessions list """ sessions = [s for s in sessions if s.get('id') != session_id] save_sessions(sessions) return sessions def format_session_display(session: Dict[str, Any]) -> str: """Format session for display in UI. Args: session: Session dictionary Returns: Formatted string for display """ name = session.get('name', session.get('id', 'Unknown')) summary = get_session_summary(session) return f"📦 {name} ({summary['total_operations']} ops)"