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.
Files changed (25) hide show
  1. {codeocean-0.7.0 → codeocean-0.9.0}/CHANGELOG.md +8 -0
  2. {codeocean-0.7.0 → codeocean-0.9.0}/PKG-INFO +1 -1
  3. {codeocean-0.7.0 → codeocean-0.9.0}/pyproject.toml +1 -1
  4. {codeocean-0.7.0 → codeocean-0.9.0}/src/codeocean/__init__.py +1 -0
  5. codeocean-0.9.0/src/codeocean/client.py +61 -0
  6. codeocean-0.9.0/src/codeocean/error.py +41 -0
  7. codeocean-0.9.0/tests/test_client.py +83 -0
  8. codeocean-0.9.0/tests/test_error.py +140 -0
  9. codeocean-0.7.0/src/codeocean/client.py +0 -32
  10. {codeocean-0.7.0 → codeocean-0.9.0}/.flake8 +0 -0
  11. {codeocean-0.7.0 → codeocean-0.9.0}/.gitignore +0 -0
  12. {codeocean-0.7.0 → codeocean-0.9.0}/LICENSE +0 -0
  13. {codeocean-0.7.0 → codeocean-0.9.0}/README.md +0 -0
  14. {codeocean-0.7.0 → codeocean-0.9.0}/RELEASE.md +0 -0
  15. {codeocean-0.7.0 → codeocean-0.9.0}/examples/create_data_asset.py +0 -0
  16. {codeocean-0.7.0 → codeocean-0.9.0}/examples/run_capsule.py +0 -0
  17. {codeocean-0.7.0 → codeocean-0.9.0}/examples/run_pipeline.py +0 -0
  18. {codeocean-0.7.0 → codeocean-0.9.0}/src/codeocean/capsule.py +0 -0
  19. {codeocean-0.7.0 → codeocean-0.9.0}/src/codeocean/components.py +0 -0
  20. {codeocean-0.7.0 → codeocean-0.9.0}/src/codeocean/computation.py +0 -0
  21. {codeocean-0.7.0 → codeocean-0.9.0}/src/codeocean/data_asset.py +0 -0
  22. {codeocean-0.7.0 → codeocean-0.9.0}/src/codeocean/enum.py +0 -0
  23. {codeocean-0.7.0 → codeocean-0.9.0}/src/codeocean/folder.py +0 -0
  24. {codeocean-0.7.0 → codeocean-0.9.0}/tests/__init__.py +0 -0
  25. {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
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codeocean
3
- Version: 0.7.0
3
+ Version: 0.9.0
4
4
  Summary: Code Ocean Python SDK
5
5
  Project-URL: Homepage, https://github.com/codeocean/codeocean-sdk-python
6
6
  Project-URL: Issues, https://github.com/codeocean/codeocean-sdk-python/issues
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "codeocean"
7
- version = "0.7.0"
7
+ version = "0.9.0"
8
8
  authors = [
9
9
  { name="Code Ocean", email="dev@codeocean.com" },
10
10
  ]
@@ -1 +1,2 @@
1
1
  from codeocean.client import CodeOcean # noqa: F401
2
+ from codeocean.error import Error # noqa: F401
@@ -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