Skip to content

Commit 3c7e64c

Browse files
feat: add error handling (#47)
1 parent ac915f9 commit 3c7e64c

File tree

4 files changed

+191
-3
lines changed

4 files changed

+191
-3
lines changed

src/codeocean/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
from codeocean.client import CodeOcean # noqa: F401
2+
from codeocean.error import Error # noqa: F401

src/codeocean/client.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@
55
from requests_toolbelt.sessions import BaseUrlSession
66
from typing import Optional
77
from urllib3.util import Retry
8+
import requests
89

910
from codeocean.capsule import Capsules
1011
from codeocean.computation import Computations
1112
from codeocean.data_asset import DataAssets
13+
from codeocean.error import Error
1214

1315

1416
@dataclass
@@ -45,11 +47,15 @@ def __post_init__(self):
4547
})
4648
if self.agent_id:
4749
self.session.headers.update({"Agent-Id": self.agent_id})
48-
self.session.hooks["response"] = [
49-
lambda response, *args, **kwargs: response.raise_for_status()
50-
]
50+
self.session.hooks["response"] = [self._error_handler]
5151
self.session.mount(self.domain, TCPKeepAliveAdapter(max_retries=self.retries))
5252

5353
self.capsules = Capsules(client=self.session)
5454
self.computations = Computations(client=self.session)
5555
self.data_assets = DataAssets(client=self.session)
56+
57+
def _error_handler(self, response, *args, **kwargs):
58+
try:
59+
response.raise_for_status()
60+
except requests.HTTPError as err:
61+
raise Error(err) from err

src/codeocean/error.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
from __future__ import annotations
2+
3+
import json
4+
import requests
5+
6+
7+
class Error(Exception):
8+
"""
9+
Represents an HTTP error with additional context extracted from the response.
10+
11+
Attributes:
12+
http_err (requests.HTTPError): The HTTP error object.
13+
status_code (int): The HTTP status code of the error response.
14+
message (str): A message describing the error, extracted from the response body.
15+
data (Any): If the response body is json, this attribute contains the json object; otherwise, it is None.
16+
17+
Args:
18+
err (requests.HTTPError): The HTTP error object.
19+
"""
20+
def __init__(self, err: requests.HTTPError):
21+
self.http_err = err
22+
self.status_code = err.response.status_code
23+
self.message = "An error occurred."
24+
self.data = None
25+
26+
try:
27+
self.data = err.response.json()
28+
if isinstance(self.data, dict):
29+
self.message = self.data.get("message", self.message)
30+
except Exception:
31+
# response wasn't JSON – fall back to text
32+
self.message = err.response.text
33+
34+
super().__init__(self.message)
35+
36+
def __str__(self) -> str:
37+
msg = str(self.http_err)
38+
msg += f"\n\nMessage: {self.message}"
39+
if self.data:
40+
msg += "\n\nData:\n" + json.dumps(self.data, indent=2)
41+
return msg

