Skip to content

Commit 73ebba4

Browse files
committed
Add centralized error handling to base client class
1 parent 7f7365c commit 73ebba4

File tree

2 files changed

+105
-7
lines changed

2 files changed

+105
-7
lines changed

src/datalab_api/__init__.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,9 @@
55
from pathlib import Path
66
from typing import Any, Optional, Union
77

8-
from ._base import BaseDatalabClient, __version__
8+
from ._base import BaseDatalabClient, DuplicateItemError, __version__
99

10-
__all__ = ("DatalabClient", "__version__")
11-
12-
13-
class DuplicateItemError(ValueError):
14-
"""Raised when the API operation would create a duplicate item."""
10+
__all__ = ("DatalabClient", "DuplicateItemError", "__version__")
1511

1612

1713
class DatalabClient(BaseDatalabClient):

src/datalab_api/_base.py

Lines changed: 103 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,14 @@
1010

1111
from .utils import AutoPrettyPrint
1212

13+
14+
class DatalabAPIError(Exception):
15+
"""Base exception for Datalab API errors."""
16+
17+
18+
class DuplicateItemError(DatalabAPIError):
19+
"""Raised when the API operation would create a duplicate item."""
20+
1321
__version__ = version("datalab-api")
1422

1523
__all__ = ("BaseDatalabClient", "__version__")
@@ -50,7 +58,7 @@ def __init__(self, datalab_api_url: str, log_level: str = "WARNING"):
5058
5159
Parameters:
5260
datalab_api_url: The URL of the Datalab API.
53-
TODO: If the URL of a datalab *UI* is provided, a request will be made to attempt
61+
If the URL of a datalab *UI* is provided, a request will be made to attempt
5462
to resolve the underlying API URL (e.g., `https://demo-api.datalab-org.io`
5563
will 'redirect' to `https://demo-api.datalab-org.io`).
5664
log_level: The logging level to use for the client. Defaults to "WARNING".
@@ -204,3 +212,97 @@ def __enter__(self):
204212
def __exit__(self, *_):
205213
if self._session is not None:
206214
self._session.close()
215+
216+
def _handle_response(
217+
self, response: httpx.Response, url: str, expected_status: int = 200
218+
) -> dict[str, Any]:
219+
"""Handle HTTP response with consistent error handling.
220+
221+
Args:
222+
response: The HTTP response object
223+
url: The URL that was requested
224+
expected_status: The expected HTTP status code (default: 200)
225+
226+
Returns:
227+
The JSON response data
228+
229+
Raises:
230+
DuplicateItemError: For 409 conflicts or duplicate key errors
231+
DatalabAPIError: For other API errors
232+
"""
233+
# Handle HTTP status code errors
234+
if response.status_code != expected_status:
235+
try:
236+
error_data = response.json()
237+
error_message = error_data.get("message", str(response.content))
238+
239+
# Handle specific error cases
240+
if response.status_code == 409:
241+
raise DuplicateItemError(f"Duplicate item error at {url}: {error_message}")
242+
243+
raise DatalabAPIError(
244+
f"HTTP {response.status_code} error at {url}: {error_message}"
245+
)
246+
except ValueError:
247+
# Response is not JSON
248+
raise DatalabAPIError(
249+
f"HTTP {response.status_code} error at {url}: {response.content}"
250+
)
251+
252+
# Parse JSON response
253+
try:
254+
data = response.json()
255+
except ValueError as e:
256+
raise DatalabAPIError(f"Invalid JSON response from {url}: {e}")
257+
258+
# Handle API-level status errors
259+
if isinstance(data, dict) and data.get("status") != "success":
260+
error_message = data.get("message", data.get("status", "Unknown error"))
261+
262+
# Check for duplicate key errors in the message
263+
if "DuplicateKeyError" in error_message:
264+
raise DuplicateItemError(f"Duplicate item error at {url}: {error_message}")
265+
266+
raise DatalabAPIError(f"API error at {url}: {error_message}")
267+
268+
return data
269+
270+
def _request(
271+
self, method: str, url: str, expected_status: int = 200, **kwargs
272+
) -> dict[str, Any]:
273+
"""Make an HTTP request with consistent error handling.
274+
275+
Args:
276+
method: HTTP method (GET, POST, PUT, etc.)
277+
url: The URL to request
278+
expected_status: The expected HTTP status code (default: 200)
279+
**kwargs: Additional arguments to pass to the request
280+
281+
Returns:
282+
The JSON response data
283+
"""
284+
try:
285+
response = self.session.request(method, url, follow_redirects=True, **kwargs)
286+
return self._handle_response(response, url, expected_status)
287+
except (httpx.RequestError, httpx.HTTPStatusError) as e:
288+
raise DatalabAPIError(f"Request failed for {url}: {e}")
289+
290+
def _get(self, url: str, **kwargs) -> dict[str, Any]:
291+
"""Make a GET request with error handling."""
292+
return self._request("GET", url, **kwargs)
293+
294+
def _post(self, url: str, expected_status: int = 200, **kwargs) -> dict[str, Any]:
295+
"""Make a POST request with error handling."""
296+
return self._request("POST", url, expected_status, **kwargs)
297+
298+
def _put(self, url: str, expected_status: int = 201, **kwargs) -> dict[str, Any]:
299+
"""Make a PUT request with error handling."""
300+
return self._request("PUT", url, expected_status, **kwargs)
301+
302+
def _patch(self, url: str, **kwargs) -> dict[str, Any]:
303+
"""Make a PATCH request with error handling."""
304+
return self._request("PATCH", url, **kwargs)
305+
306+
def _delete(self, url: str, expected_status: int = 200, **kwargs) -> dict[str, Any]:
307+
"""Make a DELETE request with error handling."""
308+
return self._request("DELETE", url, expected_status, **kwargs)

0 commit comments

Comments
 (0)