From 96df10adb0adb22103920717bbb5ff0b5cf8e3ea Mon Sep 17 00:00:00 2001 From: Daniel Bauer Date: Mon, 30 Jun 2025 12:01:53 +0200 Subject: [PATCH] fix: update tests for module rename from mcp_cordra to cordra_mcp - Updated all import statements in test files - Added types-requests dependency for mypy type checking - Fixed code formatting with ruff --- pyproject.toml | 1 + src/cordra_mcp/client.py | 53 ++++++++++++------ tests/test_client.py | 52 +++++++++--------- tests/test_server.py | 41 +++++++------- uv.lock | 112 ++++++++++++++++++++++----------------- 5 files changed, 148 insertions(+), 111 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 26cb7cf..6c73836 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -60,4 +60,5 @@ dev = [ "mypy>=1.16.1", "pytest-asyncio>=1.0.0", "ruff>=0.12.1", + "types-requests>=2.32.4.20250611", ] diff --git a/src/cordra_mcp/client.py b/src/cordra_mcp/client.py index 5008842..dda0b3e 100644 --- a/src/cordra_mcp/client.py +++ b/src/cordra_mcp/client.py @@ -1,7 +1,7 @@ """Cordra client wrapper using HTTP requests.""" import logging -from typing import Any +from typing import Any, cast import requests from pydantic import BaseModel, Field @@ -19,21 +19,26 @@ class DigitalObject(BaseModel): content: dict[str, Any] = Field(description="Object content as JSON") metadata: dict[str, Any] | None = Field(default=None, description="Object metadata") acl: dict[str, Any] | None = Field(default=None, description="Access control list") - payloads: list[dict[str, Any]] | None = Field(default=None, description="List of payloads") + payloads: list[dict[str, Any]] | None = Field( + default=None, description="List of payloads" + ) class CordraClientError(Exception): """Base exception for Cordra client errors.""" + pass class CordraNotFoundError(CordraClientError): """Exception raised when an object is not found.""" + pass class CordraAuthenticationError(CordraClientError): """Exception raised for authentication/authorization failures.""" + pass @@ -54,7 +59,9 @@ class CordraClient: if config.username and config.password: self.session.auth = (config.username, config.password) elif config.username or config.password: - logger.warning("Only username or password provided, not both. Authentication may fail.") + logger.warning( + "Only username or password provided, not both. Authentication may fail." + ) def _handle_http_error(self, response: requests.Response, context: str) -> None: """Handle HTTP errors and raise appropriate exceptions. @@ -72,7 +79,9 @@ class CordraClient: if status_code == 404: raise CordraNotFoundError(f"{context}: Resource not found") elif status_code in (401, 403): - raise CordraAuthenticationError(f"{context}: Authentication failed (HTTP {status_code})") + raise CordraAuthenticationError( + f"{context}: Authentication failed (HTTP {status_code})" + ) elif status_code >= 500: raise CordraClientError(f"{context}: Server error (HTTP {status_code})") else: @@ -100,21 +109,25 @@ class CordraClient: response = self.session.get(url, params=params, timeout=self.config.timeout) if not response.ok: - self._handle_http_error(response, f"Failed to retrieve object {object_id}") + self._handle_http_error( + response, f"Failed to retrieve object {object_id}" + ) cordra_obj = response.json() return DigitalObject( id=object_id, - type=cordra_obj.get('type', ''), - content=cordra_obj.get('content', cordra_obj), - metadata=cordra_obj.get('metadata'), - acl=cordra_obj.get('acl'), - payloads=cordra_obj.get('payloads'), + type=cordra_obj.get("type", ""), + content=cordra_obj.get("content", cordra_obj), + metadata=cordra_obj.get("metadata"), + acl=cordra_obj.get("acl"), + payloads=cordra_obj.get("payloads"), ) except requests.RequestException as e: - raise CordraClientError(f"Failed to retrieve object {object_id}: {e}") from e + raise CordraClientError( + f"Failed to retrieve object {object_id}: {e}" + ) from e async def find(self, query: str) -> list[dict[str, Any]]: """Find objects using a Cordra query. @@ -137,18 +150,22 @@ class CordraClient: response = self.session.get(url, params=params, timeout=self.config.timeout) if not response.ok: - self._handle_http_error(response, f"Failed to search with query '{query}'") + self._handle_http_error( + response, f"Failed to search with query '{query}'" + ) search_result = response.json() # Extract the results array from the response - if isinstance(search_result, dict) and 'results' in search_result: - return search_result['results'] + if isinstance(search_result, dict) and "results" in search_result: + return search_result["results"] # type: ignore else: return [] except requests.RequestException as e: - raise CordraClientError(f"Failed to search with query '{query}': {e}") from e + raise CordraClientError( + f"Failed to search with query '{query}': {e}" + ) from e async def get_schema(self, schema_name: str) -> DigitalObject: """Retrieve a schema definition by its name. @@ -177,9 +194,11 @@ class CordraClient: schema_data = schemas[0] # Get the full schema object using its ID - return await self.get_object(schema_data['id']) + return await self.get_object(schema_data["id"]) except (CordraNotFoundError, CordraAuthenticationError): raise except Exception as e: - raise CordraClientError(f"Failed to retrieve schema '{schema_name}': {e}") from e + raise CordraClientError( + f"Failed to retrieve schema '{schema_name}': {e}" + ) from e diff --git a/tests/test_client.py b/tests/test_client.py index 0c8c14c..4cfded4 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -3,13 +3,14 @@ from unittest.mock import patch import pytest -from mcp_cordra.client import ( + +from cordra_mcp.client import ( CordraClient, CordraClientError, CordraNotFoundError, DigitalObject, ) -from mcp_cordra.config import CordraConfig +from cordra_mcp.config import CordraConfig @pytest.fixture @@ -42,15 +43,15 @@ def mock_cordra_object(): "name": "file1.txt", "filename": "file1.txt", "size": 1024, - "mediaType": "text/plain" + "mediaType": "text/plain", }, { "name": "file2.pdf", "filename": "file2.pdf", "size": 2048, - "mediaType": "application/pdf" - } - ] + "mediaType": "application/pdf", + }, + ], } @@ -70,7 +71,7 @@ class TestDigitalObject: "name": "file1.txt", "mediaType": "text/plain", "size": 1024, - "filename": "file1.txt" + "filename": "file1.txt", } ], ) @@ -89,11 +90,7 @@ class TestDigitalObject: def test_digital_object_optional_fields(self): """Test DigitalObject with only required fields.""" - obj = DigitalObject( - id="test/123", - type="TestType", - content={"title": "Test"} - ) + obj = DigitalObject(id="test/123", type="TestType", content={"title": "Test"}) assert obj.id == "test/123" assert obj.type == "TestType" @@ -111,7 +108,7 @@ class TestCordraClient: client = CordraClient(config) assert client.config == config - @patch('mcp_cordra.client.requests.Session.get') + @patch("cordra_mcp.client.requests.Session.get") async def test_get_object_success(self, mock_get, client, mock_cordra_object): """Test successful object retrieval.""" mock_response = mock_get.return_value @@ -124,7 +121,10 @@ class TestCordraClient: assert isinstance(result, DigitalObject) assert result.id == "test/123" assert result.type == "TestType" - assert result.content == {"title": "Test Object", "description": "A test object"} + assert result.content == { + "title": "Test Object", + "description": "A test object", + } assert result.metadata == {"created": "2023-01-01", "modified": "2023-01-02"} assert result.acl == {"read": ["public"], "write": ["admin"]} assert result.payloads and len(result.payloads) == 2 @@ -132,10 +132,10 @@ class TestCordraClient: mock_get.assert_called_once_with( "https://test.example.com/objects/test/123", params={"full": "true"}, - timeout=30 + timeout=30, ) - @patch('mcp_cordra.client.requests.Session.get') + @patch("cordra_mcp.client.requests.Session.get") async def test_get_object_not_found(self, mock_get, client): """Test object not found exception.""" mock_response = mock_get.return_value @@ -147,10 +147,11 @@ class TestCordraClient: assert "Resource not found" in str(exc_info.value) - @patch('mcp_cordra.client.requests.Session.get') + @patch("cordra_mcp.client.requests.Session.get") async def test_get_object_general_error(self, mock_get, client): """Test general error handling.""" from requests import RequestException + mock_get.side_effect = RequestException("Connection failed") with pytest.raises(CordraClientError) as exc_info: @@ -158,16 +159,16 @@ class TestCordraClient: assert "Failed to retrieve object test/123" in str(exc_info.value) - @patch('mcp_cordra.client.requests.Session.get') + @patch("cordra_mcp.client.requests.Session.get") async def test_find_success(self, mock_get, client): """Test successful find operation.""" mock_response_data = { "results": [ {"name": "User", "identifier": "test/user-schema"}, {"name": "Project", "identifier": "test/project-schema"}, - {"name": "Document", "identifier": "test/doc-schema"} + {"name": "Document", "identifier": "test/doc-schema"}, ], - "size": 3 + "size": 3, } mock_response = mock_get.return_value mock_response.status_code = 200 @@ -184,10 +185,10 @@ class TestCordraClient: mock_get.assert_called_once_with( "https://test.example.com/search", params={"query": "type:Schema"}, - timeout=30 + timeout=30, ) - @patch('mcp_cordra.client.requests.Session.get') + @patch("cordra_mcp.client.requests.Session.get") async def test_find_empty_results(self, mock_get, client): """Test find with empty results.""" mock_response_data = {"results": [], "size": 0} @@ -202,10 +203,10 @@ class TestCordraClient: mock_get.assert_called_once_with( "https://test.example.com/search", params={"query": "type:NonExistent"}, - timeout=30 + timeout=30, ) - @patch('mcp_cordra.client.requests.Session.get') + @patch("cordra_mcp.client.requests.Session.get") async def test_find_no_results_key(self, mock_get, client): """Test find with response missing results key.""" mock_response_data = {"size": 0} # No results key @@ -218,10 +219,11 @@ class TestCordraClient: assert result == [] - @patch('mcp_cordra.client.requests.Session.get') + @patch("cordra_mcp.client.requests.Session.get") async def test_find_error(self, mock_get, client): """Test find error handling.""" from requests import RequestException + mock_get.side_effect = RequestException("Search failed") with pytest.raises(CordraClientError) as exc_info: diff --git a/tests/test_server.py b/tests/test_server.py index c517e6a..b3104f7 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -4,8 +4,9 @@ import json from unittest.mock import AsyncMock, patch import pytest -from mcp_cordra.client import CordraClientError, CordraNotFoundError, DigitalObject -from mcp_cordra.server import get_cordra_object + +from cordra_mcp.client import CordraClientError, CordraNotFoundError, DigitalObject +from cordra_mcp.server import get_cordra_object @pytest.fixture @@ -35,7 +36,7 @@ def sample_digital_object(): class TestGetCordraObject: """Test the get_cordra_object resource handler.""" - @patch('mcp_cordra.server.cordra_client') + @patch('cordra_mcp.server.cordra_client') async def test_get_object_success(self, mock_client, sample_digital_object): """Test successful object retrieval.""" mock_client.get_object = AsyncMock(return_value=sample_digital_object) @@ -55,7 +56,7 @@ class TestGetCordraObject: # Verify the client was called with the correct object ID mock_client.get_object.assert_called_once_with("people/john-doe-123") - @patch('mcp_cordra.server.cordra_client') + @patch('cordra_mcp.server.cordra_client') async def test_get_object_not_found(self, mock_client): """Test object not found exception.""" mock_client.get_object = AsyncMock( @@ -68,7 +69,7 @@ class TestGetCordraObject: assert "Object not found: people/nonexistent" in str(exc_info.value) mock_client.get_object.assert_called_once_with("people/nonexistent") - @patch('mcp_cordra.server.cordra_client') + @patch('cordra_mcp.server.cordra_client') async def test_get_object_client_error(self, mock_client): """Test general client error handling.""" mock_client.get_object = AsyncMock( @@ -82,7 +83,7 @@ class TestGetCordraObject: assert "Connection failed" in str(exc_info.value) mock_client.get_object.assert_called_once_with("people/john-doe-123") - @patch('mcp_cordra.server.cordra_client') + @patch('cordra_mcp.server.cordra_client') async def test_object_id_construction(self, mock_client, sample_digital_object): """Test that object ID is correctly constructed from prefix and suffix.""" mock_client.get_object = AsyncMock(return_value=sample_digital_object) @@ -98,7 +99,7 @@ class TestGetCordraObject: await get_cordra_object(prefix, suffix) mock_client.get_object.assert_called_with(expected_id) - @patch('mcp_cordra.server.cordra_client') + @patch('cordra_mcp.server.cordra_client') async def test_json_formatting(self, mock_client, sample_digital_object): """Test that the returned JSON is properly formatted.""" mock_client.get_object = AsyncMock(return_value=sample_digital_object) @@ -120,7 +121,7 @@ class TestGetCordraObject: assert "acl" in parsed_result assert "payloads" in parsed_result - @patch('mcp_cordra.server.cordra_client') + @patch('cordra_mcp.server.cordra_client') async def test_minimal_object(self, mock_client): """Test handling of object with minimal data.""" minimal_object = DigitalObject( @@ -147,7 +148,7 @@ class TestGetCordraObject: class TestSchemaResourceFunctions: """Test the schema resource functions.""" - @patch('mcp_cordra.server.cordra_client') + @patch('cordra_mcp.server.cordra_client') async def test_create_schema_resource_success(self, mock_client): """Test successful schema resource creation.""" mock_schema = DigitalObject( @@ -157,7 +158,7 @@ class TestSchemaResourceFunctions: ) mock_client.get_schema = AsyncMock(return_value=mock_schema) - from mcp_cordra.server import create_schema_resource + from cordra_mcp.server import create_schema_resource result = await create_schema_resource("User") # Verify the result is valid JSON @@ -169,19 +170,19 @@ class TestSchemaResourceFunctions: # Verify the client was called with correct schema name mock_client.get_schema.assert_called_once_with("User") - @patch('mcp_cordra.server.cordra_client') + @patch('cordra_mcp.server.cordra_client') async def test_create_schema_resource_not_found(self, mock_client): """Test schema resource creation with schema not found.""" mock_client.get_schema = AsyncMock(side_effect=CordraNotFoundError("Schema not found")) - from mcp_cordra.server import create_schema_resource + from cordra_mcp.server import create_schema_resource with pytest.raises(RuntimeError) as exc_info: await create_schema_resource("NonExistent") assert "Schema not found: NonExistent" in str(exc_info.value) mock_client.get_schema.assert_called_once_with("NonExistent") - @patch('mcp_cordra.server.cordra_client') + @patch('cordra_mcp.server.cordra_client') async def test_register_schema_resources_success(self, mock_client): """Test successful schema resource registration.""" mock_schemas = [ @@ -192,8 +193,8 @@ class TestSchemaResourceFunctions: mock_client.find = AsyncMock(return_value=mock_schemas) # Mock the mcp.add_resource method - with patch('mcp_cordra.server.mcp') as mock_mcp: - from mcp_cordra.server import register_schema_resources + with patch('cordra_mcp.server.mcp') as mock_mcp: + from cordra_mcp.server import register_schema_resources await register_schema_resources() # Verify the client was called with correct query @@ -202,7 +203,7 @@ class TestSchemaResourceFunctions: # Verify add_resource was called for each schema assert mock_mcp.add_resource.call_count == 3 - @patch('mcp_cordra.server.cordra_client') + @patch('cordra_mcp.server.cordra_client') async def test_register_schema_resources_missing_name(self, mock_client): """Test schema resource registration with objects missing name field.""" mock_schemas = [ @@ -212,20 +213,20 @@ class TestSchemaResourceFunctions: ] mock_client.find = AsyncMock(return_value=mock_schemas) - with patch('mcp_cordra.server.mcp') as mock_mcp: - from mcp_cordra.server import register_schema_resources + with patch('cordra_mcp.server.mcp') as mock_mcp: + from cordra_mcp.server import register_schema_resources await register_schema_resources() # Only 2 schemas should be registered (those with name field) assert mock_mcp.add_resource.call_count == 2 - @patch('mcp_cordra.server.cordra_client') + @patch('cordra_mcp.server.cordra_client') async def test_register_schema_resources_client_error(self, mock_client): """Test schema resource registration with client error.""" mock_client.find = AsyncMock(side_effect=CordraClientError("Search failed")) # Should not raise an exception, just log a warning - from mcp_cordra.server import register_schema_resources + from cordra_mcp.server import register_schema_resources await register_schema_resources() # Should complete without raising mock_client.find.assert_called_once_with("type:Schema") diff --git a/uv.lock b/uv.lock index d01ae19..b090ccd 100644 --- a/uv.lock +++ b/uv.lock @@ -111,6 +111,57 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, ] +[[package]] +name = "cordra-mcp" +version = "0.1.0" +source = { editable = "." } +dependencies = [ + { name = "mcp", extra = ["cli"] }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "requests" }, +] + +[package.optional-dependencies] +dev = [ + { name = "loguru" }, + { name = "mypy" }, + { name = "pytest" }, + { name = "pytest-asyncio" }, + { name = "pytest-cov" }, + { name = "ruff" }, +] + +[package.dev-dependencies] +dev = [ + { name = "mypy" }, + { name = "pytest-asyncio" }, + { name = "ruff" }, + { name = "types-requests" }, +] + +[package.metadata] +requires-dist = [ + { name = "loguru", marker = "extra == 'dev'", specifier = ">=0.7.0" }, + { name = "mcp", extras = ["cli"], specifier = ">=1.2.0" }, + { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.6.0" }, + { name = "pydantic", specifier = ">=2.0.0" }, + { name = "pydantic-settings", specifier = ">=2.0.0" }, + { name = "pytest", marker = "extra == 'dev'", specifier = ">=7.4.0" }, + { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.21.0" }, + { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=4.1.0" }, + { name = "requests", specifier = ">=2.25.0" }, + { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.1.0" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "mypy", specifier = ">=1.16.1" }, + { name = "pytest-asyncio", specifier = ">=1.0.0" }, + { name = "ruff", specifier = ">=0.12.1" }, + { name = "types-requests", specifier = ">=2.32.4.20250611" }, +] + [[package]] name = "coverage" version = "7.9.1" @@ -313,55 +364,6 @@ cli = [ { name = "typer" }, ] -[[package]] -name = "cordra-mcp" -version = "0.1.0" -source = { editable = "." } -dependencies = [ - { name = "mcp", extra = ["cli"] }, - { name = "pydantic" }, - { name = "pydantic-settings" }, - { name = "requests" }, -] - -[package.optional-dependencies] -dev = [ - { name = "loguru" }, - { name = "mypy" }, - { name = "pytest" }, - { name = "pytest-asyncio" }, - { name = "pytest-cov" }, - { name = "ruff" }, -] - -[package.dev-dependencies] -dev = [ - { name = "mypy" }, - { name = "pytest-asyncio" }, - { name = "ruff" }, -] - -[package.metadata] -requires-dist = [ - { name = "loguru", marker = "extra == 'dev'", specifier = ">=0.7.0" }, - { name = "mcp", extras = ["cli"], specifier = ">=1.2.0" }, - { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.6.0" }, - { name = "pydantic", specifier = ">=2.0.0" }, - { name = "pydantic-settings", specifier = ">=2.0.0" }, - { name = "pytest", marker = "extra == 'dev'", specifier = ">=7.4.0" }, - { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.21.0" }, - { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=4.1.0" }, - { name = "requests", specifier = ">=2.25.0" }, - { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.1.0" }, -] - -[package.metadata.requires-dev] -dev = [ - { name = "mypy", specifier = ">=1.16.1" }, - { name = "pytest-asyncio", specifier = ">=1.0.0" }, - { name = "ruff", specifier = ">=0.12.1" }, -] - [[package]] name = "mdurl" version = "0.1.2" @@ -840,6 +842,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/76/42/3efaf858001d2c2913de7f354563e3a3a2f0decae3efe98427125a8f441e/typer-0.16.0-py3-none-any.whl", hash = "sha256:1f79bed11d4d02d4310e3c1b7ba594183bcedb0ac73b27a9e5f28f6fb5b98855", size = 46317 }, ] +[[package]] +name = "types-requests" +version = "2.32.4.20250611" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6d/7f/73b3a04a53b0fd2a911d4ec517940ecd6600630b559e4505cc7b68beb5a0/types_requests-2.32.4.20250611.tar.gz", hash = "sha256:741c8777ed6425830bf51e54d6abe245f79b4dcb9019f1622b773463946bf826", size = 23118 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/ea/0be9258c5a4fa1ba2300111aa5a0767ee6d18eb3fd20e91616c12082284d/types_requests-2.32.4.20250611-py3-none-any.whl", hash = "sha256:ad2fe5d3b0cb3c2c902c8815a70e7fb2302c4b8c1f77bdcd738192cdb3878072", size = 20643 }, +] + [[package]] name = "typing-extensions" version = "4.14.0"