codeocean 0.6.0__tar.gz → 0.8.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 (28) hide show
  1. {codeocean-0.6.0 → codeocean-0.8.0}/CHANGELOG.md +7 -0
  2. {codeocean-0.6.0 → codeocean-0.8.0}/PKG-INFO +1 -1
  3. {codeocean-0.6.0 → codeocean-0.8.0}/pyproject.toml +1 -1
  4. codeocean-0.8.0/src/codeocean/capsule.py +281 -0
  5. codeocean-0.8.0/src/codeocean/client.py +55 -0
  6. codeocean-0.8.0/src/codeocean/components.py +148 -0
  7. codeocean-0.8.0/src/codeocean/computation.py +349 -0
  8. codeocean-0.8.0/src/codeocean/data_asset.py +849 -0
  9. codeocean-0.8.0/src/codeocean/folder.py +55 -0
  10. codeocean-0.8.0/tests/test_client.py +83 -0
  11. codeocean-0.6.0/src/codeocean/capsule.py +0 -136
  12. codeocean-0.6.0/src/codeocean/client.py +0 -32
  13. codeocean-0.6.0/src/codeocean/components.py +0 -77
  14. codeocean-0.6.0/src/codeocean/computation.py +0 -180
  15. codeocean-0.6.0/src/codeocean/data_asset.py +0 -351
  16. codeocean-0.6.0/src/codeocean/folder.py +0 -32
  17. {codeocean-0.6.0 → codeocean-0.8.0}/.flake8 +0 -0
  18. {codeocean-0.6.0 → codeocean-0.8.0}/.gitignore +0 -0
  19. {codeocean-0.6.0 → codeocean-0.8.0}/LICENSE +0 -0
  20. {codeocean-0.6.0 → codeocean-0.8.0}/README.md +0 -0
  21. {codeocean-0.6.0 → codeocean-0.8.0}/RELEASE.md +0 -0
  22. {codeocean-0.6.0 → codeocean-0.8.0}/examples/create_data_asset.py +0 -0
  23. {codeocean-0.6.0 → codeocean-0.8.0}/examples/run_capsule.py +0 -0
  24. {codeocean-0.6.0 → codeocean-0.8.0}/examples/run_pipeline.py +0 -0
  25. {codeocean-0.6.0 → codeocean-0.8.0}/src/codeocean/__init__.py +0 -0
  26. {codeocean-0.6.0 → codeocean-0.8.0}/src/codeocean/enum.py +0 -0
  27. {codeocean-0.6.0 → codeocean-0.8.0}/tests/__init__.py +0 -0
  28. {codeocean-0.6.0 → codeocean-0.8.0}/tests/test_package.py +0 -0
@@ -1,6 +1,13 @@
1
1
  CHANGELOG
2
2
  =========
3
3
 