tests/test_error.py

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
import unittest
2+
from unittest.mock import Mock
3+
import requests
4+
5+
from codeocean.error import Error
6+
7+
8+
class TestError(unittest.TestCase):
9+
"""Test cases for the Error exception class."""
10+
11+
def test_error_is_exception_subclass(self):
12+
"""Test that Error is a subclass of Exception."""
13+
self.assertTrue(issubclass(Error, Exception))
14+
15+
def test_error_with_json_dict(self):
16+
"""Test Error creation with JSON dict response containing message."""
17+
# Create mock HTTPError and response
18+
mock_response = Mock()
19+
mock_response.status_code = 400
20+
mock_response.json.return_value = {"message": "Custom error message", "datasets": [{"id": "123", "name": "tv"}]}
21+
22+
mock_http_error = Mock(spec=requests.HTTPError)
23+
mock_http_error.response = mock_response
24+
25+
# Create Error instance
26+
error = Error(mock_http_error)
27+
28+
# Verify attributes
29+
self.assertEqual(error.status_code, 400)
30+
self.assertEqual(error.message, "Custom error message")
31+
self.assertEqual(error.data, {"message": "Custom error message", "datasets": [{"id": "123", "name": "tv"}]})
32+
33+
def test_error_with_json_dict_no_message(self):
34+
"""Test Error creation with JSON dict response without message field."""
35+
# Create mock HTTPError and response
36+
mock_response = Mock()
37+
mock_response.status_code = 500
38+
mock_response.json.return_value = {"error": "some other field"}
39+
40+
mock_http_error = Mock(spec=requests.HTTPError)
41+
mock_http_error.response = mock_response
42+
43+
# Create Error instance
44+
error = Error(mock_http_error)
45+
46+
# Verify attributes
47+
self.assertEqual(error.status_code, 500)
48+
self.assertEqual(error.message, "An error occurred.")
49+
self.assertEqual(error.data, {"error": "some other field"})
50+
51+
def test_error_with_json_list(self):
52+
"""Test Error creation with JSON list response."""
53+
# Create mock HTTPError and response
54+
mock_response = Mock()
55+
mock_response.status_code = 403
56+
mock_response.json.return_value = [{"field": "error1"}, {"field": "error2"}]
57+
58+
mock_http_error = Mock(spec=requests.HTTPError)
59+
mock_http_error.response = mock_response
60+
61+
# Create Error instance
62+
error = Error(mock_http_error)
63+
64+
# Verify attributes
65+
self.assertEqual(error.status_code, 403)
66+
self.assertEqual(error.message, "An error occurred.")
67+
self.assertEqual(error.data, [{"field": "error1"}, {"field": "error2"}])
68+
69+
def test_error_with_non_json_response(self):
70+
"""Test Error creation when response is not JSON."""
71+
# Create mock HTTPError and response
72+
mock_response = Mock()
73+
mock_response.status_code = 404
74+
mock_response.json.side_effect = Exception("Not JSON")
75+
mock_response.text = "Page not found"
76+
77+
mock_http_error = Mock(spec=requests.HTTPError)
78+
mock_http_error.response = mock_response
79+
80+
# Create Error instance
81+
error = Error(mock_http_error)
82+
83+
# Verify attributes
84+
self.assertEqual(error.status_code, 404)
85+
self.assertEqual(error.message, "Page not found")
86+
self.assertIsNone(error.data)
87+
88+
def test_error_str_method_with_data(self):
89+
"""Test Error __str__ method when data is present."""
90+
# Create mock HTTPError and response
91+
mock_response = Mock()
92+
mock_response.status_code = 400
93+
mock_response.json.return_value = {"message": "Validation failed", "errors": ["field1", "field2"]}
94+
95+
mock_http_error = Mock(spec=requests.HTTPError)
96+
mock_http_error.response = mock_response
97+
mock_http_error.__str__ = Mock(return_value="400 Client Error: Bad Request for url: http://example.com")
98+
99+
# Create Error instance
100+
error = Error(mock_http_error)
101+
102+
# Test __str__ method
103+
error_str = str(error)
104+
105+
# Verify the string contains expected components
106+
self.assertEqual(error_str, """400 Client Error: Bad Request for url: http://example.com
107+
108+
Message: Validation failed
109+
110+
Data:
111+
{
112+
"message": "Validation failed",
113+
"errors": [
114+
"field1",
115+
"field2"
116+
]
117+
}""")
118+
119+
def test_error_str_method_without_data(self):
120+
"""Test Error __str__ method when data is None."""
121+
# Create mock HTTPError and response
122+
mock_response = Mock()
123+
mock_response.status_code = 404
124+
mock_response.json.side_effect = Exception("Not JSON")
125+
mock_response.text = "Page not found"
126+
127+
mock_http_error = Mock(spec=requests.HTTPError)
128+
mock_http_error.response = mock_response
129+
mock_http_error.__str__ = Mock(return_value="404 Client Error: Not Found for url: http://example.com")
130+
131+
# Create Error instance
132+
error = Error(mock_http_error)
133+
134+
# Test __str__ method
135+
error_str = str(error)
136+
137+
# Verify the string contains expected components
138+
self.assertEqual(error_str, """404 Client Error: Not Found for url: http://example.com
139+
140+
Message: Page not found""")

0 commit comments

Comments
 (0)