""" Optional feature detection for xml-pipeline. This module checks which optional dependencies are installed and provides graceful degradation when features are unavailable. """ from dataclasses import dataclass, field from importlib.util import find_spec from typing import Callable def _check_import(module: str) -> bool: """Check if a module can be imported.""" return find_spec(module) is not None # Feature registry: feature_name -> (check_function, description) FEATURES: dict[str, tuple[Callable[[], bool], str]] = { "anthropic": (lambda: _check_import("anthropic"), "Anthropic Claude SDK"), "openai": (lambda: _check_import("openai"), "OpenAI SDK"), "redis": (lambda: _check_import("redis"), "Redis for distributed keyvalue"), "search": (lambda: _check_import("duckduckgo_search"), "DuckDuckGo search"), "auth": (lambda: _check_import("pyotp") and _check_import("argon2"), "TOTP auth"), "server": (lambda: _check_import("websockets"), "WebSocket server"), } def get_available_features() -> dict[str, bool]: """Return dict of feature_name -> is_available.""" return {name: check() for name, (check, _) in FEATURES.items()} def is_feature_available(feature: str) -> bool: """Check if a specific feature is available.""" if feature not in FEATURES: return False check, _ = FEATURES[feature] return check() def require_feature(feature: str) -> None: """Raise ImportError if feature is not available.""" if not is_feature_available(feature): _, description = FEATURES.get(feature, (None, feature)) raise ImportError( f"Feature '{feature}' is not installed. " f"Install with: pip install xml-pipeline[{feature}]" ) @dataclass class FeatureCheck: """Result of checking features against a config.""" available: dict[str, bool] = field(default_factory=dict) missing: dict[str, str] = field(default_factory=dict) # feature -> reason needed def check_features(config) -> FeatureCheck: """ Check which optional features are needed for a config. Returns FeatureCheck with available features and missing ones needed by config. """ result = FeatureCheck(available=get_available_features()) # Check LLM backends for backend in getattr(config, "llm_backends", []): provider = getattr(backend, "provider", "").lower() if provider == "anthropic" and not result.available.get("anthropic"): result.missing["anthropic"] = f"LLM backend '{backend.name}' uses Anthropic" if provider == "openai" and not result.available.get("openai"): result.missing["openai"] = f"LLM backend '{backend.name}' uses OpenAI" # Check tools for listener in getattr(config, "listeners", []): # If listener uses keyvalue tool and redis is configured # This would need more sophisticated detection based on tool config pass # Check if auth is needed (multi-tenant mode) if getattr(config, "auth", None): if not result.available.get("auth"): result.missing["auth"] = "Config has auth enabled" # Check if websocket server is needed if getattr(config, "server", None): if not result.available.get("server"): result.missing["server"] = "Config has server enabled" return result # Lazy import helpers for optional dependencies def get_redis_client(): """Get Redis client, or raise helpful error.""" require_feature("redis") import redis return redis def get_anthropic_client(): """Get Anthropic client, or raise helpful error.""" require_feature("anthropic") import anthropic return anthropic def get_openai_client(): """Get OpenAI client, or raise helpful error.""" require_feature("openai") import openai return openai