refactor: restructure core→server, split large entity files into database module
- Move src/core/ → src/server/ - Split SerieList.py (531 lines) and series.py (414 lines) into src/server/database/ - Add database/models.py for SQLAlchemy models - Update all test imports to reflect new structure - Remove deprecated test files (test_serie_class.py, test_serie_folder_with_year.py)
This commit is contained in:
200
src/server/error_handler.py
Normal file
200
src/server/error_handler.py
Normal file
@@ -0,0 +1,200 @@
|
||||
"""
|
||||
Error handling and recovery strategies for core providers.
|
||||
|
||||
This module provides custom exceptions and decorators for handling
|
||||
errors in provider operations with automatic retry mechanisms.
|
||||
"""
|
||||
|
||||
import functools
|
||||
import logging
|
||||
from typing import Any, Callable, Optional, TypeVar
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Type variable for decorator
|
||||
F = TypeVar("F", bound=Callable[..., Any])
|
||||
|
||||
|
||||
class RetryableError(Exception):
|
||||
"""Exception that indicates an operation can be safely retried."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class NonRetryableError(Exception):
|
||||
"""Exception that indicates an operation should not be retried."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class NetworkError(Exception):
|
||||
"""Exception for network-related errors."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class DownloadError(Exception):
|
||||
"""Exception for download-related errors."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class RecoveryStrategies:
|
||||
"""Strategies for handling errors and recovering from failures."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
max_retries: int = 3,
|
||||
base_delay: float = 1.0,
|
||||
max_delay: float = 60.0,
|
||||
exponential_base: float = 2.0,
|
||||
) -> None:
|
||||
"""Initialize recovery strategies.
|
||||
|
||||
Args:
|
||||
max_retries: Maximum number of retry attempts.
|
||||
base_delay: Initial delay between retries in seconds.
|
||||
max_delay: Maximum delay between retries in seconds.
|
||||
exponential_base: Base for exponential backoff multiplier.
|
||||
"""
|
||||
self.max_retries = max_retries
|
||||
self.base_delay = base_delay
|
||||
self.max_delay = max_delay
|
||||
self.exponential_base = exponential_base
|
||||
|
||||
def _calculate_delay(self, attempt: int) -> float:
|
||||
"""Calculate delay for given retry attempt using exponential backoff.
|
||||
|
||||
Args:
|
||||
attempt: Zero-based retry attempt number.
|
||||
|
||||
Returns:
|
||||
Delay in seconds before next retry.
|
||||
"""
|
||||
delay = self.base_delay * (self.exponential_base ** attempt)
|
||||
return min(delay, self.max_delay)
|
||||
|
||||
def handle_network_failure(
|
||||
self,
|
||||
func: Callable, *args: Any, **kwargs: Any
|
||||
) -> Any:
|
||||
"""Handle network failures with exponential backoff retry logic."""
|
||||
last_error: Optional[Exception] = None
|
||||
for attempt in range(self.max_retries):
|
||||
try:
|
||||
return func(*args, **kwargs)
|
||||
except (NetworkError, ConnectionError, TimeoutError) as exc:
|
||||
last_error = exc
|
||||
if attempt < self.max_retries - 1:
|
||||
delay = self._calculate_delay(attempt)
|
||||
logger.warning(
|
||||
"Network error on attempt %d/%d, retrying in %.1fs: %s",
|
||||
attempt + 1, self.max_retries, delay, exc
|
||||
)
|
||||
import time
|
||||
time.sleep(delay)
|
||||
continue
|
||||
if last_error:
|
||||
raise last_error
|
||||
raise NetworkError("Network failure after retries")
|
||||
|
||||
def handle_download_failure(
|
||||
self,
|
||||
func: Callable, *args: Any, **kwargs: Any
|
||||
) -> Any:
|
||||
"""Handle download failures with exponential backoff retry logic."""
|
||||
last_error: Optional[Exception] = None
|
||||
for attempt in range(self.max_retries):
|
||||
try:
|
||||
return func(*args, **kwargs)
|
||||
except DownloadError as exc:
|
||||
last_error = exc
|
||||
if attempt < self.max_retries - 1:
|
||||
delay = self._calculate_delay(attempt)
|
||||
logger.warning(
|
||||
"Download error on attempt %d/%d, retrying in %.1fs: %s",
|
||||
attempt + 1, self.max_retries, delay, exc
|
||||
)
|
||||
import time
|
||||
time.sleep(delay)
|
||||
continue
|
||||
if last_error:
|
||||
raise last_error
|
||||
raise DownloadError("Download failed after retries")
|
||||
|
||||
|
||||
class FileCorruptionDetector:
|
||||
"""Detector for corrupted files."""
|
||||
|
||||
@staticmethod
|
||||
def is_valid_video_file(filepath: str) -> bool:
|
||||
"""Check if a video file is valid and not corrupted."""
|
||||
try:
|
||||
import os
|
||||
if not os.path.exists(filepath):
|
||||
return False
|
||||
|
||||
file_size = os.path.getsize(filepath)
|
||||
# Video files should be at least 1MB
|
||||
return file_size > 1024 * 1024
|
||||
except Exception as e:
|
||||
logger.error("Error checking file validity: %s", e)
|
||||
return False
|
||||
|
||||
|
||||
def with_error_recovery(
|
||||
max_retries: int = 3, context: str = ""
|
||||
) -> Callable[[F], F]:
|
||||
"""
|
||||
Decorator for adding error recovery to functions.
|
||||
|
||||
Args:
|
||||
max_retries: Maximum number of retry attempts
|
||||
context: Context string for logging
|
||||
|
||||
Returns:
|
||||
Decorated function with retry logic
|
||||
"""
|
||||
|
||||
def decorator(func: F) -> F:
|
||||
@functools.wraps(func)
|
||||
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
||||
last_error = None
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
return func(*args, **kwargs)
|
||||
except NonRetryableError:
|
||||
raise
|
||||
except Exception as e:
|
||||
last_error = e
|
||||
if attempt < max_retries - 1:
|
||||
logger.warning(
|
||||
"Error in %s (attempt %d/%d): %s, retrying...",
|
||||
context,
|
||||
attempt + 1,
|
||||
max_retries,
|
||||
e,
|
||||
)
|
||||
else:
|
||||
logger.error(
|
||||
"Error in %s failed after %d attempts: %s",
|
||||
context,
|
||||
max_retries,
|
||||
e,
|
||||
)
|
||||
|
||||
if last_error:
|
||||
raise last_error
|
||||
|
||||
raise RuntimeError(
|
||||
f"Unexpected error in {context} after {max_retries} attempts"
|
||||
)
|
||||
|
||||
return wrapper # type: ignore
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
# Create module-level instances for use in provider code
|
||||
recovery_strategies = RecoveryStrategies()
|
||||
file_corruption_detector = FileCorruptionDetector()
|
||||
Reference in New Issue
Block a user