"""Session management for Streamlit feed generation and delivery workflow. This module provides persistent session storage to connect Run and Deliver operations. Sessions are stored as JSON on disk and loaded on app startup. """ import json from datetime import datetime from pathlib import Path from typing import Dict, List, Optional, Any # Session storage location SESSIONS_FILE = Path("tmp/streamlit_sessions.json") def create_session_id() -> str: """Generate unique session ID based on timestamp.""" return datetime.now().strftime("%Y%m%d_%H%M%S") def create_session( run_config: Dict[str, Any], output_directory: str, files: List[str] ) -> Dict[str, Any]: """Create a new session record. Args: run_config: Configuration used for feed generation output_directory: Directory where files were generated files: List of generated file paths Returns: Session dictionary """ session_id = create_session_id() return { "id": session_id, "created_at": datetime.now().isoformat(), "run_config": run_config, "output_directory": output_directory, "files": files, "status": "generated", "delivery_info": None, } def load_sessions() -> List[Dict[str, Any]]: """Load all sessions from disk. Returns: List of session dictionaries, newest first """ if not SESSIONS_FILE.exists(): return [] try: with open(SESSIONS_FILE, 'r') as f: sessions = json.load(f) # Sort by created_at, newest first sessions.sort(key=lambda s: s.get('created_at', ''), reverse=True) return 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 a new session to the list and save. Args: session: New session to add sessions: Existing sessions list Returns: Updated sessions list """ sessions.insert(0, session) # Add to front (newest first) 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_status( session_id: str, status: str, sessions: List[Dict[str, Any]], delivery_info: Optional[Dict[str, Any]] = None ) -> List[Dict[str, Any]]: """Update session status and optionally add delivery info. Args: session_id: Session to update status: New status ('generated', 'delivered', 'failed') sessions: Existing sessions list delivery_info: Optional delivery information to attach Returns: Updated sessions list """ for session in sessions: if session.get('id') == session_id: session['status'] = status session['updated_at'] = datetime.now().isoformat() if delivery_info: session['delivery_info'] = delivery_info 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 """ created = session.get('created_at', 'Unknown') status = session.get('status', 'unknown') file_count = len(session.get('files', [])) # 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 status_emoji = { 'generated': '📝', 'delivered': '✅', 'failed': '❌', }.get(status, '❓') return f"{status_emoji} {created_str} ({file_count} files)" def get_session_files_absolute(session: Dict[str, Any]) -> List[Path]: """Get absolute paths to session files. Args: session: Session dictionary Returns: List of absolute file paths that exist """ files = session.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(session.get('output_directory', '')) combined = output_dir / file_path if combined.exists(): abs_files.append(combined.absolute()) return abs_files