Files
FastAnime/fastanime/cli/utils/session_manager.py

334 lines
11 KiB
Python

"""
Session state management utilities for the interactive CLI.
Provides comprehensive session save/resume functionality with error handling and metadata.
"""
import json
import logging
from datetime import datetime
from pathlib import Path
from typing import Dict, List, Optional
from ...core.constants import APP_DATA_DIR
from ..interactive.state import State
logger = logging.getLogger(__name__)
# Session storage directory
SESSIONS_DIR = APP_DATA_DIR / "sessions"
AUTO_SAVE_FILE = SESSIONS_DIR / "auto_save.json"
CRASH_BACKUP_FILE = SESSIONS_DIR / "crash_backup.json"
class SessionMetadata:
"""Metadata for saved sessions."""
def __init__(
self,
created_at: Optional[datetime] = None,
last_saved: Optional[datetime] = None,
session_name: Optional[str] = None,
description: Optional[str] = None,
state_count: int = 0
):
self.created_at = created_at or datetime.now()
self.last_saved = last_saved or datetime.now()
self.session_name = session_name
self.description = description
self.state_count = state_count
def to_dict(self) -> dict:
"""Convert metadata to dictionary for JSON serialization."""
return {
"created_at": self.created_at.isoformat(),
"last_saved": self.last_saved.isoformat(),
"session_name": self.session_name,
"description": self.description,
"state_count": self.state_count
}
@classmethod
def from_dict(cls, data: dict) -> "SessionMetadata":
"""Create metadata from dictionary."""
return cls(
created_at=datetime.fromisoformat(data.get("created_at", datetime.now().isoformat())),
last_saved=datetime.fromisoformat(data.get("last_saved", datetime.now().isoformat())),
session_name=data.get("session_name"),
description=data.get("description"),
state_count=data.get("state_count", 0)
)
class SessionData:
"""Complete session data including history and metadata."""
def __init__(self, history: List[State], metadata: SessionMetadata):
self.history = history
self.metadata = metadata
def to_dict(self) -> dict:
"""Convert session data to dictionary for JSON serialization."""
return {
"metadata": self.metadata.to_dict(),
"history": [state.model_dump(mode="json") for state in self.history],
"format_version": "1.0" # For future compatibility
}
@classmethod
def from_dict(cls, data: dict) -> "SessionData":
"""Create session data from dictionary."""
metadata = SessionMetadata.from_dict(data.get("metadata", {}))
history_data = data.get("history", [])
history = []
for state_dict in history_data:
try:
state = State.model_validate(state_dict)
history.append(state)
except Exception as e:
logger.warning(f"Skipping invalid state in session: {e}")
return cls(history, metadata)
class SessionManager:
"""Manages session save/resume functionality with comprehensive error handling."""
def __init__(self):
self._ensure_sessions_directory()
def _ensure_sessions_directory(self):
"""Ensure the sessions directory exists."""
SESSIONS_DIR.mkdir(parents=True, exist_ok=True)
def save_session(
self,
history: List[State],
file_path: Path,
session_name: Optional[str] = None,
description: Optional[str] = None,
feedback=None
) -> bool:
"""
Save session history to a JSON file with metadata.
Args:
history: List of session states
file_path: Path to save the session
session_name: Optional name for the session
description: Optional description
feedback: Optional feedback manager for user notifications
Returns:
True if successful, False otherwise
"""
try:
# Create metadata
metadata = SessionMetadata(
session_name=session_name,
description=description,
state_count=len(history)
)
# Create session data
session_data = SessionData(history, metadata)
# Save to file
with file_path.open('w', encoding='utf-8') as f:
json.dump(session_data.to_dict(), f, indent=2, ensure_ascii=False)
if feedback:
feedback.success(
"Session saved successfully",
f"Saved {len(history)} states to {file_path.name}"
)
logger.info(f"Session saved to {file_path} with {len(history)} states")
return True
except Exception as e:
error_msg = f"Failed to save session: {e}"
if feedback:
feedback.error("Failed to save session", str(e))
logger.error(error_msg)
return False
def load_session(self, file_path: Path, feedback=None) -> Optional[List[State]]:
"""
Load session history from a JSON file.
Args:
file_path: Path to the session file
feedback: Optional feedback manager for user notifications
Returns:
List of states if successful, None otherwise
"""
if not file_path.exists():
if feedback:
feedback.warning(
"Session file not found",
f"The file {file_path.name} does not exist"
)
logger.warning(f"Session file not found: {file_path}")
return None
try:
with file_path.open('r', encoding='utf-8') as f:
data = json.load(f)
session_data = SessionData.from_dict(data)
if feedback:
feedback.success(
"Session loaded successfully",
f"Loaded {len(session_data.history)} states from {file_path.name}"
)
logger.info(f"Session loaded from {file_path} with {len(session_data.history)} states")
return session_data.history
except json.JSONDecodeError as e:
error_msg = f"Session file is corrupted: {e}"
if feedback:
feedback.error("Session file is corrupted", str(e))
logger.error(error_msg)
return None
except Exception as e:
error_msg = f"Failed to load session: {e}"
if feedback:
feedback.error("Failed to load session", str(e))
logger.error(error_msg)
return None
def auto_save_session(self, history: List[State]) -> bool:
"""
Auto-save session for crash recovery.
Args:
history: Current session history
Returns:
True if successful, False otherwise
"""
return self.save_session(
history,
AUTO_SAVE_FILE,
session_name="Auto Save",
description="Automatically saved session"
)
def create_crash_backup(self, history: List[State]) -> bool:
"""
Create a crash backup of the current session.
Args:
history: Current session history
Returns:
True if successful, False otherwise
"""
return self.save_session(
history,
CRASH_BACKUP_FILE,
session_name="Crash Backup",
description="Session backup created before potential crash"
)
def has_auto_save(self) -> bool:
"""Check if an auto-save file exists."""
return AUTO_SAVE_FILE.exists()
def has_crash_backup(self) -> bool:
"""Check if a crash backup file exists."""
return CRASH_BACKUP_FILE.exists()
def load_auto_save(self, feedback=None) -> Optional[List[State]]:
"""Load the auto-save session."""
return self.load_session(AUTO_SAVE_FILE, feedback)
def load_crash_backup(self, feedback=None) -> Optional[List[State]]:
"""Load the crash backup session."""
return self.load_session(CRASH_BACKUP_FILE, feedback)
def clear_auto_save(self) -> bool:
"""Clear the auto-save file."""
try:
if AUTO_SAVE_FILE.exists():
AUTO_SAVE_FILE.unlink()
return True
except Exception as e:
logger.error(f"Failed to clear auto-save: {e}")
return False
def clear_crash_backup(self) -> bool:
"""Clear the crash backup file."""
try:
if CRASH_BACKUP_FILE.exists():
CRASH_BACKUP_FILE.unlink()
return True
except Exception as e:
logger.error(f"Failed to clear crash backup: {e}")
return False
def list_saved_sessions(self) -> List[Dict[str, str]]:
"""
List all saved session files with their metadata.
Returns:
List of dictionaries containing session information
"""
sessions = []
for session_file in SESSIONS_DIR.glob("*.json"):
if session_file.name in ["auto_save.json", "crash_backup.json"]:
continue
try:
with session_file.open('r', encoding='utf-8') as f:
data = json.load(f)
metadata = data.get("metadata", {})
sessions.append({
"file": session_file.name,
"path": str(session_file),
"name": metadata.get("session_name", "Unnamed"),
"description": metadata.get("description", "No description"),
"created": metadata.get("created_at", "Unknown"),
"last_saved": metadata.get("last_saved", "Unknown"),
"state_count": metadata.get("state_count", 0)
})
except Exception as e:
logger.warning(f"Failed to read session metadata from {session_file}: {e}")
# Sort by last saved time (newest first)
sessions.sort(key=lambda x: x["last_saved"], reverse=True)
return sessions
def cleanup_old_sessions(self, max_sessions: int = 10) -> int:
"""
Clean up old session files, keeping only the most recent ones.
Args:
max_sessions: Maximum number of sessions to keep
Returns:
Number of sessions deleted
"""
sessions = self.list_saved_sessions()
if len(sessions) <= max_sessions:
return 0
deleted_count = 0
sessions_to_delete = sessions[max_sessions:]
for session in sessions_to_delete:
try:
Path(session["path"]).unlink()
deleted_count += 1
logger.info(f"Deleted old session: {session['name']}")
except Exception as e:
logger.error(f"Failed to delete session {session['name']}: {e}")
return deleted_count