Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/codeocean/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
from codeocean.client import CodeOcean # noqa: F401
from codeocean.error import Error # noqa: F401
12 changes: 9 additions & 3 deletions src/codeocean/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@
from requests_toolbelt.sessions import BaseUrlSession
from typing import Optional
from urllib3.util import Retry
import requests

from codeocean.capsule import Capsules
from codeocean.computation import Computations
from codeocean.data_asset import DataAssets
from codeocean.error import Error


@dataclass
Expand Down Expand Up @@ -45,11 +47,15 @@ def __post_init__(self):
})
if self.agent_id:
self.session.headers.update({"Agent-Id": self.agent_id})
self.session.hooks["response"] = [
lambda response, *args, **kwargs: response.raise_for_status()
]
self.session.hooks["response"] = [self._error_handler]
self.session.mount(self.domain, TCPKeepAliveAdapter(max_retries=self.retries))

self.capsules = Capsules(client=self.session)
self.computations = Computations(client=self.session)
self.data_assets = DataAssets(client=self.session)

def _error_handler(self, response, *args, **kwargs):
try:
response.raise_for_status()
except requests.HTTPError as err:
raise Error(err) from err
41 changes: 41 additions & 0 deletions src/codeocean/error.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
from __future__ import annotations

import json
import requests


class Error(Exception):
"""
Represents an HTTP error with additional context extracted from the response.

Attributes:
http_err (requests.HTTPError): The HTTP error object.
status_code (int): The HTTP status code of the error response.
message (str): A message describing the error, extracted from the response body.
data (Any): If the response body is json, this attribute contains the json object; otherwise, it is None.

Args:
err (requests.HTTPError): The HTTP error object.
"""
def __init__(self, err: requests.HTTPError):
self.http_err = err
self.status_code = err.response.status_code
self.message = "An error occurred."
self.data = None

try:
self.data = err.response.json()
if isinstance(self.data, dict):
self.message = self.data.get("message", self.message)
except Exception:
# response wasn't JSON – fall back to text
self.message = err.response.text

super().__init__(self.message)

def __str__(self) -> str:
msg = str(self.http_err)
msg += f"\n\nMessage: {self.message}"
if self.data:
msg += "\n\nData:\n" + json.dumps(self.data, indent=2)
return msg
140 changes: 140 additions & 0 deletions tests/test_error.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import unittest
from unittest.mock import Mock
import requests

from codeocean.error import Error


class TestError(unittest.TestCase):
"""Test cases for the Error exception class."""

def test_error_is_exception_subclass(self):
"""Test that Error is a subclass of Exception."""
self.assertTrue(issubclass(Error, Exception))

def test_error_with_json_dict(self):
"""Test Error creation with JSON dict response containing message."""
# Create mock HTTPError and response
mock_response = Mock()
mock_response.status_code = 400
mock_response.json.return_value = {"message": "Custom error message", "datasets": [{"id": "123", "name": "tv"}]}

mock_http_error = Mock(spec=requests.HTTPError)
mock_http_error.response = mock_response

# Create Error instance
error = Error(mock_http_error)

# Verify attributes
self.assertEqual(error.status_code, 400)
self.assertEqual(error.message, "Custom error message")
self.assertEqual(error.data, {"message": "Custom error message", "datasets": [{"id": "123", "name": "tv"}]})

def test_error_with_json_dict_no_message(self):
"""Test Error creation with JSON dict response without message field."""
# Create mock HTTPError and response
mock_response = Mock()
mock_response.status_code = 500
mock_response.json.return_value = {"error": "some other field"}

mock_http_error = Mock(spec=requests.HTTPError)
mock_http_error.response = mock_response

# Create Error instance
error = Error(mock_http_error)

# Verify attributes
self.assertEqual(error.status_code, 500)
self.assertEqual(error.message, "An error occurred.")
self.assertEqual(error.data, {"error": "some other field"})

def test_error_with_json_list(self):
"""Test Error creation with JSON list response."""
# Create mock HTTPError and response
mock_response = Mock()
mock_response.status_code = 403
mock_response.json.return_value = [{"field": "error1"}, {"field": "error2"}]

mock_http_error = Mock(spec=requests.HTTPError)
mock_http_error.response = mock_response

# Create Error instance
error = Error(mock_http_error)

# Verify attributes
self.assertEqual(error.status_code, 403)
self.assertEqual(error.message, "An error occurred.")
self.assertEqual(error.data, [{"field": "error1"}, {"field": "error2"}])

def test_error_with_non_json_response(self):
"""Test Error creation when response is not JSON."""
# Create mock HTTPError and response
mock_response = Mock()
mock_response.status_code = 404
mock_response.json.side_effect = Exception("Not JSON")
mock_response.text = "Page not found"

mock_http_error = Mock(spec=requests.HTTPError)
mock_http_error.response = mock_response

# Create Error instance
error = Error(mock_http_error)

# Verify attributes
self.assertEqual(error.status_code, 404)
self.assertEqual(error.message, "Page not found")
self.assertIsNone(error.data)

def test_error_str_method_with_data(self):
"""Test Error __str__ method when data is present."""
# Create mock HTTPError and response
mock_response = Mock()
mock_response.status_code = 400
mock_response.json.return_value = {"message": "Validation failed", "errors": ["field1", "field2"]}

mock_http_error = Mock(spec=requests.HTTPError)
mock_http_error.response = mock_response
mock_http_error.__str__ = Mock(return_value="400 Client Error: Bad Request for url: http://example.com")

# Create Error instance
error = Error(mock_http_error)

# Test __str__ method
error_str = str(error)

# Verify the string contains expected components
self.assertEqual(error_str, """400 Client Error: Bad Request for url: http://example.com

Message: Validation failed

Data:
{
"message": "Validation failed",
"errors": [
"field1",
"field2"
]
}""")

def test_error_str_method_without_data(self):
"""Test Error __str__ method when data is None."""
# Create mock HTTPError and response
mock_response = Mock()
mock_response.status_code = 404
mock_response.json.side_effect = Exception("Not JSON")
mock_response.text = "Page not found"

mock_http_error = Mock(spec=requests.HTTPError)
mock_http_error.response = mock_response
mock_http_error.__str__ = Mock(return_value="404 Client Error: Not Found for url: http://example.com")

# Create Error instance
error = Error(mock_http_error)

# Test __str__ method
error_str = str(error)

# Verify the string contains expected components
self.assertEqual(error_str, """404 Client Error: Not Found for url: http://example.com

Message: Page not found""")