4
+ ## 0.8.0 (2025-07-11)
5
+ - [#50](https://github.com/codeocean/codeocean-sdk-python/pull/50) feat: Add Min-Server-Version header support
6
+ - [#49](https://github.com/codeocean/codeocean-sdk-python/pull/49) feat: Identify AI agents in API requests
7
+
8
+ ## 0.7.0 (2025-06-24)
9
+ - [#46](https://github.com/codeocean/codeocean-sdk-python/pull/46) feat: Add API documentation
10
+
4
11
  ## 0.6.0 (2025-06-03)
5
12
  - [#44](https://github.com/codeocean/codeocean-sdk-python/pull/44) feat: support for nextflow_profiles in data assets and computations
6
13
  - [#43](https://github.com/codeocean/codeocean-sdk-python/pull/43) fix: updates .gitignore to exclude .env
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codeocean
3
- Version: 0.6.0
3
+ Version: 0.8.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.6.0"
7
+ version = "0.8.0"
8
8
  authors = [
9
9
  { name="Code Ocean", email="dev@codeocean.com" },
10
10
  ]
@@ -0,0 +1,281 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field as dataclass_field
4
+ from dataclasses_json import dataclass_json
5
+ from typing import Optional, Iterator
6
+ from requests_toolbelt.sessions import BaseUrlSession
7
+
8
+ from codeocean.components import Ownership, SortOrder, SearchFilter, Permissions
9
+ from codeocean.computation import Computation
10
+ from codeocean.data_asset import DataAssetAttachParams, DataAssetAttachResults
11
+ from codeocean.enum import StrEnum
12
+
13
+
14
+ class CapsuleStatus(StrEnum):
15
+ """Status of a capsule indicating its release state."""
16
+
17
+ NonRelease = "non_release"
18
+ Release = "release"
19
+
20
+
21
+ class CapsuleSortBy(StrEnum):
22
+ """Fields available for sorting capsule search results."""
23
+
24
+ Created = "created"
25
+ LastAccessed = "last_accessed"
26
+ Name = "name"
27
+
28
+
29
+ @dataclass_json
30
+ @dataclass(frozen=True)
31
+ class OriginalCapsuleInfo:
32
+ """Information about the original capsule when this capsule is duplicated from
33
+ another."""
34
+
35
+ id: Optional[str] = dataclass_field(
36
+ default=None,
37
+ metadata={"description": "Original capsule ID"},
38
+ )
39
+ major_version: Optional[int] = dataclass_field(
40
+ default=None,
41
+ metadata={"description": "Original capsule major version"},
42
+ )
43
+ minor_version: Optional[int] = dataclass_field(
44
+ default=None,
45
+ metadata={"description": "Original capsule minor version"},
46
+ )
47
+ name: Optional[str] = dataclass_field(
48
+ default=None,
49
+ metadata={"description": "Original capsule name"},
50
+ )
51
+ created: Optional[int] = dataclass_field(
52
+ default=None,
53
+ metadata={"description": "Original capsule creation time (int64 timestamp)"},
54
+ )
55
+ public: Optional[bool] = dataclass_field(
56
+ default=None,
57
+ metadata={"description": "Indicates whether the original capsule is public"},
58
+ )
59
+
60
+
61
+ @dataclass_json
62
+ @dataclass(frozen=True)
63
+ class Capsule:
64
+ """Represents a Code Ocean capsule with its metadata and properties."""
65
+
66
+ id: str = dataclass_field(
67
+ metadata={"description": "Capsule ID"},
68
+ )
69
+ created: int = dataclass_field(
70
+ metadata={"description": "Capsule creation time (int64 timestamp)"},
71
+ )
72
+ name: str = dataclass_field(
73
+ metadata={"description": "Capsule display name"},
74
+ )
75
+ status: CapsuleStatus = dataclass_field(
76
+ metadata={"description": "Status of the capsule (non_release or release)"},
77
+ )
78
+ owner: str = dataclass_field(
79
+ metadata={"description": "Capsule owner's ID"},
80
+ )
81
+ slug: str = dataclass_field(
82
+ metadata={"description": "Alternate capsule ID (URL-friendly identifier)"},
83
+ )
84
+ article: Optional[dict] = dataclass_field(
85
+ default=None,
86
+ metadata={
87
+ "description": "Capsule article info with URL, ID, DOI, "
88
+ "citation, state, name, journal_name, and "
89
+ "publish_time"
90
+ },
91
+ )
92
+ cloned_from_url: Optional[str] = dataclass_field(
93
+ default=None,
94
+ metadata={"description": "URL to external Git repository linked to capsule"},
95
+ )
96
+ description: Optional[str] = dataclass_field(
97
+ default=None,
98
+ metadata={"description": "Capsule description"},
99
+ )
100
+ field: Optional[str] = dataclass_field(
101
+ default=None,
102
+ metadata={"description": "Capsule research field"},
103
+ )
104
+ tags: Optional[list[str]] = dataclass_field(
105
+ default=None,
106
+ metadata={"description": "List of tags associated with the capsule"},
107
+ )
108
+ original_capsule: Optional[OriginalCapsuleInfo] = dataclass_field(
109
+ default=None,
110
+ metadata={
111
+ "description": "Original capsule info when this capsule is duplicated from another"
112
+ },
113
+ )
114
+ release_capsule: Optional[str] = dataclass_field(
115
+ default=None,
116
+ metadata={"description": "Release capsule ID"},
117
+ )
118
+ submission: Optional[dict] = dataclass_field(
119
+ default=None,
120
+ metadata={
121
+ "description": "Submission info with timestamp, commit hash, "
122
+ "verification_capsule, verified status, and "
123
+ "verified_timestamp"
124
+ },
125
+ )
126
+ versions: Optional[list[dict]] = dataclass_field(
127
+ default=None,
128
+ metadata={
129
+ "description": "Capsule versions with major_version, minor_version, release_time, and DOI"
130
+ },
131
+ )
132
+
133
+
134
+ @dataclass_json
135
+ @dataclass(frozen=True)
136
+ class CapsuleSearchParams:
137
+ """Parameters for searching capsules with various filters and pagination
138
+ options."""
139
+
140
+ query: Optional[str] = dataclass_field(
141
+ default=None,
142
+ metadata={
143
+ "description": "Search query in free text or structured format (name:... tag:...)"
144
+ },
145
+ )
146
+ next_token: Optional[str] = dataclass_field(
147
+ default=None,
148
+ metadata={
149
+ "description": "Token for next page of results from previous response"
150
+ },
151
+ )
152
+ offset: Optional[int] = dataclass_field(
153
+ default=None,
154
+ metadata={
155
+ "description": "Starting index for search results (ignored if next_token is set)"
156
+ },
157
+ )
158
+ limit: Optional[int] = dataclass_field(
159
+ default=None,
160
+ metadata={
161
+ "description": "Number of items to return (up to 1000, defaults to 100)"
162
+ },
163
+ )
164
+ sort_field: Optional[CapsuleSortBy] = dataclass_field(
165
+ default=None,
166
+ metadata={"description": "Field to sort by (created, name, or last_accessed)"},
167
+ )
168
+ sort_order: Optional[SortOrder] = dataclass_field(
169
+ default=None,
170
+ metadata={
171
+ "description": "Sort order ('asc' or 'desc') - must be provided with a sort_field parameter as well!"
172
+ },
173
+ )
174
+ ownership: Optional[Ownership] = dataclass_field(
175
+ default=None,
176
+ metadata={
177
+ "description": "Filter by ownership ('private', 'created' or 'shared') - defaults to all accessible"
178
+ },
179
+ )
180
+ status: Optional[CapsuleStatus] = dataclass_field(
181
+ default=None,
182
+ metadata={"description": "Filter by status (release or non_release) - defaults to all"},
183
+ )
184
+ favorite: Optional[bool] = dataclass_field(
185
+ default=None,
186
+ metadata={"description": "Search only favorite capsules"},
187
+ )
188
+ archived: Optional[bool] = dataclass_field(
189
+ default=None,
190
+ metadata={"description": "Search only archived capsules"},
191
+ )
192
+ filters: Optional[list[SearchFilter]] = dataclass_field(
193
+ default=None,
194
+ metadata={
195
+ "description": "Additional field-level filters for name, description, tags, or custom fields"
196
+ },
197
+ )
198
+
199
+
200
+ @dataclass_json
201
+ @dataclass(frozen=True)
202
+ class CapsuleSearchResults:
203
+ """Results from a capsule search operation with pagination support."""
204
+
205
+ has_more: bool = dataclass_field(
206
+ metadata={"description": "Indicates if there are more results available"},
207
+ )
208
+ results: list[Capsule] = dataclass_field(
209
+ metadata={"description": "Array of capsules found matching the search criteria"},
210
+ )
211
+ next_token: Optional[str] = dataclass_field(
212
+ default=None,
213
+ metadata={"description": "Token for fetching the next page of results"},
214
+ )
215
+
216
+
217
+ @dataclass
218
+ class Capsules:
219
+ """Client for interacting with Code Ocean capsule APIs."""
220
+
221
+ client: BaseUrlSession
222
+
223
+ def get_capsule(self, capsule_id: str) -> Capsule:
224
+ """Retrieve metadata for a specific capsule by its ID."""
225
+ res = self.client.get(f"capsules/{capsule_id}")
226
+
227
+ return Capsule.from_dict(res.json())
228
+
229
+ def list_computations(self, capsule_id: str) -> list[Computation]:
230
+ """Get all computations associated with a specific capsule."""
231
+ res = self.client.get(f"capsules/{capsule_id}/computations")
232
+
233
+ return [Computation.from_dict(c) for c in res.json()]
234
+
235
+ def update_permissions(self, capsule_id: str, permissions: Permissions):
236
+ """Update permissions for a capsule."""
237
+ self.client.post(
238
+ f"capsules/{capsule_id}/permissions",
239
+ json=permissions.to_dict(),
240
+ )
241
+
242
+ def attach_data_assets(
243
+ self,
244
+ capsule_id: str,
245
+ attach_params: list[DataAssetAttachParams],
246
+ ) -> list[DataAssetAttachResults]:
247
+ """Attach one or more data assets to a capsule with optional mount paths."""
248
+ res = self.client.post(
249
+ f"capsules/{capsule_id}/data_assets",
250
+ json=[j.to_dict() for j in attach_params],
251
+ )
252
+
253
+ return [DataAssetAttachResults.from_dict(c) for c in res.json()]
254
+
255
+ def detach_data_assets(self, capsule_id: str, data_assets: list[str]):
256
+ """Detach one or more data assets from a capsule by their IDs."""
257
+ self.client.delete(
258
+ f"capsules/{capsule_id}/data_assets/",
259
+ json=data_assets,
260
+ )
261
+
262
+ def search_capsules(self, search_params: CapsuleSearchParams) -> CapsuleSearchResults:
263
+ """Search for capsules with filtering, sorting, and pagination
264
+ options."""
265
+ res = self.client.post("capsules/search", json=search_params.to_dict())
266
+
267
+ return CapsuleSearchResults.from_dict(res.json())
268
+
269
+ def search_capsules_iterator(self, search_params: CapsuleSearchParams) -> Iterator[Capsule]:
270
+ """Iterate through all capsules matching search criteria with automatic pagination."""
271
+ params = search_params.to_dict()
272
+ while True:
273
+ response = self.search_capsules(search_params=CapsuleSearchParams(**params))
274
+
275
+ for result in response.results:
276
+ yield result
277
+
278
+ if not response.has_more:
279
+ return
280
+
281
+ params["next_token"] = response.next_token
@@ -0,0 +1,55 @@
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
+ Code Ocean API client.
18
+
19
+ This class provides a unified interface to access Code Ocean's API endpoints
20
+ for managing capsules, pipelines, computations, and data assets.
21
+
22
+ Fields:
23
+ domain: The Code Ocean domain URL (e.g., 'https://codeocean.acme.com')
24
+ token: Code Ocean API access token
25
+ retries: Optional retry configuration for failed HTTP requests. Can be an integer
26
+ (number of retries) or a urllib3.util.Retry object for advanced
27
+ retry configuration. Defaults to 0 (no retries)
28
+ agent_id: Optional agent identifier for tracking AI agent API usage on behalf of users
29
+ """
30
+
31
+ domain: str
32
+ token: str
33
+ retries: Optional[Retry | int] = 0
34
+ agent_id: Optional[str] = None
35
+
36
+ # Minimum server version required by this SDK
37
+ MIN_SERVER_VERSION = "3.6.0"
38
+
39
+ def __post_init__(self):
40
+ self.session = BaseUrlSession(base_url=f"{self.domain}/api/v1/")
41
+ self.session.auth = (self.token, "")
42
+ self.session.headers.update({
43
+ "Content-Type": "application/json",
44
+ "Min-Server-Version": CodeOcean.MIN_SERVER_VERSION,
45
+ })
46
+ if self.agent_id:
47
+ self.session.headers.update({"Agent-Id": self.agent_id})
48
+ self.session.hooks["response"] = [
49
+ lambda response, *args, **kwargs: response.raise_for_status()
50
+ ]
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)
@@ -0,0 +1,148 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses_json import dataclass_json
4
+ from dataclasses import dataclass, field
5
+ from typing import Optional
6
+
7
+ from codeocean.enum import StrEnum
8
+
9
+
10
+ class UserRole(StrEnum):
11
+ """Role levels for user permissions of Code Ocean resources."""
12
+
13
+ Owner = "owner"
14
+ Editor = "editor"
15
+ Viewer = "viewer"
16
+
17
+
18
+ @dataclass_json
19
+ @dataclass(frozen=True)
20
+ class UserPermissions:
21
+ """User permission configuration with email and role assignment."""
22
+
23
+ email: str = field(
24
+ metadata={"description": "User email address for permission assignment"},
25
+ )
26
+ role: UserRole = field(
27
+ metadata={
28
+ "description": "Permission level granted to the user (owner, editor, or viewer)",
29
+ },
30
+ )
31
+
32
+
33
+ class GroupRole(StrEnum):
34
+ """Role levels for group permissions of Code Ocean resources."""
35
+
36
+ Owner = "owner"
37
+ Editor = "editor"
38
+ Viewer = "viewer"
39
+ Discoverable = "discoverable"
40
+
41
+
42
+ @dataclass_json
43
+ @dataclass(frozen=True)
44
+ class GroupPermissions:
45
+ """Group permission configuration with group identifier and role assignment."""
46
+
47
+ group: str = field(
48
+ metadata={"description": "Group identifier for permission assignment"},
49
+ )
50
+ role: GroupRole = field(
51
+ metadata={
52
+ "description": "Permission level granted to the group (owner, editor, viewer, or discoverable)",
53
+ },
54
+ )
55
+
56
+
57
+ class EveryoneRole(StrEnum):
58
+ """Role levels for public access permissions of Code Ocean resources."""
59
+
60
+ Viewer = "viewer"
61
+ Discoverable = "discoverable"
62
+ None_ = "none"
63
+
64
+
65
+ @dataclass_json
66
+ @dataclass(frozen=True)
67
+ class Permissions:
68
+ """Complete permission configuration for Code Ocean resources including users, groups, and public access."""
69
+
70
+ users: Optional[list[UserPermissions]] = field(
71
+ default=None,
72
+ metadata={"description": "List of user-specific permissions"},
73
+ )
74
+ groups: Optional[list[GroupPermissions]] = field(
75
+ default=None,
76
+ metadata={"description": "List of group-specific permissions"},
77
+ )
78
+ everyone: Optional[EveryoneRole] = field(
79
+ default=None,
80
+ metadata={"description": "Public access level (viewer, discoverable, or none)"},
81
+ )
82
+ share_assets: Optional[bool] = field(
83
+ default=None,
84
+ metadata={
85
+ "description": "Whether to share all related assets (attached data assets and "
86
+ "pipeline capsules) with added users and groups",
87
+ },
88
+ )
89
+
90
+
91
+ class SortOrder(StrEnum):
92
+ """Sort order options for search operations."""
93
+
94
+ Ascending = "asc"
95
+ Descending = "desc"
96
+
97
+
98
+ @dataclass_json
99
+ @dataclass(frozen=True)
100
+ class SearchFilterRange:
101
+ """Numeric range filter for search operations with minimum and maximum values."""
102
+
103
+ min: float = field(
104
+ metadata={"description": "Minimum value for range filter"},
105
+ )
106
+ max: float = field(
107
+ metadata={"description": "Maximum value for range filter"},
108
+ )
109
+
110
+
111
+ @dataclass_json
112
+ @dataclass(frozen=True)
113
+ class SearchFilter:
114
+ """Search filter configuration for field-level filtering with various value types and range support."""
115
+
116
+ key: str = field(
117
+ metadata={
118
+ "description": "Field name to filter on (name, description, tags, or custom field key)",
119
+ },
120
+ )
121
+ value: Optional[str | float] = field(
122
+ default=None,
123
+ metadata={"description": "Single field value to include/exclude"},
124
+ )
125
+ values: Optional[list[str | float]] = field(
126
+ default=None,
127
+ metadata={"description": "Multiple field values for inclusion/exclusion"},
128
+ )
129
+ range: Optional[SearchFilterRange] = field(
130
+ default=None,
131
+ metadata={
132
+ "description": "Numeric range filter (only one of min/max must be set)",
133
+ },
134
+ )
135
+ exclude: Optional[bool] = field(
136
+ default=None,
137
+ metadata={
138
+ "description": "Whether to include (false) or exclude (true) the specified values",
139
+ },
140
+ )
141
+
142
+
143
+ class Ownership(StrEnum):
144
+ """Ownership filter options for search operations."""
145
+
146
+ Private = "private"
147
+ Shared = "shared"
148
+ Created = "created"