TradingAgents/tradingagents/spektiv/dataflows/vendor_registry.py

254 lines
7.6 KiB
Python

"""
Vendor Registry System for Interface Routing (Issue #11).
This module provides:
1. VendorCapability - Enum for vendor capabilities
2. VendorMetadata - Dataclass for vendor information
3. VendorRegistry - Thread-safe singleton for vendor registration and lookup
4. VendorRegistrationError - Custom exception for registration errors
The registry enables centralized vendor management with priority-based
routing, capability tracking, and thread-safe access patterns.
"""
import threading
from dataclasses import dataclass, field
from enum import Enum
from typing import List, Dict, Any, Optional
class VendorCapability(str, Enum):
"""
Enum for vendor capabilities.
Defines standard data provider capabilities for routing requests
to appropriate vendors.
"""
STOCK_DATA = "stock_data"
FUNDAMENTALS = "fundamentals"
TECHNICAL_INDICATORS = "technical_indicators"
NEWS = "news"
MACROECONOMIC = "macroeconomic"
INSIDER_DATA = "insider_data"
@dataclass
class VendorMetadata:
"""
Metadata for a registered vendor.
Attributes:
name: Vendor identifier (e.g., "alpha_vantage")
capabilities: List of VendorCapability values
methods: Dict mapping method names to implementation names
priority: Vendor priority for routing (higher = preferred)
rate_limit: Maximum calls per minute (None = unlimited)
"""
name: str
capabilities: List[str]
methods: Dict[str, str]
priority: int = 0
rate_limit: Optional[int] = None
class VendorRegistrationError(Exception):
"""Exception raised for vendor registration errors."""
pass
class VendorRegistry:
"""
Thread-safe singleton registry for vendor management.
Provides centralized registration, lookup, and routing for data vendors.
Uses double-checked locking for thread-safe singleton pattern.
Thread Safety:
All public methods use internal locking to ensure thread-safe access.
Registry state is protected by _lock during mutations.
Usage:
registry = VendorRegistry()
registry.register_vendor(vendor_class, metadata)
vendors = registry.get_vendor_for_method("get_stock_data")
"""
_instance = None
_lock = threading.Lock()
def __new__(cls):
"""
Create or return singleton instance with double-checked locking.
Returns:
VendorRegistry: Singleton instance
"""
if cls._instance is None:
with cls._lock:
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance._initialized = False
return cls._instance
def __init__(self):
"""Initialize registry storage (only once for singleton)."""
if self._initialized:
return
with self._lock:
if self._initialized:
return
self._vendors: Dict[str, Dict[str, Any]] = {}
self._method_map: Dict[str, List[Dict[str, Any]]] = {}
self._initialized = True
def register_vendor(self, vendor_class: type, metadata: VendorMetadata) -> None:
"""
Register a vendor with its metadata.
Args:
vendor_class: Vendor class to register
metadata: VendorMetadata describing vendor capabilities
Thread Safety:
Uses lock to ensure atomic registration
"""
with self._lock:
# Store vendor and metadata
self._vendors[metadata.name] = {
'class': vendor_class,
'metadata': metadata
}
# Update method map for each method
for method_name, impl_name in metadata.methods.items():
if method_name not in self._method_map:
self._method_map[method_name] = []
# Remove existing entry for this vendor if present
self._method_map[method_name] = [
entry for entry in self._method_map[method_name]
if entry['vendor'] != metadata.name
]
# Add new entry
self._method_map[method_name].append({
'vendor': metadata.name,
'priority': metadata.priority,
'implementation': impl_name
})
# Sort by priority (highest first)
self._method_map[method_name].sort(
key=lambda x: x['priority'],
reverse=True
)
def get_vendor_for_method(self, method_name: str) -> List[str]:
"""
Get list of vendors supporting a method, ordered by priority.
Args:
method_name: Method name to lookup (e.g., "get_stock_data")
Returns:
List of vendor names ordered by priority (highest first)
Empty list if no vendors support the method
Thread Safety:
Read-only operation, no locking needed for immutable view
"""
if method_name not in self._method_map:
return []
return [entry['vendor'] for entry in self._method_map[method_name]]
def get_vendor_metadata(self, vendor_name: str) -> VendorMetadata:
"""
Get metadata for a specific vendor.
Args:
vendor_name: Name of vendor to lookup
Returns:
VendorMetadata for the vendor
Raises:
ValueError: If vendor not found
Thread Safety:
Read-only operation, no locking needed for immutable view
"""
if vendor_name not in self._vendors:
raise ValueError(f"Vendor '{vendor_name}' not found")
return self._vendors[vendor_name]['metadata']
def list_all_vendors(self) -> List[str]:
"""
List all registered vendor names.
Returns:
List of registered vendor names
Thread Safety:
Read-only operation, no locking needed for immutable view
"""
return list(self._vendors.keys())
def get_methods_by_capability(self, capability: str) -> List[str]:
"""
Get all methods provided by vendors with a specific capability.
Args:
capability: Capability to search for (e.g., "stock_data")
Returns:
List of method names provided by vendors with this capability
Thread Safety:
Read-only operation, no locking needed for immutable view
"""
methods = set()
for vendor_name, vendor_data in self._vendors.items():
metadata = vendor_data['metadata']
if capability in metadata.capabilities:
methods.update(metadata.methods.keys())
return list(methods)
def get_vendor_implementation(self, vendor_name: str, method_name: str) -> Optional[str]:
"""
Get the implementation name for a specific vendor and method.
Args:
vendor_name: Name of vendor
method_name: Name of method
Returns:
Implementation method name, or None if not found
Thread Safety:
Read-only operation, no locking needed for immutable view
"""
if vendor_name not in self._vendors:
return None
metadata = self._vendors[vendor_name]['metadata']
return metadata.methods.get(method_name)
def clear_registry(self) -> None:
"""
Clear all registered vendors (primarily for testing).
Thread Safety:
Uses lock to ensure atomic clear operation
"""
with self._lock:
self._vendors.clear()
self._method_map.clear()