codeocean 0.7.0__tar.gz → 0.9.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {codeocean-0.7.0 → codeocean-0.9.0}/CHANGELOG.md +8 -0
- {codeocean-0.7.0 → codeocean-0.9.0}/PKG-INFO +1 -1
- {codeocean-0.7.0 → codeocean-0.9.0}/pyproject.toml +1 -1
- {codeocean-0.7.0 → codeocean-0.9.0}/src/codeocean/__init__.py +1 -0
- codeocean-0.9.0/src/codeocean/client.py +61 -0
- codeocean-0.9.0/src/codeocean/error.py +41 -0
- codeocean-0.9.0/tests/test_client.py +83 -0
- codeocean-0.9.0/tests/test_error.py +140 -0
- codeocean-0.7.0/src/codeocean/client.py +0 -32
- {codeocean-0.7.0 → codeocean-0.9.0}/.flake8 +0 -0
- {codeocean-0.7.0 → codeocean-0.9.0}/.gitignore +0 -0
- {codeocean-0.7.0 → codeocean-0.9.0}/LICENSE +0 -0
- {codeocean-0.7.0 → codeocean-0.9.0}/README.md +0 -0
- {codeocean-0.7.0 → codeocean-0.9.0}/RELEASE.md +0 -0
- {codeocean-0.7.0 → codeocean-0.9.0}/examples/create_data_asset.py +0 -0
- {codeocean-0.7.0 → codeocean-0.9.0}/examples/run_capsule.py +0 -0
- {codeocean-0.7.0 → codeocean-0.9.0}/examples/run_pipeline.py +0 -0
- {codeocean-0.7.0 → codeocean-0.9.0}/src/codeocean/capsule.py +0 -0
- {codeocean-0.7.0 → codeocean-0.9.0}/src/codeocean/components.py +0 -0
- {codeocean-0.7.0 → codeocean-0.9.0}/src/codeocean/computation.py +0 -0
- {codeocean-0.7.0 → codeocean-0.9.0}/src/codeocean/data_asset.py +0 -0
- {codeocean-0.7.0 → codeocean-0.9.0}/src/codeocean/enum.py +0 -0
- {codeocean-0.7.0 → codeocean-0.9.0}/src/codeocean/folder.py +0 -0
- {codeocean-0.7.0 → codeocean-0.9.0}/tests/__init__.py +0 -0
- {codeocean-0.7.0 → codeocean-0.9.0}/tests/test_package.py +0 -0
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
CHANGELOG
|
|
2
2
|
=========
|
|
3
3
|
|
|
4
|
+
## 0.9.0 (2025-08-05)
|
|
5
|
+
- [#47](https://github.com/codeocean/codeocean-sdk-python/pull/47) feat: add error handling
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
## 0.8.0 (2025-07-11)
|
|
9
|
+
- [#50](https://github.com/codeocean/codeocean-sdk-python/pull/50) feat: Add Min-Server-Version header support
|
|
10
|
+
- [#49](https://github.com/codeocean/codeocean-sdk-python/pull/49) feat: Identify AI agents in API requests
|
|
11
|
+
|
|
4
12
|
## 0.7.0 (2025-06-24)
|
|
5
13
|
- [#46](https://github.com/codeocean/codeocean-sdk-python/pull/46) feat: Add API documentation
|
|
6
14
|
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from requests_toolbelt.adapters.socket_options import TCPKeepAliveAdapter
|
|
5
|
+
from requests_toolbelt.sessions import BaseUrlSession
|
|
6
|
+
from typing import Optional
|
|
7
|
+
from urllib3.util import Retry
|
|
8
|
+
import requests
|
|
9
|
+
|
|
10
|
+
from codeocean.capsule import Capsules
|
|
11
|
+
from codeocean.computation import Computations
|
|
12
|
+
from codeocean.data_asset import DataAssets
|
|
13
|
+
from codeocean.error import Error
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class CodeOcean:
|
|
18
|
+
"""
|
|
19
|
+
Code Ocean API client.
|
|
20
|
+
|
|
21
|
+
This class provides a unified interface to access Code Ocean's API endpoints
|
|
22
|
+
for managing capsules, pipelines, computations, and data assets.
|
|
23
|
+
|
|
24
|
+
Fields:
|
|
25
|
+
domain: The Code Ocean domain URL (e.g., 'https://codeocean.acme.com')
|
|
26
|
+
token: Code Ocean API access token
|
|
27
|
+
retries: Optional retry configuration for failed HTTP requests. Can be an integer
|
|
28
|
+
(number of retries) or a urllib3.util.Retry object for advanced
|
|
29
|
+
retry configuration. Defaults to 0 (no retries)
|
|
30
|
+
agent_id: Optional agent identifier for tracking AI agent API usage on behalf of users
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
domain: str
|
|
34
|
+
token: str
|
|
35
|
+
retries: Optional[Retry | int] = 0
|
|
36
|
+
agent_id: Optional[str] = None
|
|
37
|
+
|
|
38
|
+
# Minimum server version required by this SDK
|
|
39
|
+
MIN_SERVER_VERSION = "3.6.0"
|
|
40
|
+
|
|
41
|
+
def __post_init__(self):
|
|
42
|
+
self.session = BaseUrlSession(base_url=f"{self.domain}/api/v1/")
|
|
43
|
+
self.session.auth = (self.token, "")
|
|
44
|
+
self.session.headers.update({
|
|
45
|
+
"Content-Type": "application/json",
|
|
46
|
+
"Min-Server-Version": CodeOcean.MIN_SERVER_VERSION,
|
|
47
|
+
})
|
|
48
|
+
if self.agent_id:
|
|
49
|
+
self.session.headers.update({"Agent-Id": self.agent_id})
|
|
50
|
+
self.session.hooks["response"] = [self._error_handler]
|
|
51
|
+
self.session.mount(self.domain, TCPKeepAliveAdapter(max_retries=self.retries))
|
|
52
|
+
|
|
53
|
+
self.capsules = Capsules(client=self.session)
|
|
54
|
+
self.computations = Computations(client=self.session)
|
|
55
|
+
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
|
|
@@ -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
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
from unittest.mock import patch
|
|
3
|
+
from urllib3.util import Retry
|
|
4
|
+
|
|
5
|
+
from codeocean.client import CodeOcean
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TestClient(unittest.TestCase):
|
|
9
|
+
"""Test cases for the CodeOcean client class."""
|
|
10
|
+
|
|
11
|
+
def setUp(self):
|
|
12
|
+
"""Set up test fixtures."""
|
|
13
|
+
self.test_domain = "https://codeocean.acme.com"
|
|
14
|
+
self.test_token = "test_token_123"
|
|
15
|
+
self.test_agent_id = "test_agent_456"
|
|
16
|
+
|
|
17
|
+
def test_basic_init(self):
|
|
18
|
+
"""Test a basic client initialization."""
|
|
19
|
+
client = CodeOcean(
|
|
20
|
+
domain=self.test_domain,
|
|
21
|
+
token=self.test_token,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
# Verify base URL is set correctly
|
|
25
|
+
self.assertEqual(client.session.base_url, f"{self.test_domain}/api/v1/")
|
|
26
|
+
|
|
27
|
+
# Verify auth is correctly set
|
|
28
|
+
self.assertEqual(client.session.auth, (self.test_token, ""))
|
|
29
|
+
|
|
30
|
+
# Verify the session headers are correctly configured
|
|
31
|
+
headers = client.session.headers
|
|
32
|
+
self.assertIn("Content-Type", headers)
|
|
33
|
+
self.assertEqual(headers["Content-Type"], "application/json")
|
|
34
|
+
self.assertIn("Min-Server-Version", headers)
|
|
35
|
+
self.assertEqual(headers["Min-Server-Version"], CodeOcean.MIN_SERVER_VERSION)
|
|
36
|
+
|
|
37
|
+
@patch("codeocean.client.TCPKeepAliveAdapter")
|
|
38
|
+
def test_retry_configuration_types(self, mock_adapter):
|
|
39
|
+
"""Test that both integer and Retry object work for retries parameter."""
|
|
40
|
+
# Test with integer
|
|
41
|
+
CodeOcean(
|
|
42
|
+
domain=self.test_domain,
|
|
43
|
+
token=self.test_token,
|
|
44
|
+
retries=5,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
# Test with Retry object
|
|
48
|
+
retry_obj = Retry(total=3, backoff_factor=0.3)
|
|
49
|
+
CodeOcean(
|
|
50
|
+
domain=self.test_domain,
|
|
51
|
+
token=self.test_token,
|
|
52
|
+
retries=retry_obj,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
# Assert both configurations work
|
|
56
|
+
self.assertEqual(mock_adapter.call_count, 2)
|
|
57
|
+
mock_adapter.assert_any_call(max_retries=5)
|
|
58
|
+
mock_adapter.assert_any_call(max_retries=retry_obj)
|
|
59
|
+
|
|
60
|
+
def test_agent_id_header_set_when_provided(self):
|
|
61
|
+
"""Test that Agent-Id header is set when agent_id is provided."""
|
|
62
|
+
client = CodeOcean(
|
|
63
|
+
domain=self.test_domain,
|
|
64
|
+
token=self.test_token,
|
|
65
|
+
agent_id=self.test_agent_id,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
# Verify the session headers are correctly configured
|
|
69
|
+
headers = client.session.headers
|
|
70
|
+
self.assertIn("Agent-Id", headers)
|
|
71
|
+
self.assertEqual(headers["Agent-Id"], self.test_agent_id)
|
|
72
|
+
|
|
73
|
+
def test_agent_id_header_not_set_when_none(self):
|
|
74
|
+
"""Test that Agent-Id header is not set when agent_id is None."""
|
|
75
|
+
client = CodeOcean(
|
|
76
|
+
domain=self.test_domain,
|
|
77
|
+
token=self.test_token,
|
|
78
|
+
agent_id=None,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
# Verify the session headers are correctly configured
|
|
82
|
+
headers = client.session.headers
|
|
83
|
+
self.assertNotIn("Agent-Id", headers)
|
|
@@ -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""")
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from dataclasses import dataclass
|
|
4
|
-
from requests_toolbelt.adapters.socket_options import TCPKeepAliveAdapter
|
|
5
|
-
from requests_toolbelt.sessions import BaseUrlSession
|
|
6
|
-
from typing import Optional
|
|
7
|
-
from urllib3.util import Retry
|
|
8
|
-
|
|
9
|
-
from codeocean.capsule import Capsules
|
|
10
|
-
from codeocean.computation import Computations
|
|
11
|
-
from codeocean.data_asset import DataAssets
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
@dataclass
|
|
15
|
-
class CodeOcean:
|
|
16
|
-
|
|
17
|
-
domain: str
|
|
18
|
-
token: str
|
|
19
|
-
retries: Optional[Retry | int] = 0
|
|
20
|
-
|
|
21
|
-
def __post_init__(self):
|
|
22
|
-
self.session = BaseUrlSession(base_url=f"{self.domain}/api/v1/")
|
|
23
|
-
self.session.auth = (self.token, "")
|
|
24
|
-
self.session.headers.update({"Content-Type": "application/json"})
|
|
25
|
-
self.session.hooks["response"] = [
|
|
26
|
-
lambda response, *args, **kwargs: response.raise_for_status()
|
|
27
|
-
]
|
|
28
|
-
self.session.mount(self.domain, TCPKeepAliveAdapter(max_retries=self.retries))
|
|
29
|
-
|
|
30
|
-
self.capsules = Capsules(client=self.session)
|
|
31
|
-
self.computations = Computations(client=self.session)
|
|
32
|
-
self.data_assets = DataAssets(client=self.session)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|