Skip to content
126 changes: 103 additions & 23 deletions src/giskard_hub/_base_client.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
from __future__ import annotations

from typing import Optional
import json
from typing import Optional, Tuple

import httpx

from .errors import (
HubAPIError,
HubAuthenticationError,
HubForbiddenError,
HubJSONDecodeError,
HubValidationError,
)

_default_http_client_kwargs = {
"follow_redirects": True,
"timeout": httpx.Timeout(30.0),
Expand All @@ -19,34 +28,105 @@ def __init__(self, *, http_client: Optional[httpx.Client] = None):
def _headers(self):
return {}

def _extract_error_message(self, response: httpx.Response, default_msg: str) -> str:
"""Extract error message from response, falling back to default_msg if not found"""
try:
error_data = response.json()
except json.JSONDecodeError:
return default_msg

return error_data.get("message", default_msg)

def _extract_validation_errors(self, response: httpx.Response) -> Tuple[str, str]:
"""Extract validation error message and field errors from response"""
error_message = "Validation error: please check your request"
fields_str = ""

try:
error_data = response.json()
except json.JSONDecodeError:
return error_message, fields_str

error_message = error_data.get("message", error_message)
fields_str = str(error_data.get("fields", fields_str))

return error_message, fields_str

def _request(self, method: str, path: str, *, cast_to=None, **kwargs):
res = self._http.request(
method=method,
url=path,
headers=self._headers(),
**kwargs,
)
try:
res = self._http.request(
method=method,
url=path,
headers=self._headers(),
**kwargs,
)
except Exception as e:
raise HubAPIError(
f"Unexpected error while making HTTP request: {str(e)}",
response_text=str(e),
) from e

# Handle authentication errors
if res.status_code == 401:
raise HubAuthenticationError(
"Authentication failed: please check your API key",
status_code=res.status_code,
response_text=res.text,
)

# Handle forbidden errors
if res.status_code == 403:
error_message = self._extract_error_message(
res, "You don't have permission to access this resource"
)
raise HubForbiddenError(
error_message,
status_code=res.status_code,
response_text=res.text,
)

# Handle validation errors
if res.status_code == 422:
error_message, fields_str = self._extract_validation_errors(res)
if fields_str:
error_message = f"{error_message}\n{fields_str}"

raise HubValidationError(
error_message,
status_code=res.status_code,
response_text=res.text,
)

# Handle other HTTP errors
try:
res.raise_for_status()
except httpx.HTTPStatusError as e:
if e.response.status_code == httpx.codes.UNPROCESSABLE_ENTITY:
raise ValueError("Validation error: " + e.response.text)

try:
detail = e.response.json()
raise httpx.HTTPStatusError(
message="API Error: " + detail.get("detail", e.response.text),
request=e.request,
response=e.response,
)
except TypeError:
pass

raise e
data = res.json()
error_message = self._extract_error_message(e.response, e.response.text)
raise HubAPIError(
error_message,
status_code=e.response.status_code,
response_text=e.response.text,
) from e

# Parse response JSON
try:
data = res.json()
except json.JSONDecodeError as e:
raise HubJSONDecodeError(
f"Failed to decode API response as JSON: {str(e)}",
status_code=res.status_code,
response_text=res.text,
) from e

if cast_to:
data = self._cast_data_to(cast_to, data)
try:
data = self._cast_data_to(cast_to, data)
except Exception as e:
raise HubAPIError(
f"Error casting API response data: {str(e)}",
status_code=res.status_code,
response_text=res.text,
) from e

return data

Expand Down
4 changes: 2 additions & 2 deletions src/giskard_hub/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,12 +109,12 @@ def __init__(
data = resp.json()
except Exception as e:
raise HubConnectionError(
f"Failed to connect to Giskard Hub at {self._hub_url}."
f"Failed to connect to Giskard Hub at {self._hub_url}"
) from e

if "openapi" not in data:
raise HubConnectionError(
f"The response doesn't appear to include an OpenAPI specification at {self._hub_url}."
f"The response doesn't appear to include an OpenAPI specification at {self._hub_url}"
)

# Define the resources
Expand Down
30 changes: 29 additions & 1 deletion src/giskard_hub/errors.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,30 @@
class HubConnectionError(Exception):
class HubAPIError(Exception):
"""Base class for all Giskard Hub API errors."""

def __init__(
self, message: str, status_code: int = None, response_text: str = None
):
self.message = message
self.status_code = status_code
self.response_text = response_text
super().__init__(self.message)


class HubConnectionError(HubAPIError):
"""Error raised when a connection to Giskard Hub fails."""


class HubValidationError(HubAPIError):
"""Error raised when the API request validation fails."""


class HubJSONDecodeError(HubAPIError):
"""Error raised when the API response cannot be decoded as JSON."""


class HubAuthenticationError(HubAPIError):
"""Error raised when authentication with the Hub fails."""


class HubForbiddenError(HubAPIError):
"""Error raised when the user is not authorized to access a resource."""
Loading