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.
Files changed (24) hide show
  1. {codeocean-0.8.0 → codeocean-0.9.1}/CHANGELOG.md +6 -0
  2. {codeocean-0.8.0 → codeocean-0.9.1}/PKG-INFO +1 -1
  3. {codeocean-0.8.0 → codeocean-0.9.1}/pyproject.toml +1 -1
  4. {codeocean-0.8.0 → codeocean-0.9.1}/src/codeocean/__init__.py +1 -0
  5. {codeocean-0.8.0 → codeocean-0.9.1}/src/codeocean/capsule.py +32 -1
  6. {codeocean-0.8.0 → codeocean-0.9.1}/src/codeocean/client.py +9 -3
  7. {codeocean-0.8.0 → codeocean-0.9.1}/src/codeocean/data_asset.py +24 -2
  8. codeocean-0.9.1/src/codeocean/error.py +41 -0
  9. codeocean-0.9.1/tests/test_error.py +140 -0
  10. {codeocean-0.8.0 → codeocean-0.9.1}/.flake8 +0 -0
  11. {codeocean-0.8.0 → codeocean-0.9.1}/.gitignore +0 -0
  12. {codeocean-0.8.0 → codeocean-0.9.1}/LICENSE +0 -0
  13. {codeocean-0.8.0 → codeocean-0.9.1}/README.md +0 -0
  14. {codeocean-0.8.0 → codeocean-0.9.1}/RELEASE.md +0 -0
  15. {codeocean-0.8.0 → codeocean-0.9.1}/examples/create_data_asset.py +0 -0
  16. {codeocean-0.8.0 → codeocean-0.9.1}/examples/run_capsule.py +0 -0
  17. {codeocean-0.8.0 → codeocean-0.9.1}/examples/run_pipeline.py +0 -0
  18. {codeocean-0.8.0 → codeocean-0.9.1}/src/codeocean/components.py +0 -0
  19. {codeocean-0.8.0 → codeocean-0.9.1}/src/codeocean/computation.py +0 -0
  20. {codeocean-0.8.0 → codeocean-0.9.1}/src/codeocean/enum.py +0 -0
  21. {codeocean-0.8.0 → codeocean-0.9.1}/src/codeocean/folder.py +0 -0
  22. {codeocean-0.8.0 → codeocean-0.9.1}/tests/__init__.py +0 -0
  23. {codeocean-0.8.0 → codeocean-0.9.1}/tests/test_client.py +0 -0
  24. {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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codeocean
3
- Version: 0.8.0
3
+ Version: 0.9.1
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.8.0"
7
+ version = "0.9.1"
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
@@ -140,7 +140,38 @@ class CapsuleSearchParams:
140
140
  query: Optional[str] = dataclass_field(
141
141
  default=None,
142
142
  metadata={
143
- "description": "Search query in free text or structured format (name:... tag:...)"
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 query in free text or structured format "
542
- "(name:... tag:... run_script:... commit_id:...)",
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