feat: better error handling.

This commit is contained in:
daniel
2025-06-29 23:45:00 +02:00
parent 53cbb53178
commit 4ef2f50e72
3 changed files with 63 additions and 28 deletions

View File

@@ -32,6 +32,11 @@ class CordraNotFoundError(CordraClientError):
pass pass
class CordraAuthenticationError(CordraClientError):
"""Exception raised for authentication/authorization failures."""
pass
class CordraClient: class CordraClient:
"""Client for interacting with Cordra repository using HTTP requests.""" """Client for interacting with Cordra repository using HTTP requests."""
@@ -51,6 +56,28 @@ class CordraClient:
elif config.username or 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.
Args:
response: The HTTP response object
context: Context description for the error message
Raises:
CordraNotFoundError: For 404 errors
CordraAuthenticationError: For 401/403 errors
CordraClientError: For other HTTP errors
"""
status_code = response.status_code
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})")
elif status_code >= 500:
raise CordraClientError(f"{context}: Server error (HTTP {status_code})")
else:
raise CordraClientError(f"{context}: HTTP error {status_code}")
async def get_object(self, object_id: str) -> DigitalObject: async def get_object(self, object_id: str) -> DigitalObject:
"""Retrieve a digital object by its ID. """Retrieve a digital object by its ID.
@@ -61,22 +88,20 @@ class CordraClient:
The full digital object The full digital object
Raises: Raises:
ValueError: If object_id is empty
CordraNotFoundError: If the object is not found CordraNotFoundError: If the object is not found
CordraAuthenticationError: If authentication fails
CordraClientError: For other API errors CordraClientError: For other API errors
""" """
try:
# Build URL: cordra_base_url/objects/prefix/postfix
url = f"{self.config.cordra_url}/objects/{object_id}" url = f"{self.config.cordra_url}/objects/{object_id}"
# Add full=true parameter to get complete object details
params = {"full": "true"} params = {"full": "true"}
try:
response = self.session.get(url, params=params, timeout=self.config.timeout) response = self.session.get(url, params=params, timeout=self.config.timeout)
if response.status_code == 404: if not response.ok:
raise CordraNotFoundError(f"Object not found: {object_id}") self._handle_http_error(response, f"Failed to retrieve object {object_id}")
response.raise_for_status()
cordra_obj = response.json() cordra_obj = response.json()
return DigitalObject( return DigitalObject(
@@ -88,12 +113,8 @@ class CordraClient:
payloads=cordra_obj.get('payloads'), payloads=cordra_obj.get('payloads'),
) )
except CordraNotFoundError:
raise
except requests.RequestException as e: 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
except Exception as e:
raise CordraClientError(f"Failed to retrieve object {object_id}: {e}") from e
async def find(self, query: str) -> list[dict[str, Any]]: async def find(self, query: str) -> list[dict[str, Any]]:
"""Find objects using a Cordra query. """Find objects using a Cordra query.
@@ -105,15 +126,18 @@ class CordraClient:
List of objects matching the query as dictionaries List of objects matching the query as dictionaries
Raises: Raises:
CordraClientError: If there's an API error ValueError: If query is empty
CordraAuthenticationError: If authentication fails
CordraClientError: For other API errors
""" """
try:
# Use HTTP GET request to search endpoint
url = f"{self.config.cordra_url}/search" url = f"{self.config.cordra_url}/search"
params = {"query": query} params = {"query": query}
try:
response = self.session.get(url, params=params, timeout=self.config.timeout) response = self.session.get(url, params=params, timeout=self.config.timeout)
response.raise_for_status()
if not response.ok:
self._handle_http_error(response, f"Failed to search with query '{query}'")
search_result = response.json() search_result = response.json()
@@ -125,5 +149,3 @@ class CordraClient:
except requests.RequestException as e: 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
except Exception as e:
raise CordraClientError(f"Failed to search with query '{query}': {e}") from e

View File

@@ -5,7 +5,12 @@ import json
from mcp.server.fastmcp import FastMCP from mcp.server.fastmcp import FastMCP
from .client import CordraClient, CordraClientError, CordraNotFoundError from .client import (
CordraClient,
CordraClientError,
CordraNotFoundError,
CordraAuthenticationError,
)
from .config import CordraConfig from .config import CordraConfig
# Initialize the MCP server # Initialize the MCP server
@@ -36,8 +41,12 @@ async def get_cordra_object(prefix: str, suffix: str) -> str:
object_dict = digital_object.model_dump() object_dict = digital_object.model_dump()
return json.dumps(object_dict, indent=2) return json.dumps(object_dict, indent=2)
except CordraNotFoundError as e: except ValueError as e:
raise RuntimeError(f"Object not found: {object_id}") from e raise RuntimeError(f"Invalid parameters: {e}") from e
except CordraNotFoundError:
raise RuntimeError(f"Object not found: {object_id}")
except CordraAuthenticationError as e:
raise RuntimeError(f"Authentication failed: {e}") from e
except CordraClientError as e: except CordraClientError as e:
raise RuntimeError(f"Failed to retrieve object {object_id}: {e}") from e raise RuntimeError(f"Failed to retrieve object {object_id}: {e}") from e
@@ -68,7 +77,9 @@ async def list_cordra_schemas() -> str:
} }
return json.dumps(result, indent=2) return json.dumps(result, indent=2)
except Exception as e: except CordraAuthenticationError as e:
raise RuntimeError(f"Authentication failed: {e}") from e
except CordraClientError as e:
raise RuntimeError(f"Failed to list schemas: {e}") from e raise RuntimeError(f"Failed to list schemas: {e}") from e

View File

@@ -8,6 +8,7 @@ from mcp_cordra.client import (
CordraClient, CordraClient,
CordraClientError, CordraClientError,
CordraNotFoundError, CordraNotFoundError,
CordraAuthenticationError,
DigitalObject, DigitalObject,
) )
from mcp_cordra.config import CordraConfig from mcp_cordra.config import CordraConfig
@@ -141,11 +142,12 @@ class TestCordraClient:
"""Test object not found exception.""" """Test object not found exception."""
mock_response = mock_get.return_value mock_response = mock_get.return_value
mock_response.status_code = 404 mock_response.status_code = 404
mock_response.ok = False
with pytest.raises(CordraNotFoundError) as exc_info: with pytest.raises(CordraNotFoundError) as exc_info:
await client.get_object("test/nonexistent") await client.get_object("test/nonexistent")
assert "Object not found: test/nonexistent" in str(exc_info.value) assert "Resource not found" in str(exc_info.value)
@patch('mcp_cordra.client.requests.Session.get') @patch('mcp_cordra.client.requests.Session.get')
async def test_get_object_general_error(self, mock_get, client): async def test_get_object_general_error(self, mock_get, client):