codeocean 0.8.0__tar.gz → 0.9.1__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.8.0 → codeocean-0.9.1}/CHANGELOG.md +6 -0
- {codeocean-0.8.0 → codeocean-0.9.1}/PKG-INFO +1 -1
- {codeocean-0.8.0 → codeocean-0.9.1}/pyproject.toml +1 -1
- {codeocean-0.8.0 → codeocean-0.9.1}/src/codeocean/__init__.py +1 -0
- {codeocean-0.8.0 → codeocean-0.9.1}/src/codeocean/capsule.py +32 -1
- {codeocean-0.8.0 → codeocean-0.9.1}/src/codeocean/client.py +9 -3
- {codeocean-0.8.0 → codeocean-0.9.1}/src/codeocean/data_asset.py +24 -2
- codeocean-0.9.1/src/codeocean/error.py +41 -0
- codeocean-0.9.1/tests/test_error.py +140 -0
- {codeocean-0.8.0 → codeocean-0.9.1}/.flake8 +0 -0
- {codeocean-0.8.0 → codeocean-0.9.1}/.gitignore +0 -0
- {codeocean-0.8.0 → codeocean-0.9.1}/LICENSE +0 -0
- {codeocean-0.8.0 → codeocean-0.9.1}/README.md +0 -0
- {codeocean-0.8.0 → codeocean-0.9.1}/RELEASE.md +0 -0
- {codeocean-0.8.0 → codeocean-0.9.1}/examples/create_data_asset.py +0 -0
- {codeocean-0.8.0 → codeocean-0.9.1}/examples/run_capsule.py +0 -0
- {codeocean-0.8.0 → codeocean-0.9.1}/examples/run_pipeline.py +0 -0
- {codeocean-0.8.0 → codeocean-0.9.1}/src/codeocean/components.py +0 -0
- {codeocean-0.8.0 → codeocean-0.9.1}/src/codeocean/computation.py +0 -0
- {codeocean-0.8.0 → codeocean-0.9.1}/src/codeocean/enum.py +0 -0
- {codeocean-0.8.0 → codeocean-0.9.1}/src/codeocean/folder.py +0 -0
- {codeocean-0.8.0 → codeocean-0.9.1}/tests/__init__.py +0 -0
- {codeocean-0.8.0 → codeocean-0.9.1}/tests/test_client.py +0 -0
- {codeocean-0.8.0 → codeocean-0.9.1}/tests/test_package.py +0 -0
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
CHANGELOG
|
|
2
2
|
=========
|
|
3
3
|
|
|
4
|
+
## 0.9.1 (2025-08-29)
|
|
5
|
+
- [#56](https://github.com/codeocean/codeocean-sdk-python/pull/56) fix: update query description in search APIs
|
|
6
|
+
|
|
7
|
+
## 0.9.0 (2025-08-05)
|
|
8
|
+
- [#47](https://github.com/codeocean/codeocean-sdk-python/pull/47) feat: add error handling
|
|
9
|
+
|
|
4
10
|
## 0.8.0 (2025-07-11)
|
|
5
11
|
- [#50](https://github.com/codeocean/codeocean-sdk-python/pull/50) feat: Add Min-Server-Version header support
|
|
6
12
|
- [#49](https://github.com/codeocean/codeocean-sdk-python/pull/49) feat: Identify AI agents in API requests
|
|
@@ -140,7 +140,38 @@ class CapsuleSearchParams:
|
|
|
140
140
|
query: Optional[str] = dataclass_field(
|
|
141
141
|
default=None,
|
|
142
142
|
metadata={
|
|
143
|
-
"description": "Search
|
|
143
|
+
"description": """Search expression supporting free text and field:value filters.
|
|
144
|
+
Valid fields:
|
|
145
|
+
- id
|
|
146
|
+
- name
|
|
147
|
+
- doi
|
|
148
|
+
- tag
|
|
149
|
+
- field
|
|
150
|
+
- affiliation
|
|
151
|
+
- journal
|
|
152
|
+
- article
|
|
153
|
+
- author
|
|
154
|
+
|
|
155
|
+
Free text:
|
|
156
|
+
- Matches across weighted fields (name, tags, description, authors, etc.)
|
|
157
|
+
|
|
158
|
+
Syntax rules:
|
|
159
|
+
- Same field repeated = OR
|
|
160
|
+
- Different fields = AND
|
|
161
|
+
- Quotes = exact phrase
|
|
162
|
+
- No explicit OR operator
|
|
163
|
+
- No wildcards (*)
|
|
164
|
+
- Not case sensitive
|
|
165
|
+
|
|
166
|
+
Notes:
|
|
167
|
+
- "description" is not directly searchable; it is covered by free-text matching.
|
|
168
|
+
|
|
169
|
+
Examples:
|
|
170
|
+
- name:RNA-seq tag:genomics
|
|
171
|
+
- name:"single cell analysis"
|
|
172
|
+
- Synergy
|
|
173
|
+
- name:Synergy
|
|
174
|
+
"""
|
|
144
175
|
},
|
|
145
176
|
)
|
|
146
177
|
next_token: Optional[str] = dataclass_field(
|
|
@@ -5,10 +5,12 @@ from requests_toolbelt.adapters.socket_options import TCPKeepAliveAdapter
|
|
|
5
5
|
from requests_toolbelt.sessions import BaseUrlSession
|
|
6
6
|
from typing import Optional
|
|
7
7
|
from urllib3.util import Retry
|
|
8
|
+
import requests
|
|
8
9
|
|
|
9
10
|
from codeocean.capsule import Capsules
|
|
10
11
|
from codeocean.computation import Computations
|
|
11
12
|
from codeocean.data_asset import DataAssets
|
|
13
|
+
from codeocean.error import Error
|
|
12
14
|
|
|
13
15
|
|
|
14
16
|
@dataclass
|
|
@@ -45,11 +47,15 @@ class CodeOcean:
|
|
|
45
47
|
})
|
|
46
48
|
if self.agent_id:
|
|
47
49
|
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]
|
|
51
51
|
self.session.mount(self.domain, TCPKeepAliveAdapter(max_retries=self.retries))
|
|
52
52
|
|
|
53
53
|
self.capsules = Capsules(client=self.session)
|
|
54
54
|
self.computations = Computations(client=self.session)
|
|
55
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
|
|
@@ -538,8 +538,30 @@ class DataAssetSearchParams:
|
|
|
538
538
|
query: Optional[str] = field(
|
|
539
539
|
default=None,
|
|
540
540
|
metadata={
|
|
541
|
-
"description": "Search
|
|
542
|
-
|
|
541
|
+
"description": """Search expression supporting free text and field:value filters.
|
|
542
|
+
Valid fields:
|
|
543
|
+
- name
|
|
544
|
+
- tag
|
|
545
|
+
- run_script
|
|
546
|
+
- commit_id
|
|
547
|
+
- contained_data_id
|
|
548
|
+
|
|
549
|
+
Free text:
|
|
550
|
+
- Matches across weighted fields (name, tags, description, custom metadata)
|
|
551
|
+
|
|
552
|
+
Syntax rules:
|
|
553
|
+
- Same field repeated = OR
|
|
554
|
+
- Different fields = AND
|
|
555
|
+
- Quotes = exact phrase
|
|
556
|
+
- No explicit OR operator
|
|
557
|
+
- No wildcards (*)
|
|
558
|
+
- Not case sensitive
|
|
559
|
+
|
|
560
|
+
Examples:
|
|
561
|
+
- name:RNA-seq tag:genomics
|
|
562
|
+
- name:"analysis pipeline"
|
|
563
|
+
- name:Synergy
|
|
564
|
+
"""
|
|
543
565
|
},
|
|
544
566
|
)
|
|
545
567
|
next_token: Optional[str] = field(
|
|
@@ -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,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""")
|
|
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
|