"""Custom exception hierarchy for the Bible translator. Provides specific exception types for different error scenarios to enable proper error handling and recovery strategies. """ from __future__ import annotations class TranslationError(Exception): """Base exception for all translation-related errors.""" def __init__(self, message: str, details: dict | None = None) -> None: """Initialize the translation error. Args: message: Human-readable error message. details: Optional dictionary with additional error details. """ super().__init__(message) self.details = details or {} class ClaudeAPIError(TranslationError): """Error communicating with Claude CLI/API.""" def __init__( self, message: str, stderr: str | None = None, return_code: int | None = None, ) -> None: """Initialize the Claude API error. Args: message: Human-readable error message. stderr: Standard error output from the CLI. return_code: Process return code. """ details = { "stderr": stderr, "return_code": return_code, } super().__init__(message, details) self.stderr = stderr self.return_code = return_code class RateLimitError(ClaudeAPIError): """Rate limit exceeded when calling Claude.""" def __init__( self, message: str = "Rate limit exceeded", retry_after: float | None = None, ) -> None: """Initialize the rate limit error. Args: message: Human-readable error message. retry_after: Suggested wait time in seconds before retrying. """ super().__init__(message) self.retry_after = retry_after self.details["retry_after"] = retry_after class ModelNotFoundError(ClaudeAPIError): """Requested model is not available.""" def __init__(self, model: str) -> None: """Initialize the model not found error. Args: model: The model identifier that was not found. """ super().__init__(f"Model not found: {model}") self.model = model self.details["model"] = model class CLINotFoundError(ClaudeAPIError): """Claude CLI is not installed or not in PATH.""" def __init__(self) -> None: """Initialize the CLI not found error.""" super().__init__( "Claude CLI not found. Install with: npm install -g @anthropic-ai/claude-code" ) class VerseNotFoundError(TranslationError): """Requested verse does not exist.""" def __init__(self, book: str, chapter: int, verse: int) -> None: """Initialize the verse not found error. Args: book: Book name. chapter: Chapter number. verse: Verse number. """ super().__init__(f"Verse not found: {book} {chapter}:{verse}") self.book = book self.chapter = chapter self.verse = verse self.details = {"book": book, "chapter": chapter, "verse": verse} class InvalidReferenceError(TranslationError): """Invalid verse reference format.""" def __init__(self, reference: str, reason: str | None = None) -> None: """Initialize the invalid reference error. Args: reference: The malformed reference string. reason: Optional explanation of what's wrong. """ msg = f"Invalid reference format: {reference}" if reason: msg += f" ({reason})" super().__init__(msg) self.reference = reference self.reason = reason self.details = {"reference": reference, "reason": reason} class ChunkParsingError(TranslationError): """Error parsing chunk/pericope translation response.""" def __init__( self, message: str, response: str | None = None, expected_verses: list[str] | None = None, ) -> None: """Initialize the chunk parsing error. Args: message: Human-readable error message. response: The raw response that failed to parse. expected_verses: List of verse references that were expected. """ super().__init__(message) self.response = response self.expected_verses = expected_verses self.details = { "response_preview": response[:500] if response else None, "expected_verses": expected_verses, } class PericopeNotFoundError(TranslationError): """Requested pericope does not exist.""" def __init__(self, pericope_id: str) -> None: """Initialize the pericope not found error. Args: pericope_id: The pericope identifier that was not found. """ super().__init__(f"Pericope not found: {pericope_id}") self.pericope_id = pericope_id self.details = {"pericope_id": pericope_id} class EvaluationError(TranslationError): """Error during translation evaluation.""" def __init__( self, message: str, verse_ref: str | None = None, dimension: str | None = None, ) -> None: """Initialize the evaluation error. Args: message: Human-readable error message. verse_ref: The verse being evaluated. dimension: The evaluation dimension that failed. """ super().__init__(message) self.verse_ref = verse_ref self.dimension = dimension self.details = {"verse_ref": verse_ref, "dimension": dimension} class QualityThresholdError(TranslationError): """Translation failed to meet quality threshold.""" def __init__( self, message: str, score: float, threshold: float, dimension: str | None = None, ) -> None: """Initialize the quality threshold error. Args: message: Human-readable error message. score: The actual score achieved. threshold: The required minimum score. dimension: The dimension that failed (if specific). """ super().__init__(message) self.score = score self.threshold = threshold self.dimension = dimension self.details = { "score": score, "threshold": threshold, "dimension": dimension, } class StorageError(TranslationError): """Error reading or writing translation data.""" def __init__( self, message: str, path: str | None = None, operation: str | None = None, ) -> None: """Initialize the storage error. Args: message: Human-readable error message. path: The file path involved. operation: The operation that failed (read/write). """ super().__init__(message) self.path = path self.operation = operation self.details = {"path": path, "operation": operation} class GlossaryMatchError(TranslationError): """Error in glossary term matching.""" def __init__(self, message: str, term: str | None = None) -> None: """Initialize the glossary match error. Args: message: Human-readable error message. term: The term being matched. """ super().__init__(message) self.term = term self.details = {"term": term} class ConfigurationError(TranslationError): """Error in configuration loading or validation.""" def __init__(self, message: str, config_key: str | None = None) -> None: """Initialize the configuration error. Args: message: Human-readable error message. config_key: The configuration key that caused the error. """ super().__init__(message) self.config_key = config_key self.details = {"config_key": config_key} def classify_claude_error(stderr: str, return_code: int) -> ClaudeAPIError: """Classify a Claude CLI error based on stderr and return code. Args: stderr: Standard error output from the CLI. return_code: Process return code. Returns: Appropriate ClaudeAPIError subclass instance. """ stderr_lower = stderr.lower() # Check for rate limiting if "rate limit" in stderr_lower or "429" in stderr_lower: return RateLimitError(stderr) # Check for model not found if "model" in stderr_lower and ("not found" in stderr_lower or "invalid" in stderr_lower): # Try to extract model name return ClaudeAPIError(f"Model error: {stderr}", stderr, return_code) # Check for CLI not installed if return_code == 127 or "command not found" in stderr_lower: return CLINotFoundError() # Check for network errors if "network" in stderr_lower or "connection" in stderr_lower or "timeout" in stderr_lower: return ClaudeAPIError(f"Network error: {stderr}", stderr, return_code) # Default to generic API error return ClaudeAPIError(f"Claude CLI failed: {stderr}", stderr, return_code)