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.
This commit is contained in:
Ajasra
2025-06-07 14:29:09 +08:00
parent 0c8c1c06e3
commit 24394d7b92
5 changed files with 356 additions and 49 deletions

View File

@@ -0,0 +1,30 @@
"""
Custom exceptions for the exchanges module.
This module contains all custom exceptions used in the exchanges package
to provide more specific error handling and better error messages.
"""
class ExchangeError(Exception):
"""Base exception for all exchange-related errors."""
pass
class ExchangeNotSupportedError(ExchangeError):
"""Raised when an exchange is not supported or not found in registry."""
pass
class InvalidConfigurationError(ExchangeError):
"""Raised when exchange configuration is invalid."""
pass
class CollectorCreationError(ExchangeError):
"""Raised when there's an error creating a collector instance."""
pass
class ExchangeConnectionError(ExchangeError):
"""Raised when there's an error connecting to an exchange."""
pass
class ValidationError(ExchangeError):
"""Raised when validation fails for exchange parameters."""
pass

View File

@@ -6,12 +6,22 @@ from different exchanges based on configuration.
"""
import importlib
from typing import Dict, List, Optional, Any, Type
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:
@@ -24,6 +34,16 @@ class ExchangeCollectorConfig:
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."""
@@ -40,15 +60,17 @@ class ExchangeFactory:
Instance of the appropriate collector class
Raises:
ValueError: If exchange is not supported
ImportError: If collector class cannot be imported
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()
raise ValueError(f"Exchange '{config.exchange}' not supported. "
f"Supported exchanges: {supported}")
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']
@@ -58,6 +80,7 @@ class ExchangeFactory:
try:
# Import the module
logger.debug(f"Importing collector module {module_path}")
module = importlib.import_module(module_path)
# Get the collector class
@@ -77,12 +100,17 @@ class ExchangeFactory:
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:
raise ImportError(f"Failed to import collector class '{collector_class_path}': {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:
raise RuntimeError(f"Failed to create collector for '{config.exchange}': {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]:
@@ -96,15 +124,17 @@ class ExchangeFactory:
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)
except Exception as e:
# Log error but continue with other collectors
print(f"Failed to create collector for {config.exchange} {config.symbol}: {e}")
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
@@ -140,7 +170,7 @@ class ExchangeFactory:
return []
@staticmethod
def validate_config(config: ExchangeCollectorConfig) -> bool:
def validate_config(config: ExchangeCollectorConfig) -> Tuple[bool, List[str]]:
"""
Validate collector configuration.
@@ -148,25 +178,34 @@ class ExchangeFactory:
config: Configuration to validate
Returns:
True if valid, False otherwise
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:
return False
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:
return False
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:
return False
errors.append(f"Data type '{data_type.value}' not supported for {config.exchange}")
return True
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,
@@ -186,6 +225,8 @@ def create_okx_collector(symbol: str,
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,

View File

@@ -5,6 +5,12 @@ This module contains the registry of supported exchanges and their capabilities,
separated to avoid circular import issues.
"""
from utils.logger import get_logger
from .exceptions import ExchangeNotSupportedError
# Initialize logger
logger = get_logger('exchanges')
# Exchange registry for factory pattern
EXCHANGE_REGISTRY = {
'okx': {
@@ -17,11 +23,33 @@ EXCHANGE_REGISTRY = {
}
def get_supported_exchanges():
def get_supported_exchanges() -> list:
"""Get list of supported exchange names."""
return list(EXCHANGE_REGISTRY.keys())
exchanges = list(EXCHANGE_REGISTRY.keys())
logger.debug(f"Available exchanges: {exchanges}")
return exchanges
def get_exchange_info(exchange_name: str):
"""Get information about a specific exchange."""
return EXCHANGE_REGISTRY.get(exchange_name.lower())
def get_exchange_info(exchange_name: str) -> dict:
"""
Get information about a specific exchange.
Args:
exchange_name: Name of the exchange
Returns:
Dictionary containing exchange information
Raises:
ExchangeNotSupportedError: If exchange is not found in registry
"""
exchange_name = exchange_name.lower()
exchange_info = EXCHANGE_REGISTRY.get(exchange_name)
if not exchange_info:
error_msg = f"Exchange '{exchange_name}' not found in registry"
logger.error(error_msg)
raise ExchangeNotSupportedError(error_msg)
logger.debug(f"Retrieved info for exchange: {exchange_name}")
return exchange_info