Ajasra 24394d7b92 Add custom exceptions and enhance error handling in exchanges module
- Introduced a new `exceptions.py` file containing custom exceptions for the exchanges module, improving error specificity and handling.
- Updated the `factory.py` and `registry.py` files to utilize the new exceptions, enhancing robustness in error reporting and logging.
- Implemented validation logic in `ExchangeCollectorConfig` to ensure proper configuration, raising appropriate exceptions when validation fails.
- Enhanced logging throughout the factory methods to provide better insights into the collector creation process and error scenarios.
- Added comprehensive documentation for the exchanges module, detailing the architecture, error handling, and usage examples.

These changes significantly improve the error handling and maintainability of the exchanges module, aligning with project standards and enhancing developer experience.
2025-06-07 14:29:09 +08:00

237 lines
8.3 KiB
Python

"""
Exchange Factory for creating data collectors.
This module provides a factory pattern for creating data collectors
from different exchanges based on configuration.
"""
import importlib
from typing import Dict, List, Optional, Any, Type, Tuple
from dataclasses import dataclass
from utils.logger import get_logger
from ..base_collector import BaseDataCollector, DataType
from .registry import EXCHANGE_REGISTRY, get_supported_exchanges, get_exchange_info
from .exceptions import (
ExchangeError,
ExchangeNotSupportedError,
InvalidConfigurationError,
CollectorCreationError,
ValidationError
)
# Initialize logger
logger = get_logger('exchanges')
@dataclass
class ExchangeCollectorConfig:
"""Configuration for creating an exchange collector."""
exchange: str
symbol: str
data_types: List[DataType]
auto_restart: bool = True
health_check_interval: float = 30.0
store_raw_data: bool = True
custom_params: Optional[Dict[str, Any]] = None
def __post_init__(self):
"""Validate configuration after initialization."""
if not self.exchange:
raise InvalidConfigurationError("Exchange name cannot be empty")
if not self.symbol:
raise InvalidConfigurationError("Symbol cannot be empty")
if not self.data_types:
raise InvalidConfigurationError("At least one data type must be specified")
logger.debug(f"Created collector config for {self.exchange} {self.symbol}")
class ExchangeFactory:
"""Factory for creating exchange-specific data collectors."""
@staticmethod
def create_collector(config: ExchangeCollectorConfig) -> BaseDataCollector:
"""
Create a data collector for the specified exchange.
Args:
config: Configuration for the collector
Returns:
Instance of the appropriate collector class
Raises:
ExchangeNotSupportedError: If exchange is not supported
CollectorCreationError: If collector creation fails
"""
exchange_name = config.exchange.lower()
logger.info(f"Creating collector for {exchange_name} {config.symbol}")
if exchange_name not in EXCHANGE_REGISTRY:
supported = get_supported_exchanges()
error_msg = f"Exchange '{config.exchange}' not supported. Supported exchanges: {supported}"
logger.error(error_msg)
raise ExchangeNotSupportedError(error_msg)
exchange_info = get_exchange_info(exchange_name)
collector_class_path = exchange_info['collector']
# Parse module and class name
module_path, class_name = collector_class_path.rsplit('.', 1)
try:
# Import the module
logger.debug(f"Importing collector module {module_path}")
module = importlib.import_module(module_path)
# Get the collector class
collector_class = getattr(module, class_name)
# Prepare collector arguments
collector_args = {
'symbol': config.symbol,
'data_types': config.data_types,
'auto_restart': config.auto_restart,
'health_check_interval': config.health_check_interval,
'store_raw_data': config.store_raw_data
}
# Add any custom parameters
if config.custom_params:
collector_args.update(config.custom_params)
# Create and return the collector instance
logger.info(f"Successfully created collector for {exchange_name} {config.symbol}")
return collector_class(**collector_args)
except ImportError as e:
error_msg = f"Failed to import collector class '{collector_class_path}': {e}"
logger.error(error_msg)
raise CollectorCreationError(error_msg) from e
except Exception as e:
error_msg = f"Failed to create collector for '{config.exchange}': {e}"
logger.error(error_msg)
raise CollectorCreationError(error_msg) from e
@staticmethod
def create_multiple_collectors(configs: List[ExchangeCollectorConfig]) -> List[BaseDataCollector]:
"""
Create multiple collectors from a list of configurations.
Args:
configs: List of collector configurations
Returns:
List of collector instances
"""
collectors = []
logger.info(f"Creating {len(configs)} collectors")
for config in configs:
try:
collector = ExchangeFactory.create_collector(config)
collectors.append(collector)
logger.debug(f"Successfully created collector for {config.exchange} {config.symbol}")
except ExchangeError as e:
logger.error(f"Failed to create collector for {config.exchange} {config.symbol}: {e}")
logger.info(f"Successfully created {len(collectors)} out of {len(configs)} collectors")
return collectors
@staticmethod
def get_supported_pairs(exchange: str) -> List[str]:
"""
Get supported trading pairs for an exchange.
Args:
exchange: Exchange name
Returns:
List of supported trading pairs
"""
exchange_info = get_exchange_info(exchange)
if exchange_info:
return exchange_info.get('supported_pairs', [])
return []
@staticmethod
def get_supported_data_types(exchange: str) -> List[str]:
"""
Get supported data types for an exchange.
Args:
exchange: Exchange name
Returns:
List of supported data types
"""
exchange_info = get_exchange_info(exchange)
if exchange_info:
return exchange_info.get('supported_data_types', [])
return []
@staticmethod
def validate_config(config: ExchangeCollectorConfig) -> Tuple[bool, List[str]]:
"""
Validate collector configuration.
Args:
config: Configuration to validate
Returns:
Tuple of (is_valid, list_of_errors)
"""
logger.debug(f"Validating configuration for {config.exchange} {config.symbol}")
errors = []
# Check if exchange is supported
if config.exchange.lower() not in EXCHANGE_REGISTRY:
errors.append(f"Exchange '{config.exchange}' not supported")
# Check if symbol is supported
supported_pairs = ExchangeFactory.get_supported_pairs(config.exchange)
if supported_pairs and config.symbol not in supported_pairs:
errors.append(f"Symbol '{config.symbol}' not supported for {config.exchange}")
# Check if data types are supported
supported_data_types = ExchangeFactory.get_supported_data_types(config.exchange)
if supported_data_types:
for data_type in config.data_types:
if data_type.value not in supported_data_types:
errors.append(f"Data type '{data_type.value}' not supported for {config.exchange}")
is_valid = len(errors) == 0
if not is_valid:
logger.warning(f"Configuration validation failed for {config.exchange}: {errors}")
else:
logger.debug(f"Configuration validation passed for {config.exchange}")
return is_valid, errors
def create_okx_collector(symbol: str,
data_types: Optional[List[DataType]] = None,
**kwargs) -> BaseDataCollector:
"""
Convenience function to create an OKX collector.
Args:
symbol: Trading pair symbol (e.g., 'BTC-USDT')
data_types: List of data types to collect
**kwargs: Additional collector parameters
Returns:
OKX collector instance
"""
if data_types is None:
data_types = [DataType.TRADE, DataType.ORDERBOOK]
logger.debug(f"Creating OKX collector for {symbol}")
config = ExchangeCollectorConfig(
exchange='okx',
symbol=symbol,
data_types=data_types,
**kwargs
)
return ExchangeFactory.create_collector(config)