diff --git a/code_quality_checks.sh b/code_quality_checks.sh index 8031ed8..8529543 100755 --- a/code_quality_checks.sh +++ b/code_quality_checks.sh @@ -67,16 +67,16 @@ echo "📋 Step 1: Running Linting and Formatting Checks" echo "--------------------------------------------------" echo "🔧 Running ruff linting with auto-fix..." -$RUFF check --fix --exclude test_simulation_files +$RUFF check --fix --exclude test_simulation_files --exclude .zen_venv echo "🎨 Running black code formatting..." -$BLACK . --exclude="test_simulation_files/" +$BLACK . --exclude="test_simulation_files/" --exclude=".zen_venv/" echo "📦 Running import sorting with isort..." $ISORT . --skip-glob=".zen_venv/*" --skip-glob="test_simulation_files/*" echo "✅ Verifying all linting passes..." -$RUFF check --exclude test_simulation_files +$RUFF check --exclude test_simulation_files --exclude .zen_venv echo "✅ Step 1 Complete: All linting and formatting checks passed!" echo "" diff --git a/conf/__init__.py b/conf/__init__.py new file mode 100644 index 0000000..ee1bfd4 --- /dev/null +++ b/conf/__init__.py @@ -0,0 +1 @@ +"""Configuration data for Zen MCP Server.""" diff --git a/providers/openrouter_registry.py b/providers/openrouter_registry.py index 97b8f60..949e2c8 100644 --- a/providers/openrouter_registry.py +++ b/providers/openrouter_registry.py @@ -1,10 +1,12 @@ """OpenRouter model registry for managing model configurations and aliases.""" +import importlib.resources import logging import os from pathlib import Path from typing import Optional +# Import handled via importlib.resources.files() calls directly from utils.file_utils import read_json_file from .base import ( @@ -26,7 +28,8 @@ class OpenRouterModelRegistry: self.alias_map: dict[str, str] = {} # alias -> model_name self.model_map: dict[str, ModelCapabilities] = {} # model_name -> config - # Determine config path + # Determine config path and loading strategy + self.use_resources = False if config_path: # Direct config_path parameter self.config_path = Path(config_path) @@ -37,9 +40,33 @@ class OpenRouterModelRegistry: # Environment variable path self.config_path = Path(env_path) else: - # Default to conf/custom_models.json - use relative path from this file - # This works in development environment - self.config_path = Path(__file__).parent.parent / "conf" / "custom_models.json" + # Try importlib.resources for robust packaging support + self.config_path = None + self.use_resources = False + + try: + resource_traversable = importlib.resources.files("conf").joinpath("custom_models.json") + if hasattr(resource_traversable, "read_text"): + self.use_resources = True + else: + raise AttributeError("read_text not available") + except Exception: + pass + + if not self.use_resources: + # Fallback to file system paths + potential_paths = [ + Path(__file__).parent.parent / "conf" / "custom_models.json", + Path.cwd() / "conf" / "custom_models.json", + ] + + for path in potential_paths: + if path.exists(): + self.config_path = path + break + + if self.config_path is None: + self.config_path = potential_paths[0] # Load configuration self.reload() @@ -91,20 +118,44 @@ class OpenRouterModelRegistry: self.model_map = {} def _read_config(self) -> list[ModelCapabilities]: - """Read configuration from file. + """Read configuration from file or package resources. Returns: List of model configurations """ - if not self.config_path.exists(): - logging.warning(f"OpenRouter model config not found at {self.config_path}") - return [] - try: - # Use centralized JSON reading utility - data = read_json_file(str(self.config_path)) + if self.use_resources: + # Use importlib.resources for packaged environments + try: + resource_path = importlib.resources.files("conf").joinpath("custom_models.json") + if hasattr(resource_path, "read_text"): + # Python 3.9+ + config_text = resource_path.read_text(encoding="utf-8") + else: + # Python 3.8 fallback + with resource_path.open("r", encoding="utf-8") as f: + config_text = f.read() + + import json + + data = json.loads(config_text) + logging.debug("Loaded OpenRouter config from package resources") + except Exception as e: + logging.warning(f"Failed to load config from resources: {e}") + return [] + else: + # Use file path loading + if not self.config_path.exists(): + logging.warning(f"OpenRouter model config not found at {self.config_path}") + return [] + + # Use centralized JSON reading utility + data = read_json_file(str(self.config_path)) + logging.debug(f"Loaded OpenRouter config from file: {self.config_path}") + if data is None: - raise ValueError(f"Could not read or parse JSON from {self.config_path}") + location = "resources" if self.use_resources else str(self.config_path) + raise ValueError(f"Could not read or parse JSON from {location}") # Parse models configs = [] @@ -137,7 +188,8 @@ class OpenRouterModelRegistry: # Re-raise ValueError for specific config errors raise except Exception as e: - raise ValueError(f"Error reading config from {self.config_path}: {e}") + location = "resources" if self.use_resources else str(self.config_path) + raise ValueError(f"Error reading config from {location}: {e}") def _build_maps(self, configs: list[ModelCapabilities]) -> None: """Build alias and model maps from configurations. diff --git a/pyproject.toml b/pyproject.toml index 476e8c9..3080a66 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,7 +12,7 @@ dependencies = [ ] [tool.setuptools.packages.find] -include = ["tools*", "providers*", "systemprompts*", "utils*"] +include = ["tools*", "providers*", "systemprompts*", "utils*", "conf*"] [tool.setuptools] py-modules = ["server", "config"] diff --git a/requirements.txt b/requirements.txt index a021f7a..6e2b713 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,6 +3,7 @@ google-genai>=1.19.0 openai>=1.55.2 # Minimum version for httpx 0.28.0 compatibility pydantic>=2.0.0 python-dotenv>=1.0.0 +importlib-resources>=5.0.0; python_version<"3.9" # Development dependencies (install with pip install -r requirements-dev.txt) # pytest>=7.4.0 diff --git a/tests/test_uvx_resource_packaging.py b/tests/test_uvx_resource_packaging.py new file mode 100644 index 0000000..86df066 --- /dev/null +++ b/tests/test_uvx_resource_packaging.py @@ -0,0 +1,103 @@ +"""Tests for uvx path resolution functionality.""" + +from pathlib import Path +from unittest.mock import patch + +from providers.openrouter_registry import OpenRouterModelRegistry + + +class TestUvxPathResolution: + """Test uvx path resolution for OpenRouter model registry.""" + + def test_normal_operation(self): + """Test that normal operation works in development environment.""" + registry = OpenRouterModelRegistry() + assert len(registry.list_models()) > 0 + assert len(registry.list_aliases()) > 0 + + def test_config_path_resolution(self): + """Test that the config path resolution finds the config file in multiple locations.""" + # Check that the config file exists in the development location + config_file = Path(__file__).parent.parent / "conf" / "custom_models.json" + assert config_file.exists(), "Config file should exist in conf/custom_models.json" + + # Test that a registry can find and use the config + registry = OpenRouterModelRegistry() + + # When using resources, config_path is None; when using file system, it should exist + if registry.use_resources: + assert registry.config_path is None, "When using resources, config_path should be None" + else: + assert registry.config_path.exists(), "When using file system, config path should exist" + + assert len(registry.list_models()) > 0, "Registry should load models from config" + + def test_explicit_config_path_override(self): + """Test that explicit config path works correctly.""" + config_path = Path(__file__).parent.parent / "conf" / "custom_models.json" + + registry = OpenRouterModelRegistry(config_path=str(config_path)) + + # Should use the provided file path + assert registry.config_path == config_path + assert len(registry.list_models()) > 0 + + def test_environment_variable_override(self): + """Test that CUSTOM_MODELS_CONFIG_PATH environment variable works.""" + config_path = Path(__file__).parent.parent / "conf" / "custom_models.json" + + with patch.dict("os.environ", {"CUSTOM_MODELS_CONFIG_PATH": str(config_path)}): + registry = OpenRouterModelRegistry() + + # Should use environment path + assert registry.config_path == config_path + assert len(registry.list_models()) > 0 + + @patch("providers.openrouter_registry.importlib.resources.files") + @patch("pathlib.Path.exists") + def test_multiple_path_fallback(self, mock_exists, mock_files): + """Test that multiple path resolution works for different deployment scenarios.""" + # Make resources loading fail to trigger file system fallback + mock_files.side_effect = Exception("Resource loading failed") + + # Simulate dev path failing, and working directory path succeeding + # The third `True` is for the check within `reload()` + mock_exists.side_effect = [False, True, True] + + registry = OpenRouterModelRegistry() + + # Should have fallen back to file system mode + assert not registry.use_resources, "Should fall back to file system when resources fail" + + # Assert that the registry fell back to the second potential path + assert registry.config_path == Path.cwd() / "conf" / "custom_models.json" + + # Should load models successfully + assert len(registry.list_models()) > 0 + + def test_missing_config_handling(self): + """Test behavior when config file is missing.""" + # Use a non-existent path + registry = OpenRouterModelRegistry(config_path="/nonexistent/path/config.json") + + # Should gracefully handle missing config + assert len(registry.list_models()) == 0 + assert len(registry.list_aliases()) == 0 + + def test_resource_loading_success(self): + """Test successful resource loading via importlib.resources.""" + # Just test that the registry works normally in our environment + # This validates the resource loading mechanism indirectly + registry = OpenRouterModelRegistry() + + # Should load successfully using either resources or file system fallback + assert len(registry.list_models()) > 0 + assert len(registry.list_aliases()) > 0 + + def test_use_resources_attribute(self): + """Test that the use_resources attribute is properly set.""" + registry = OpenRouterModelRegistry() + + # Should have the use_resources attribute + assert hasattr(registry, "use_resources") + assert isinstance(registry.use_resources, bool)