237 lines
7.9 KiB
Python
237 lines
7.9 KiB
Python
"""
|
|
Modulo per la gestione robusta degli errori nei market providers.
|
|
|
|
Fornisce decoratori e utilità per:
|
|
- Retry automatico con backoff esponenziale
|
|
- Logging standardizzato degli errori
|
|
- Gestione di timeout e rate limiting
|
|
- Fallback tra provider multipli
|
|
"""
|
|
|
|
import time
|
|
import logging
|
|
from functools import wraps
|
|
from typing import Any, Callable, Optional, Type, Union, List
|
|
from requests.exceptions import RequestException, Timeout, ConnectionError
|
|
from binance.exceptions import BinanceAPIException, BinanceRequestException
|
|
from .base import ProductInfo
|
|
|
|
# Configurazione logging
|
|
logger = logging.getLogger(__name__)
|
|
|
|
class MarketAPIError(Exception):
|
|
"""Eccezione base per errori delle API di mercato."""
|
|
pass
|
|
|
|
class RateLimitError(MarketAPIError):
|
|
"""Eccezione per errori di rate limiting."""
|
|
pass
|
|
|
|
class AuthenticationError(MarketAPIError):
|
|
"""Eccezione per errori di autenticazione."""
|
|
pass
|
|
|
|
class DataNotFoundError(MarketAPIError):
|
|
"""Eccezione quando i dati richiesti non sono disponibili."""
|
|
pass
|
|
|
|
def retry_on_failure(
|
|
max_retries: int = 3,
|
|
delay: float = 1.0,
|
|
backoff_factor: float = 2.0,
|
|
exceptions: tuple = (RequestException, BinanceAPIException, BinanceRequestException)
|
|
) -> Callable:
|
|
"""
|
|
Decoratore per retry automatico con backoff esponenziale.
|
|
|
|
Args:
|
|
max_retries: Numero massimo di tentativi
|
|
delay: Delay iniziale in secondi
|
|
backoff_factor: Fattore di moltiplicazione per il delay
|
|
exceptions: Tuple di eccezioni da catturare per il retry
|
|
|
|
Returns:
|
|
Decoratore per la funzione
|
|
"""
|
|
def decorator(func: Callable) -> Callable:
|
|
@wraps(func)
|
|
def wrapper(*args, **kwargs) -> Any:
|
|
last_exception = None
|
|
current_delay = delay
|
|
|
|
for attempt in range(max_retries + 1):
|
|
try:
|
|
return func(*args, **kwargs)
|
|
except exceptions as e:
|
|
last_exception = e
|
|
|
|
if attempt == max_retries:
|
|
logger.error(
|
|
f"Function {func.__name__} failed after {max_retries + 1} attempts. "
|
|
f"Last error: {str(e)}"
|
|
)
|
|
raise MarketAPIError(f"Max retries exceeded: {str(e)}") from e
|
|
|
|
logger.warning(
|
|
f"Attempt {attempt + 1}/{max_retries + 1} failed for {func.__name__}: {str(e)}. "
|
|
f"Retrying in {current_delay:.1f}s..."
|
|
)
|
|
|
|
time.sleep(current_delay)
|
|
current_delay *= backoff_factor
|
|
except Exception as e:
|
|
# Per eccezioni non previste, non fare retry
|
|
logger.error(f"Unexpected error in {func.__name__}: {str(e)}")
|
|
raise
|
|
|
|
# Questo non dovrebbe mai essere raggiunto
|
|
if last_exception:
|
|
raise last_exception
|
|
else:
|
|
raise MarketAPIError("Unknown error occurred")
|
|
|
|
return wrapper
|
|
return decorator
|
|
|
|
def handle_api_errors(func: Callable) -> Callable:
|
|
"""
|
|
Decoratore per gestione standardizzata degli errori API.
|
|
|
|
Converte errori specifici dei provider in eccezioni standardizzate.
|
|
"""
|
|
@wraps(func)
|
|
def wrapper(*args, **kwargs) -> Any:
|
|
try:
|
|
return func(*args, **kwargs)
|
|
except BinanceAPIException as e:
|
|
if e.code == -1021: # Timestamp error
|
|
raise MarketAPIError(f"Binance timestamp error: {e.message}")
|
|
elif e.code == -1003: # Rate limit
|
|
raise RateLimitError(f"Binance rate limit exceeded: {e.message}")
|
|
elif e.code in [-2014, -2015]: # API key errors
|
|
raise AuthenticationError(f"Binance authentication error: {e.message}")
|
|
else:
|
|
raise MarketAPIError(f"Binance API error [{e.code}]: {e.message}")
|
|
except ConnectionError as e:
|
|
raise MarketAPIError(f"Connection error: {str(e)}")
|
|
except Timeout as e:
|
|
raise MarketAPIError(f"Request timeout: {str(e)}")
|
|
except RequestException as e:
|
|
raise MarketAPIError(f"Request error: {str(e)}")
|
|
except Exception as e:
|
|
logger.error(f"Unexpected error in {func.__name__}: {str(e)}")
|
|
raise MarketAPIError(f"Unexpected error: {str(e)}") from e
|
|
|
|
return wrapper
|
|
|
|
def safe_execute(
|
|
func: Callable,
|
|
default_value: Any = None,
|
|
log_errors: bool = True,
|
|
error_message: Optional[str] = None
|
|
) -> Any:
|
|
"""
|
|
Esegue una funzione in modo sicuro, restituendo un valore di default in caso di errore.
|
|
|
|
Args:
|
|
func: Funzione da eseguire
|
|
default_value: Valore da restituire in caso di errore
|
|
log_errors: Se loggare gli errori
|
|
error_message: Messaggio di errore personalizzato
|
|
|
|
Returns:
|
|
Risultato della funzione o valore di default
|
|
"""
|
|
try:
|
|
return func()
|
|
except Exception as e:
|
|
if log_errors:
|
|
message = error_message or f"Error executing {func.__name__}"
|
|
logger.warning(f"{message}: {str(e)}")
|
|
return default_value
|
|
|
|
class ProviderFallback:
|
|
"""
|
|
Classe per gestire il fallback tra provider multipli.
|
|
"""
|
|
|
|
def __init__(self, providers: List[Any]):
|
|
"""
|
|
Inizializza con una lista di provider ordinati per priorità.
|
|
|
|
Args:
|
|
providers: Lista di provider ordinati per priorità
|
|
"""
|
|
self.providers = providers
|
|
|
|
def execute_with_fallback(
|
|
self,
|
|
method_name: str,
|
|
*args,
|
|
**kwargs
|
|
) -> list[ProductInfo]:
|
|
"""
|
|
Esegue un metodo su tutti i provider fino a trovarne uno che funziona.
|
|
|
|
Args:
|
|
method_name: Nome del metodo da chiamare
|
|
*args: Argomenti posizionali
|
|
**kwargs: Argomenti nominali
|
|
|
|
Returns:
|
|
Risultato del primo provider che funziona
|
|
|
|
Raises:
|
|
MarketAPIError: Se tutti i provider falliscono
|
|
"""
|
|
last_error = None
|
|
|
|
for i, provider in enumerate(self.providers):
|
|
try:
|
|
if hasattr(provider, method_name):
|
|
method = getattr(provider, method_name)
|
|
result = method(*args, **kwargs)
|
|
|
|
if i > 0: # Se non è il primo provider
|
|
logger.info(f"Fallback successful: used provider {type(provider).__name__}")
|
|
|
|
return result
|
|
else:
|
|
logger.warning(f"Provider {type(provider).__name__} doesn't have method {method_name}")
|
|
continue
|
|
|
|
except Exception as e:
|
|
last_error = e
|
|
logger.warning(
|
|
f"Provider {type(provider).__name__} failed for {method_name}: {str(e)}"
|
|
)
|
|
continue
|
|
|
|
# Se arriviamo qui, tutti i provider hanno fallito
|
|
raise MarketAPIError(
|
|
f"All providers failed for method {method_name}. Last error: {str(last_error)}"
|
|
)
|
|
|
|
def validate_response_data(data: Any, required_fields: Optional[List[str]] = None) -> bool:
|
|
"""
|
|
Valida che i dati di risposta contengano i campi richiesti.
|
|
|
|
Args:
|
|
data: Dati da validare
|
|
required_fields: Lista di campi richiesti
|
|
|
|
Returns:
|
|
True se i dati sono validi, False altrimenti
|
|
"""
|
|
if data is None:
|
|
return False
|
|
|
|
if required_fields is None:
|
|
return True
|
|
|
|
if isinstance(data, dict):
|
|
return all(field in data for field in required_fields)
|
|
elif hasattr(data, '__dict__'):
|
|
return all(hasattr(data, field) for field in required_fields)
|
|
|
|
return False |