codeocean 0.13.0__tar.gz → 0.14.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.13.0 → codeocean-0.14.0}/CHANGELOG.md +4 -0
- {codeocean-0.13.0 → codeocean-0.14.0}/PKG-INFO +1 -1
- {codeocean-0.13.0 → codeocean-0.14.0}/examples/run_pipeline.py +2 -10
- {codeocean-0.13.0 → codeocean-0.14.0}/pyproject.toml +1 -1
- codeocean-0.14.0/src/codeocean/capsule.py +114 -0
- {codeocean-0.13.0 → codeocean-0.14.0}/src/codeocean/client.py +1 -1
- codeocean-0.14.0/src/codeocean/computation.py +164 -0
- codeocean-0.14.0/src/codeocean/data_asset.py +237 -0
- codeocean-0.14.0/src/codeocean/models/__init__.py +1 -0
- {codeocean-0.13.0/src/codeocean → codeocean-0.14.0/src/codeocean/models}/capsule.py +2 -90
- codeocean-0.14.0/src/codeocean/models/components.py +148 -0
- {codeocean-0.13.0/src/codeocean → codeocean-0.14.0/src/codeocean/models}/computation.py +0 -128
- {codeocean-0.13.0/src/codeocean → codeocean-0.14.0/src/codeocean/models}/data_asset.py +36 -205
- {codeocean-0.13.0 → codeocean-0.14.0}/src/codeocean/pipeline.py +5 -5
- {codeocean-0.13.0 → codeocean-0.14.0}/.flake8 +0 -0
- {codeocean-0.13.0 → codeocean-0.14.0}/.gitignore +0 -0
- {codeocean-0.13.0 → codeocean-0.14.0}/LICENSE +0 -0
- {codeocean-0.13.0 → codeocean-0.14.0}/README.md +0 -0
- {codeocean-0.13.0 → codeocean-0.14.0}/RELEASE.md +0 -0
- {codeocean-0.13.0 → codeocean-0.14.0}/examples/create_data_asset.py +0 -0
- {codeocean-0.13.0 → codeocean-0.14.0}/examples/run_capsule.py +0 -0
- {codeocean-0.13.0 → codeocean-0.14.0}/src/codeocean/__init__.py +0 -0
- {codeocean-0.13.0 → codeocean-0.14.0}/src/codeocean/components.py +0 -0
- {codeocean-0.13.0 → codeocean-0.14.0}/src/codeocean/custom_metadata.py +0 -0
- {codeocean-0.13.0 → codeocean-0.14.0}/src/codeocean/enum.py +0 -0
- {codeocean-0.13.0 → codeocean-0.14.0}/src/codeocean/error.py +0 -0
- {codeocean-0.13.0/src/codeocean → codeocean-0.14.0/src/codeocean/models}/folder.py +0 -0
- {codeocean-0.13.0 → codeocean-0.14.0}/tests/__init__.py +0 -0
- {codeocean-0.13.0 → codeocean-0.14.0}/tests/test_client.py +0 -0
- {codeocean-0.13.0 → codeocean-0.14.0}/tests/test_error.py +0 -0
- {codeocean-0.13.0 → codeocean-0.14.0}/tests/test_package.py +0 -0
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
CHANGELOG
|
|
2
2
|
=========
|
|
3
3
|
|
|
4
|
+
## 0.14.0 (2026-01-29)
|
|
5
|
+
- [#66](https://github.com/codeocean/codeocean-sdk-python/pull/66) feat: Code Ocean version 4.2 functionality
|
|
6
|
+
- **Minimum Code Ocean platform version updated to `4.2.0`.**
|
|
7
|
+
|
|
4
8
|
## 0.13.0 (2026-01-23)
|
|
5
9
|
- [#64](https://github.com/codeocean/codeocean-sdk-python/pull/64) feat: Code Ocean version 4.1 functionality
|
|
6
10
|
- **Minimum Code Ocean platform version updated to `4.1.0`.**
|
|
@@ -1,16 +1,8 @@
|
|
|
1
1
|
import os
|
|
2
2
|
|
|
3
3
|
from codeocean import CodeOcean
|
|
4
|
-
from codeocean.computation import RunParams
|
|
5
|
-
from codeocean.data_asset import
|
|
6
|
-
DataAssetParams,
|
|
7
|
-
DataAssetsRunParam,
|
|
8
|
-
PipelineProcessParams,
|
|
9
|
-
Source,
|
|
10
|
-
ComputationSource,
|
|
11
|
-
Target,
|
|
12
|
-
AWSS3Target,
|
|
13
|
-
)
|
|
4
|
+
from codeocean.computation import RunParams, DataAssetsRunParam, PipelineProcessParams
|
|
5
|
+
from codeocean.data_asset import DataAssetParams, Source, ComputationSource, Target, AWSS3Target
|
|
14
6
|
|
|
15
7
|
|
|
16
8
|
# Create the client using your domain and API token.
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from requests_toolbelt.sessions import BaseUrlSession
|
|
5
|
+
from typing import Optional, Iterator
|
|
6
|
+
|
|
7
|
+
from codeocean.models.capsule import (
|
|
8
|
+
Capsule,
|
|
9
|
+
CapsuleSearchParams,
|
|
10
|
+
CapsuleSearchResults,
|
|
11
|
+
AppPanel,
|
|
12
|
+
)
|
|
13
|
+
# Re-exports for backward compatibility
|
|
14
|
+
from codeocean.models.capsule import ( # noqa: F401
|
|
15
|
+
CapsuleStatus,
|
|
16
|
+
CapsuleSortBy,
|
|
17
|
+
OriginalCapsuleInfo,
|
|
18
|
+
AppPanelDataAssetKind,
|
|
19
|
+
AppPanelParameterType,
|
|
20
|
+
AppPanelCategories,
|
|
21
|
+
AppPanelParameters,
|
|
22
|
+
AppPanelGeneral,
|
|
23
|
+
AppPanelDataAsset,
|
|
24
|
+
AppPanelResult,
|
|
25
|
+
AppPanelProcess,
|
|
26
|
+
)
|
|
27
|
+
from codeocean.models.components import Permissions
|
|
28
|
+
from codeocean.models.computation import Computation
|
|
29
|
+
from codeocean.models.data_asset import DataAssetAttachParams, DataAssetAttachResults
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class Capsules:
|
|
34
|
+
"""Client for interacting with Code Ocean capsule APIs."""
|
|
35
|
+
|
|
36
|
+
client: BaseUrlSession
|
|
37
|
+
_route: str = "capsules"
|
|
38
|
+
|
|
39
|
+
def get_capsule(self, capsule_id: str) -> Capsule:
|
|
40
|
+
"""Retrieve metadata for a specific capsule by its ID."""
|
|
41
|
+
res = self.client.get(f"{self._route}/{capsule_id}")
|
|
42
|
+
|
|
43
|
+
return Capsule.from_dict(res.json())
|
|
44
|
+
|
|
45
|
+
def delete_capsule(self, capsule_id: str):
|
|
46
|
+
"""Delete a capsule permanently."""
|
|
47
|
+
self.client.delete(f"{self._route}/{capsule_id}")
|
|
48
|
+
|
|
49
|
+
def get_capsule_app_panel(self, capsule_id: str, version: Optional[int] = None) -> AppPanel:
|
|
50
|
+
"""Retrieve app panel information for a specific capsule by its ID."""
|
|
51
|
+
res = self.client.get(f"{self._route}/{capsule_id}/app_panel", params={"version": version} if version else None)
|
|
52
|
+
|
|
53
|
+
return AppPanel.from_dict(res.json())
|
|
54
|
+
|
|
55
|
+
def list_computations(self, capsule_id: str) -> list[Computation]:
|
|
56
|
+
"""Get all computations associated with a specific capsule."""
|
|
57
|
+
res = self.client.get(f"{self._route}/{capsule_id}/computations")
|
|
58
|
+
|
|
59
|
+
return [Computation.from_dict(c) for c in res.json()]
|
|
60
|
+
|
|
61
|
+
def update_permissions(self, capsule_id: str, permissions: Permissions):
|
|
62
|
+
"""Update permissions for a capsule."""
|
|
63
|
+
self.client.post(
|
|
64
|
+
f"{self._route}/{capsule_id}/permissions",
|
|
65
|
+
json=permissions.to_dict(),
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
def attach_data_assets(
|
|
69
|
+
self,
|
|
70
|
+
capsule_id: str,
|
|
71
|
+
attach_params: list[DataAssetAttachParams],
|
|
72
|
+
) -> list[DataAssetAttachResults]:
|
|
73
|
+
"""Attach one or more data assets to a capsule with optional mount paths."""
|
|
74
|
+
res = self.client.post(
|
|
75
|
+
f"{self._route}/{capsule_id}/data_assets",
|
|
76
|
+
json=[j.to_dict() for j in attach_params],
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
return [DataAssetAttachResults.from_dict(c) for c in res.json()]
|
|
80
|
+
|
|
81
|
+
def detach_data_assets(self, capsule_id: str, data_assets: list[str]):
|
|
82
|
+
"""Detach one or more data assets from a capsule by their IDs."""
|
|
83
|
+
self.client.delete(
|
|
84
|
+
f"{self._route}/{capsule_id}/data_assets/",
|
|
85
|
+
json=data_assets,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
def archive_capsule(self, capsule_id: str, archive: bool):
|
|
89
|
+
"""Archive or unarchive a capsule to control its visibility and accessibility."""
|
|
90
|
+
self.client.patch(
|
|
91
|
+
f"{self._route}/{capsule_id}/archive",
|
|
92
|
+
params={"archive": archive},
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
def search_capsules(self, search_params: CapsuleSearchParams) -> CapsuleSearchResults:
|
|
96
|
+
"""Search for capsules with filtering, sorting, and pagination
|
|
97
|
+
options."""
|
|
98
|
+
res = self.client.post(f"{self._route}/search", json=search_params.to_dict())
|
|
99
|
+
|
|
100
|
+
return CapsuleSearchResults.from_dict(res.json())
|
|
101
|
+
|
|
102
|
+
def search_capsules_iterator(self, search_params: CapsuleSearchParams) -> Iterator[Capsule]:
|
|
103
|
+
"""Iterate through all capsules matching search criteria with automatic pagination."""
|
|
104
|
+
params = search_params.to_dict()
|
|
105
|
+
while True:
|
|
106
|
+
response = self.search_capsules(search_params=CapsuleSearchParams(**params))
|
|
107
|
+
|
|
108
|
+
for result in response.results:
|
|
109
|
+
yield result
|
|
110
|
+
|
|
111
|
+
if not response.has_more:
|
|
112
|
+
return
|
|
113
|
+
|
|
114
|
+
params["next_token"] = response.next_token
|
|
@@ -38,7 +38,7 @@ class CodeOcean:
|
|
|
38
38
|
agent_id: Optional[str] = None
|
|
39
39
|
|
|
40
40
|
# Minimum server version required by this SDK
|
|
41
|
-
MIN_SERVER_VERSION = "4.
|
|
41
|
+
MIN_SERVER_VERSION = "4.2.0"
|
|
42
42
|
|
|
43
43
|
def __post_init__(self):
|
|
44
44
|
self.session = BaseUrlSession(base_url=f"{self.domain}/api/v1/")
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from requests_toolbelt.sessions import BaseUrlSession
|
|
5
|
+
from typing import Optional
|
|
6
|
+
from time import sleep, time
|
|
7
|
+
from warnings import warn
|
|
8
|
+
|
|
9
|
+
from codeocean.models.computation import Computation, ComputationState, RunParams
|
|
10
|
+
# Re-exports for backward compatibility
|
|
11
|
+
from codeocean.models.computation import ( # noqa: F401
|
|
12
|
+
ComputationEndStatus,
|
|
13
|
+
Param,
|
|
14
|
+
PipelineProcess,
|
|
15
|
+
InputDataAsset,
|
|
16
|
+
DataAssetsRunParam,
|
|
17
|
+
NamedRunParam,
|
|
18
|
+
PipelineProcessParams,
|
|
19
|
+
)
|
|
20
|
+
from codeocean.models.data_asset import DataAssetAttachParams, DataAssetAttachResults
|
|
21
|
+
from codeocean.models.folder import FileURLs, Folder, DownloadFileURL
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class Computations:
|
|
26
|
+
"""Client for interacting with Code Ocean computation APIs."""
|
|
27
|
+
|
|
28
|
+
client: BaseUrlSession
|
|
29
|
+
|
|
30
|
+
def get_computation(self, computation_id: str) -> Computation:
|
|
31
|
+
"""Retrieve metadata and status information for a specific computation by its ID."""
|
|
32
|
+
res = self.client.get(f"computations/{computation_id}")
|
|
33
|
+
|
|
34
|
+
return Computation.from_dict(res.json())
|
|
35
|
+
|
|
36
|
+
def run_capsule(self, run_params: RunParams) -> Computation:
|
|
37
|
+
"""
|
|
38
|
+
Execute a capsule or pipeline with specified parameters and data assets.
|
|
39
|
+
|
|
40
|
+
For capsule execution: Set run_params.capsule_id and optionally provide data_assets,
|
|
41
|
+
parameters, or named_parameters.
|
|
42
|
+
|
|
43
|
+
For pipeline execution: Set run_params.pipeline_id and optionally provide data_assets,
|
|
44
|
+
processes (with process-specific parameters), and nextflow_profile configuration.
|
|
45
|
+
|
|
46
|
+
"""
|
|
47
|
+
res = self.client.post("computations", json=run_params.to_dict())
|
|
48
|
+
|
|
49
|
+
return Computation.from_dict(res.json())
|
|
50
|
+
|
|
51
|
+
# Alias for run_capsule
|
|
52
|
+
run_pipeline = run_capsule
|
|
53
|
+
|
|
54
|
+
def wait_until_completed(
|
|
55
|
+
self,
|
|
56
|
+
computation: Computation,
|
|
57
|
+
polling_interval: float = 5,
|
|
58
|
+
timeout: Optional[float] = None,
|
|
59
|
+
) -> Computation:
|
|
60
|
+
"""
|
|
61
|
+
Poll a computation until it reaches 'Completed' or 'Failed' state with configurable timing.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
computation: The computation object to monitor
|
|
65
|
+
polling_interval: Time between status checks in seconds (minimum 5 seconds)
|
|
66
|
+
timeout: Maximum time to wait in seconds, or None for no timeout
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
Updated computation object once completed or failed
|
|
70
|
+
|
|
71
|
+
Raises:
|
|
72
|
+
ValueError: If polling_interval < 5 or timeout constraints are violated
|
|
73
|
+
TimeoutError: If computation doesn't complete within the timeout period
|
|
74
|
+
"""
|
|
75
|
+
if polling_interval < 5:
|
|
76
|
+
raise ValueError(
|
|
77
|
+
f"Polling interval {polling_interval} should be greater than or equal to 5"
|
|
78
|
+
)
|
|
79
|
+
if timeout is not None and timeout < polling_interval:
|
|
80
|
+
raise ValueError(
|
|
81
|
+
f"Timeout {timeout} should be greater than or equal to polling interval {polling_interval}"
|
|
82
|
+
)
|
|
83
|
+
if timeout is not None and timeout < 0:
|
|
84
|
+
raise ValueError(
|
|
85
|
+
f"Timeout {timeout} should be greater than or equal to 0 (seconds), or None"
|
|
86
|
+
)
|
|
87
|
+
t0 = time()
|
|
88
|
+
while True:
|
|
89
|
+
comp = self.get_computation(computation.id)
|
|
90
|
+
|
|
91
|
+
if comp.state in [ComputationState.Completed, ComputationState.Failed]:
|
|
92
|
+
return comp
|
|
93
|
+
|
|
94
|
+
if timeout is not None and (time() - t0) > timeout:
|
|
95
|
+
raise TimeoutError(
|
|
96
|
+
f"Computation {computation.id} did not complete within {timeout} seconds"
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
sleep(polling_interval)
|
|
100
|
+
|
|
101
|
+
def attach_data_assets(
|
|
102
|
+
self,
|
|
103
|
+
computation_id: str,
|
|
104
|
+
attach_params: list[DataAssetAttachParams],
|
|
105
|
+
) -> list[DataAssetAttachResults]:
|
|
106
|
+
"""Attach one or more data assets to a cloud workstation session computation."""
|
|
107
|
+
res = self.client.post(
|
|
108
|
+
f"computations/{computation_id}/data_assets",
|
|
109
|
+
json=[j.to_dict() for j in attach_params],
|
|
110
|
+
)
|
|
111
|
+
return [DataAssetAttachResults.from_dict(c) for c in res.json()]
|
|
112
|
+
|
|
113
|
+
def detach_data_assets(self, computation_id: str, data_assets: list[str]):
|
|
114
|
+
"""Detach one or more data assets from a cloud workstation session computation by their IDs."""
|
|
115
|
+
self.client.delete(
|
|
116
|
+
f"computations/{computation_id}/data_assets/",
|
|
117
|
+
json=data_assets,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
def list_computation_results(self, computation_id: str, path: str = "") -> Folder:
|
|
121
|
+
"""List result files and folders generated by a computation
|
|
122
|
+
at the specified path. Empty path retrieves the /results root folder."""
|
|
123
|
+
data = {
|
|
124
|
+
"path": path,
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
res = self.client.post(f"computations/{computation_id}/results", json=data)
|
|
128
|
+
|
|
129
|
+
return Folder.from_dict(res.json())
|
|
130
|
+
|
|
131
|
+
def get_result_file_download_url(self, computation_id: str, path: str) -> DownloadFileURL:
|
|
132
|
+
"""[DEPRECATED] Generate a download URL for a specific result file from a computation.
|
|
133
|
+
|
|
134
|
+
Deprecated: Use get_result_file_urls instead.
|
|
135
|
+
"""
|
|
136
|
+
warn(
|
|
137
|
+
"get_result_file_download_url is deprecated and will be removed in a future release. "
|
|
138
|
+
"Use get_result_file_urls instead.",
|
|
139
|
+
DeprecationWarning,
|
|
140
|
+
stacklevel=2,
|
|
141
|
+
)
|
|
142
|
+
res = self.client.get(
|
|
143
|
+
f"computations/{computation_id}/results/download_url",
|
|
144
|
+
params={"path": path},
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
return DownloadFileURL.from_dict(res.json())
|
|
148
|
+
|
|
149
|
+
def get_result_file_urls(self, computation_id: str, path: str) -> FileURLs:
|
|
150
|
+
"""Generate view and download URLs for a specific result file from a computation."""
|
|
151
|
+
res = self.client.get(
|
|
152
|
+
f"computations/{computation_id}/results/urls",
|
|
153
|
+
params={"path": path},
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
return FileURLs.from_dict(res.json())
|
|
157
|
+
|
|
158
|
+
def delete_computation(self, computation_id: str):
|
|
159
|
+
"""Delete a computation and stop it if currently running."""
|
|
160
|
+
self.client.delete(f"computations/{computation_id}")
|
|
161
|
+
|
|
162
|
+
def rename_computation(self, computation_id: str, name: str):
|
|
163
|
+
"""Rename an existing computation with a new display name."""
|
|
164
|
+
self.client.patch(f"computations/{computation_id}", params={"name": name})
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from requests_toolbelt.sessions import BaseUrlSession
|
|
5
|
+
from time import sleep, time
|
|
6
|
+
from typing import Iterator
|
|
7
|
+
from warnings import warn
|
|
8
|
+
|
|
9
|
+
from codeocean.models.components import Permissions
|
|
10
|
+
from codeocean.models.data_asset import (
|
|
11
|
+
DataAsset,
|
|
12
|
+
DataAssetState,
|
|
13
|
+
DataAssetUpdateParams,
|
|
14
|
+
DataAssetParams,
|
|
15
|
+
DataAssetSearchParams,
|
|
16
|
+
DataAssetSearchResults,
|
|
17
|
+
TransferDataParams,
|
|
18
|
+
)
|
|
19
|
+
# Re-exports for backward compatibility
|
|
20
|
+
from codeocean.models.data_asset import ( # noqa: F401
|
|
21
|
+
DataAssetType,
|
|
22
|
+
DataAssetOrigin,
|
|
23
|
+
Provenance,
|
|
24
|
+
SourceBucket,
|
|
25
|
+
AppParameter,
|
|
26
|
+
ResultsInfo,
|
|
27
|
+
Source,
|
|
28
|
+
AWSS3Source,
|
|
29
|
+
GCPCloudStorageSource,
|
|
30
|
+
ComputationSource,
|
|
31
|
+
CloudWorkstationSource,
|
|
32
|
+
Target,
|
|
33
|
+
AWSS3Target,
|
|
34
|
+
DataAssetAttachParams,
|
|
35
|
+
DataAssetAttachResults,
|
|
36
|
+
DataAssetSortBy,
|
|
37
|
+
DataAssetSearchOrigin,
|
|
38
|
+
ContainedDataAsset,
|
|
39
|
+
)
|
|
40
|
+
from codeocean.models.folder import FileURLs, Folder, DownloadFileURL
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class DataAssets:
|
|
45
|
+
"""Client for interacting with Code Ocean data asset APIs."""
|
|
46
|
+
|
|
47
|
+
client: BaseUrlSession
|
|
48
|
+
|
|
49
|
+
def get_data_asset(self, data_asset_id: str) -> DataAsset:
|
|
50
|
+
"""Retrieve metadata for a specific data asset by its ID."""
|
|
51
|
+
res = self.client.get(f"data_assets/{data_asset_id}")
|
|
52
|
+
|
|
53
|
+
return DataAsset.from_dict(res.json())
|
|
54
|
+
|
|
55
|
+
def update_metadata(self, data_asset_id: str, update_params: DataAssetUpdateParams) -> DataAsset:
|
|
56
|
+
"""
|
|
57
|
+
Update metadata for a data asset including name, description, tags, mount,
|
|
58
|
+
and custom metadata.
|
|
59
|
+
|
|
60
|
+
Supports updating various metadata types:
|
|
61
|
+
- Basic metadata: name (display name), description (free text description)
|
|
62
|
+
- Organization: tags (keywords for searching), mount (default mount folder path)
|
|
63
|
+
- Custom metadata: admin-defined custom fields with user-set values according to
|
|
64
|
+
deployment configuration
|
|
65
|
+
(string, number, or date fields in unix epoch format)
|
|
66
|
+
"""
|
|
67
|
+
res = self.client.put(
|
|
68
|
+
f"data_assets/{data_asset_id}",
|
|
69
|
+
json=update_params.to_dict(),
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
return DataAsset.from_dict(res.json())
|
|
73
|
+
|
|
74
|
+
def create_data_asset(self, data_asset_params: DataAssetParams) -> DataAsset:
|
|
75
|
+
"""
|
|
76
|
+
Create a new data asset from various sources including S3 buckets,
|
|
77
|
+
computation results, or combined assets.
|
|
78
|
+
|
|
79
|
+
Data assets are versioned, immutable collections of files that serve as inputs
|
|
80
|
+
or outputs for computational workflows in Code Ocean. Internal data assets
|
|
81
|
+
store files within Code Ocean's infrastructure, while external data assets
|
|
82
|
+
reference files in external storage (S3/GCP) without copying.
|
|
83
|
+
|
|
84
|
+
Supports creating data assets from AWS S3, GCP Cloud Storage, computation
|
|
85
|
+
results, or combining existing data assets. Returns confirmation of creation
|
|
86
|
+
request validity, not success, as creation takes time. Use wait_until_ready()
|
|
87
|
+
to monitor creation progress.
|
|
88
|
+
|
|
89
|
+
"""
|
|
90
|
+
res = self.client.post("data_assets", json=data_asset_params.to_dict())
|
|
91
|
+
|
|
92
|
+
return DataAsset.from_dict(res.json())
|
|
93
|
+
|
|
94
|
+
def wait_until_ready(
|
|
95
|
+
self,
|
|
96
|
+
data_asset: DataAsset,
|
|
97
|
+
polling_interval: float = 5,
|
|
98
|
+
timeout: float | None = None,
|
|
99
|
+
) -> DataAsset:
|
|
100
|
+
"""
|
|
101
|
+
Poll a data asset until it reaches 'Ready' or 'Failed' state with configurable
|
|
102
|
+
timing.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
data_asset: The data asset object to monitor
|
|
106
|
+
polling_interval: Time between status checks in seconds
|
|
107
|
+
(minimum 5 seconds)
|
|
108
|
+
timeout: Maximum time to wait in seconds, or None for no timeout
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
Updated data asset object once ready or failed
|
|
112
|
+
|
|
113
|
+
Raises:
|
|
114
|
+
ValueError: If polling_interval < 5 or timeout constraints are violated
|
|
115
|
+
TimeoutError: If data asset doesn't become ready within timeout period
|
|
116
|
+
"""
|
|
117
|
+
if polling_interval < 5:
|
|
118
|
+
raise ValueError(
|
|
119
|
+
f"Polling interval {polling_interval} should be greater than or equal to 5"
|
|
120
|
+
)
|
|
121
|
+
if timeout is not None and timeout < polling_interval:
|
|
122
|
+
raise ValueError(
|
|
123
|
+
f"Timeout {timeout} should be greater than or equal to polling interval {polling_interval}"
|
|
124
|
+
)
|
|
125
|
+
if timeout is not None and timeout < 0:
|
|
126
|
+
raise ValueError(
|
|
127
|
+
f"Timeout {timeout} should be greater than or equal to 0 (seconds), or None"
|
|
128
|
+
)
|
|
129
|
+
t0 = time()
|
|
130
|
+
while True:
|
|
131
|
+
da = self.get_data_asset(data_asset.id)
|
|
132
|
+
|
|
133
|
+
if da.state in [DataAssetState.Ready, DataAssetState.Failed]:
|
|
134
|
+
return da
|
|
135
|
+
|
|
136
|
+
if timeout is not None and (time() - t0) > timeout:
|
|
137
|
+
raise TimeoutError(
|
|
138
|
+
f"Data asset {data_asset.id} was not ready within {timeout} seconds"
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
sleep(polling_interval)
|
|
142
|
+
|
|
143
|
+
def delete_data_asset(self, data_asset_id: str):
|
|
144
|
+
"""Delete a data asset permanently."""
|
|
145
|
+
self.client.delete(f"data_assets/{data_asset_id}")
|
|
146
|
+
|
|
147
|
+
def update_permissions(self, data_asset_id: str, permissions: Permissions):
|
|
148
|
+
"""Update permissions for a data asset to control user and group access."""
|
|
149
|
+
self.client.post(
|
|
150
|
+
f"data_assets/{data_asset_id}/permissions",
|
|
151
|
+
json=permissions.to_dict(),
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
def archive_data_asset(self, data_asset_id: str, archive: bool):
|
|
155
|
+
"""Archive or unarchive a data asset to control its visibility and accessibility."""
|
|
156
|
+
self.client.patch(
|
|
157
|
+
f"data_assets/{data_asset_id}/archive",
|
|
158
|
+
params={"archive": archive},
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
def search_data_assets(self, search_params: DataAssetSearchParams) -> DataAssetSearchResults:
|
|
162
|
+
"""Search for data assets with filtering, sorting, and pagination options."""
|
|
163
|
+
res = self.client.post("data_assets/search", json=search_params.to_dict())
|
|
164
|
+
|
|
165
|
+
return DataAssetSearchResults.from_dict(res.json())
|
|
166
|
+
|
|
167
|
+
def search_data_assets_iterator(self, search_params: DataAssetSearchParams) -> Iterator[DataAsset]:
|
|
168
|
+
"""
|
|
169
|
+
Iterate through all data assets matching search criteria with
|
|
170
|
+
automatic pagination.
|
|
171
|
+
"""
|
|
172
|
+
params = search_params.to_dict()
|
|
173
|
+
while True:
|
|
174
|
+
response = self.search_data_assets(
|
|
175
|
+
search_params=DataAssetSearchParams(**params),
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
for result in response.results:
|
|
179
|
+
yield result
|
|
180
|
+
|
|
181
|
+
if not response.has_more:
|
|
182
|
+
return
|
|
183
|
+
|
|
184
|
+
params["next_token"] = response.next_token
|
|
185
|
+
|
|
186
|
+
def list_data_asset_files(self, data_asset_id: str, path: str = "") -> Folder:
|
|
187
|
+
"""
|
|
188
|
+
List files and folders within an internal data asset at the specified path.
|
|
189
|
+
Empty path retrieves root level contents.
|
|
190
|
+
"""
|
|
191
|
+
data = {
|
|
192
|
+
"path": path,
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
res = self.client.post(f"data_assets/{data_asset_id}/files", json=data)
|
|
196
|
+
|
|
197
|
+
return Folder.from_dict(res.json())
|
|
198
|
+
|
|
199
|
+
def get_data_asset_file_download_url(self, data_asset_id: str, path: str) -> DownloadFileURL:
|
|
200
|
+
"""(Deprecated) Generate a download URL for a specific file from an internal data asset.
|
|
201
|
+
|
|
202
|
+
Deprecated: Use get_data_asset_file_urls instead.
|
|
203
|
+
"""
|
|
204
|
+
warn(
|
|
205
|
+
"get_data_asset_file_download_url is deprecated and will be removed in a future release. "
|
|
206
|
+
"Use get_data_asset_file_urls instead.",
|
|
207
|
+
DeprecationWarning,
|
|
208
|
+
stacklevel=2,
|
|
209
|
+
)
|
|
210
|
+
res = self.client.get(
|
|
211
|
+
f"data_assets/{data_asset_id}/files/download_url",
|
|
212
|
+
params={"path": path},
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
return DownloadFileURL.from_dict(res.json())
|
|
216
|
+
|
|
217
|
+
def get_data_asset_file_urls(self, data_asset_id: str, path: str) -> FileURLs:
|
|
218
|
+
"""Generate view and download URLs for a specific file from an internal data asset."""
|
|
219
|
+
res = self.client.get(
|
|
220
|
+
f"data_assets/{data_asset_id}/files/urls",
|
|
221
|
+
params={"path": path},
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
return FileURLs.from_dict(res.json())
|
|
225
|
+
|
|
226
|
+
def transfer_data_asset(self, data_asset_id: str, transfer_params: TransferDataParams):
|
|
227
|
+
"""
|
|
228
|
+
Transfer a data asset's files to a different S3 storage location (Admin only).
|
|
229
|
+
|
|
230
|
+
Can convert internal data assets to external or change storage location of
|
|
231
|
+
external data assets. Maintains provenance for result data assets. Use
|
|
232
|
+
force=True when transferring data assets used by release pipelines.
|
|
233
|
+
"""
|
|
234
|
+
self.client.post(
|
|
235
|
+
f"data_assets/{data_asset_id}/transfer",
|
|
236
|
+
json=transfer_params.to_dict(),
|
|
237
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Models for Code Ocean SDK."""
|
|
@@ -2,13 +2,10 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from dataclasses import dataclass, field as dataclass_field
|
|
4
4
|
from dataclasses_json import dataclass_json
|
|
5
|
-
from typing import Optional
|
|
6
|
-
from requests_toolbelt.sessions import BaseUrlSession
|
|
5
|
+
from typing import Optional
|
|
7
6
|
|
|
8
|
-
from codeocean.components import Ownership, SortOrder, SearchFilter, Permissions
|
|
9
|
-
from codeocean.computation import Computation
|
|
10
|
-
from codeocean.data_asset import DataAssetAttachParams, DataAssetAttachResults
|
|
11
7
|
from codeocean.enum import StrEnum
|
|
8
|
+
from codeocean.models.components import Ownership, SortOrder, SearchFilter
|
|
12
9
|
|
|
13
10
|
|
|
14
11
|
class CapsuleStatus(StrEnum):
|
|
@@ -463,88 +460,3 @@ class AppPanel:
|
|
|
463
460
|
default=None,
|
|
464
461
|
metadata={"description": "Pipeline processes and their App Panels."}
|
|
465
462
|
)
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
@dataclass
|
|
469
|
-
class Capsules:
|
|
470
|
-
"""Client for interacting with Code Ocean capsule APIs."""
|
|
471
|
-
|
|
472
|
-
client: BaseUrlSession
|
|
473
|
-
_route: str = "capsules"
|
|
474
|
-
|
|
475
|
-
def get_capsule(self, capsule_id: str) -> Capsule:
|
|
476
|
-
"""Retrieve metadata for a specific capsule by its ID."""
|
|
477
|
-
res = self.client.get(f"{self._route}/{capsule_id}")
|
|
478
|
-
|
|
479
|
-
return Capsule.from_dict(res.json())
|
|
480
|
-
|
|
481
|
-
def delete_capsule(self, capsule_id: str):
|
|
482
|
-
"""Delete a capsule permanently."""
|
|
483
|
-
self.client.delete(f"{self._route}/{capsule_id}")
|
|
484
|
-
|
|
485
|
-
def get_capsule_app_panel(self, capsule_id: str, version: Optional[int] = None) -> AppPanel:
|
|
486
|
-
"""Retrieve app panel information for a specific capsule by its ID."""
|
|
487
|
-
res = self.client.get(f"{self._route}/{capsule_id}/app_panel", params={"version": version} if version else None)
|
|
488
|
-
|
|
489
|
-
return AppPanel.from_dict(res.json())
|
|
490
|
-
|
|
491
|
-
def list_computations(self, capsule_id: str) -> list[Computation]:
|
|
492
|
-
"""Get all computations associated with a specific capsule."""
|
|
493
|
-
res = self.client.get(f"{self._route}/{capsule_id}/computations")
|
|
494
|
-
|
|
495
|
-
return [Computation.from_dict(c) for c in res.json()]
|
|
496
|
-
|
|
497
|
-
def update_permissions(self, capsule_id: str, permissions: Permissions):
|
|
498
|
-
"""Update permissions for a capsule."""
|
|
499
|
-
self.client.post(
|
|
500
|
-
f"{self._route}/{capsule_id}/permissions",
|
|
501
|
-
json=permissions.to_dict(),
|
|
502
|
-
)
|
|
503
|
-
|
|
504
|
-
def attach_data_assets(
|
|
505
|
-
self,
|
|
506
|
-
capsule_id: str,
|
|
507
|
-
attach_params: list[DataAssetAttachParams],
|
|
508
|
-
) -> list[DataAssetAttachResults]:
|
|
509
|
-
"""Attach one or more data assets to a capsule with optional mount paths."""
|
|
510
|
-
res = self.client.post(
|
|
511
|
-
f"{self._route}/{capsule_id}/data_assets",
|
|
512
|
-
json=[j.to_dict() for j in attach_params],
|
|
513
|
-
)
|
|
514
|
-
|
|
515
|
-
return [DataAssetAttachResults.from_dict(c) for c in res.json()]
|
|
516
|
-
|
|
517
|
-
def detach_data_assets(self, capsule_id: str, data_assets: list[str]):
|
|
518
|
-
"""Detach one or more data assets from a capsule by their IDs."""
|
|
519
|
-
self.client.delete(
|
|
520
|
-
f"{self._route}/{capsule_id}/data_assets/",
|
|
521
|
-
json=data_assets,
|
|
522
|
-
)
|
|
523
|
-
|
|
524
|
-
def archive_capsule(self, capsule_id: str, archive: bool):
|
|
525
|
-
"""Archive or unarchive a capsule to control its visibility and accessibility."""
|
|
526
|
-
self.client.patch(
|
|
527
|
-
f"{self._route}/{capsule_id}/archive",
|
|
528
|
-
params={"archive": archive},
|
|
529
|
-
)
|
|
530
|
-
|
|
531
|
-
def search_capsules(self, search_params: CapsuleSearchParams) -> CapsuleSearchResults:
|
|
532
|
-
"""Search for capsules with filtering, sorting, and pagination
|
|
533
|
-
options."""
|
|
534
|
-
res = self.client.post(f"{self._route}/search", json=search_params.to_dict())
|
|
535
|
-
|
|
536
|
-
return CapsuleSearchResults.from_dict(res.json())
|
|
537
|
-
|
|
538
|
-
def search_capsules_iterator(self, search_params: CapsuleSearchParams) -> Iterator[Capsule]:
|
|
539
|
-
"""Iterate through all capsules matching search criteria with automatic pagination."""
|
|
540
|
-
params = search_params.to_dict()
|
|
541
|
-
while True:
|
|
542
|
-
response = self.search_capsules(search_params=CapsuleSearchParams(**params))
|
|
543
|
-
|
|
544
|
-
for result in response.results:
|
|
545
|
-
yield result
|
|
546
|
-
|
|
547
|
-
if not response.has_more:
|
|
548
|
-
return
|
|
549
|
-
|
|
550
|
-
params["next_token"] = response.next_token
|
|
@@ -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"
|
|
@@ -2,13 +2,9 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from dataclasses import dataclass, field
|
|
4
4
|
from dataclasses_json import dataclass_json
|
|
5
|
-
from requests_toolbelt.sessions import BaseUrlSession
|
|
6
5
|
from typing import Optional
|
|
7
|
-
from time import sleep, time
|
|
8
|
-
from warnings import warn
|
|
9
6
|
|
|
10
7
|
from codeocean.enum import StrEnum
|
|
11
|
-
from codeocean.folder import FileURLs, Folder, DownloadFileURL
|
|
12
8
|
|
|
13
9
|
|
|
14
10
|
class ComputationState(StrEnum):
|
|
@@ -245,127 +241,3 @@ class RunParams:
|
|
|
245
241
|
default=None,
|
|
246
242
|
metadata={"description": "Process-specific parameters for pipeline runs"},
|
|
247
243
|
)
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
@dataclass
|
|
251
|
-
class Computations:
|
|
252
|
-
"""Client for interacting with Code Ocean computation APIs."""
|
|
253
|
-
|
|
254
|
-
client: BaseUrlSession
|
|
255
|
-
|
|
256
|
-
def get_computation(self, computation_id: str) -> Computation:
|
|
257
|
-
"""Retrieve metadata and status information for a specific computation by its ID."""
|
|
258
|
-
res = self.client.get(f"computations/{computation_id}")
|
|
259
|
-
|
|
260
|
-
return Computation.from_dict(res.json())
|
|
261
|
-
|
|
262
|
-
def run_capsule(self, run_params: RunParams) -> Computation:
|
|
263
|
-
"""
|
|
264
|
-
Execute a capsule or pipeline with specified parameters and data assets.
|
|
265
|
-
|
|
266
|
-
For capsule execution: Set run_params.capsule_id and optionally provide data_assets,
|
|
267
|
-
parameters, or named_parameters.
|
|
268
|
-
|
|
269
|
-
For pipeline execution: Set run_params.pipeline_id and optionally provide data_assets,
|
|
270
|
-
processes (with process-specific parameters), and nextflow_profile configuration.
|
|
271
|
-
|
|
272
|
-
"""
|
|
273
|
-
res = self.client.post("computations", json=run_params.to_dict())
|
|
274
|
-
|
|
275
|
-
return Computation.from_dict(res.json())
|
|
276
|
-
|
|
277
|
-
# Alias for run_capsule
|
|
278
|
-
run_pipeline = run_capsule
|
|
279
|
-
|
|
280
|
-
def wait_until_completed(
|
|
281
|
-
self,
|
|
282
|
-
computation: Computation,
|
|
283
|
-
polling_interval: float = 5,
|
|
284
|
-
timeout: Optional[float] = None,
|
|
285
|
-
) -> Computation:
|
|
286
|
-
"""
|
|
287
|
-
Poll a computation until it reaches 'Completed' or 'Failed' state with configurable timing.
|
|
288
|
-
|
|
289
|
-
Args:
|
|
290
|
-
computation: The computation object to monitor
|
|
291
|
-
polling_interval: Time between status checks in seconds (minimum 5 seconds)
|
|
292
|
-
timeout: Maximum time to wait in seconds, or None for no timeout
|
|
293
|
-
|
|
294
|
-
Returns:
|
|
295
|
-
Updated computation object once completed or failed
|
|
296
|
-
|
|
297
|
-
Raises:
|
|
298
|
-
ValueError: If polling_interval < 5 or timeout constraints are violated
|
|
299
|
-
TimeoutError: If computation doesn't complete within the timeout period
|
|
300
|
-
"""
|
|
301
|
-
if polling_interval < 5:
|
|
302
|
-
raise ValueError(
|
|
303
|
-
f"Polling interval {polling_interval} should be greater than or equal to 5"
|
|
304
|
-
)
|
|
305
|
-
if timeout is not None and timeout < polling_interval:
|
|
306
|
-
raise ValueError(
|
|
307
|
-
f"Timeout {timeout} should be greater than or equal to polling interval {polling_interval}"
|
|
308
|
-
)
|
|
309
|
-
if timeout is not None and timeout < 0:
|
|
310
|
-
raise ValueError(
|
|
311
|
-
f"Timeout {timeout} should be greater than or equal to 0 (seconds), or None"
|
|
312
|
-
)
|
|
313
|
-
t0 = time()
|
|
314
|
-
while True:
|
|
315
|
-
comp = self.get_computation(computation.id)
|
|
316
|
-
|
|
317
|
-
if comp.state in [ComputationState.Completed, ComputationState.Failed]:
|
|
318
|
-
return comp
|
|
319
|
-
|
|
320
|
-
if timeout is not None and (time() - t0) > timeout:
|
|
321
|
-
raise TimeoutError(
|
|
322
|
-
f"Computation {computation.id} did not complete within {timeout} seconds"
|
|
323
|
-
)
|
|
324
|
-
|
|
325
|
-
sleep(polling_interval)
|
|
326
|
-
|
|
327
|
-
def list_computation_results(self, computation_id: str, path: str = "") -> Folder:
|
|
328
|
-
"""List result files and folders generated by a computation
|
|
329
|
-
at the specified path. Empty path retrieves the /results root folder."""
|
|
330
|
-
data = {
|
|
331
|
-
"path": path,
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
res = self.client.post(f"computations/{computation_id}/results", json=data)
|
|
335
|
-
|
|
336
|
-
return Folder.from_dict(res.json())
|
|
337
|
-
|
|
338
|
-
def get_result_file_download_url(self, computation_id: str, path: str) -> DownloadFileURL:
|
|
339
|
-
"""[DEPRECATED] Generate a download URL for a specific result file from a computation.
|
|
340
|
-
|
|
341
|
-
Deprecated: Use get_result_file_urls instead.
|
|
342
|
-
"""
|
|
343
|
-
warn(
|
|
344
|
-
"get_result_file_download_url is deprecated and will be removed in a future release. "
|
|
345
|
-
"Use get_result_file_urls instead.",
|
|
346
|
-
DeprecationWarning,
|
|
347
|
-
stacklevel=2,
|
|
348
|
-
)
|
|
349
|
-
res = self.client.get(
|
|
350
|
-
f"computations/{computation_id}/results/download_url",
|
|
351
|
-
params={"path": path},
|
|
352
|
-
)
|
|
353
|
-
|
|
354
|
-
return DownloadFileURL.from_dict(res.json())
|
|
355
|
-
|
|
356
|
-
def get_result_file_urls(self, computation_id: str, path: str) -> FileURLs:
|
|
357
|
-
"""Generate view and download URLs for a specific result file from a computation."""
|
|
358
|
-
res = self.client.get(
|
|
359
|
-
f"computations/{computation_id}/results/urls",
|
|
360
|
-
params={"path": path},
|
|
361
|
-
)
|
|
362
|
-
|
|
363
|
-
return FileURLs.from_dict(res.json())
|
|
364
|
-
|
|
365
|
-
def delete_computation(self, computation_id: str):
|
|
366
|
-
"""Delete a computation and stop it if currently running."""
|
|
367
|
-
self.client.delete(f"computations/{computation_id}")
|
|
368
|
-
|
|
369
|
-
def rename_computation(self, computation_id: str, name: str):
|
|
370
|
-
"""Rename an existing computation with a new display name."""
|
|
371
|
-
self.client.patch(f"computations/{computation_id}", params={"name": name})
|
|
@@ -2,15 +2,11 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from dataclasses_json import dataclass_json
|
|
4
4
|
from dataclasses import dataclass, field
|
|
5
|
-
from
|
|
6
|
-
from time import sleep, time
|
|
7
|
-
from typing import Optional, Iterator
|
|
8
|
-
from warnings import warn
|
|
5
|
+
from typing import Optional
|
|
9
6
|
|
|
10
|
-
from codeocean.components import Ownership, SortOrder, SearchFilter, Permissions
|
|
11
|
-
from codeocean.computation import PipelineProcess, Param
|
|
12
7
|
from codeocean.enum import StrEnum
|
|
13
|
-
from codeocean.
|
|
8
|
+
from codeocean.models.components import Ownership, SortOrder, SearchFilter
|
|
9
|
+
from codeocean.models.computation import PipelineProcess, Param
|
|
14
10
|
|
|
15
11
|
|
|
16
12
|
class DataAssetType(StrEnum):
|
|
@@ -382,6 +378,32 @@ class ComputationSource:
|
|
|
382
378
|
)
|
|
383
379
|
|
|
384
380
|
|
|
381
|
+
@dataclass_json
|
|
382
|
+
@dataclass(frozen=True)
|
|
383
|
+
class CloudWorkstationSource:
|
|
384
|
+
"""Cloud Workstation session source configuration for creating data assets."""
|
|
385
|
+
|
|
386
|
+
id: str = field(
|
|
387
|
+
metadata={"description": "Computation ID of the Cloud Workstation session"},
|
|
388
|
+
)
|
|
389
|
+
path: str = field(
|
|
390
|
+
metadata={
|
|
391
|
+
"description": (
|
|
392
|
+
"Path within the Cloud Workstation to create the data asset from"
|
|
393
|
+
),
|
|
394
|
+
},
|
|
395
|
+
)
|
|
396
|
+
run_script: Optional[str] = field(
|
|
397
|
+
default=None,
|
|
398
|
+
metadata={
|
|
399
|
+
"description": (
|
|
400
|
+
"Path to the script that was executed, relative to the capsule folder. "
|
|
401
|
+
"Existence determines if the data would be of type result"
|
|
402
|
+
),
|
|
403
|
+
},
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
|
|
385
407
|
@dataclass_json
|
|
386
408
|
@dataclass(frozen=True)
|
|
387
409
|
class Source:
|
|
@@ -399,6 +421,10 @@ class Source:
|
|
|
399
421
|
default=None,
|
|
400
422
|
metadata={"description": "Computation source configuration"},
|
|
401
423
|
)
|
|
424
|
+
cloud_workstation: Optional[CloudWorkstationSource] = field(
|
|
425
|
+
default=None,
|
|
426
|
+
metadata={"description": "Cloud Workstation source configuration"},
|
|
427
|
+
)
|
|
402
428
|
|
|
403
429
|
|
|
404
430
|
@dataclass_json
|
|
@@ -459,7 +485,9 @@ class DataAssetParams:
|
|
|
459
485
|
)
|
|
460
486
|
source: Optional[Source] = field(
|
|
461
487
|
default=None,
|
|
462
|
-
metadata={
|
|
488
|
+
metadata={
|
|
489
|
+
"description": "Source configuration (AWS S3, GCP, computation, or cloud workstation)",
|
|
490
|
+
},
|
|
463
491
|
)
|
|
464
492
|
target: Optional[Target] = field(
|
|
465
493
|
default=None,
|
|
@@ -708,200 +736,3 @@ class TransferDataParams:
|
|
|
708
736
|
),
|
|
709
737
|
},
|
|
710
738
|
)
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
@dataclass
|
|
714
|
-
class DataAssets:
|
|
715
|
-
"""Client for interacting with Code Ocean data asset APIs."""
|
|
716
|
-
|
|
717
|
-
client: BaseUrlSession
|
|
718
|
-
|
|
719
|
-
def get_data_asset(self, data_asset_id: str) -> DataAsset:
|
|
720
|
-
"""Retrieve metadata for a specific data asset by its ID."""
|
|
721
|
-
res = self.client.get(f"data_assets/{data_asset_id}")
|
|
722
|
-
|
|
723
|
-
return DataAsset.from_dict(res.json())
|
|
724
|
-
|
|
725
|
-
def update_metadata(self, data_asset_id: str, update_params: DataAssetUpdateParams) -> DataAsset:
|
|
726
|
-
"""
|
|
727
|
-
Update metadata for a data asset including name, description, tags, mount,
|
|
728
|
-
and custom metadata.
|
|
729
|
-
|
|
730
|
-
Supports updating various metadata types:
|
|
731
|
-
- Basic metadata: name (display name), description (free text description)
|
|
732
|
-
- Organization: tags (keywords for searching), mount (default mount folder path)
|
|
733
|
-
- Custom metadata: admin-defined custom fields with user-set values according to
|
|
734
|
-
deployment configuration
|
|
735
|
-
(string, number, or date fields in unix epoch format)
|
|
736
|
-
"""
|
|
737
|
-
res = self.client.put(
|
|
738
|
-
f"data_assets/{data_asset_id}",
|
|
739
|
-
json=update_params.to_dict(),
|
|
740
|
-
)
|
|
741
|
-
|
|
742
|
-
return DataAsset.from_dict(res.json())
|
|
743
|
-
|
|
744
|
-
def create_data_asset(self, data_asset_params: DataAssetParams) -> DataAsset:
|
|
745
|
-
"""
|
|
746
|
-
Create a new data asset from various sources including S3 buckets,
|
|
747
|
-
computation results, or combined assets.
|
|
748
|
-
|
|
749
|
-
Data assets are versioned, immutable collections of files that serve as inputs
|
|
750
|
-
or outputs for computational workflows in Code Ocean. Internal data assets
|
|
751
|
-
store files within Code Ocean's infrastructure, while external data assets
|
|
752
|
-
reference files in external storage (S3/GCP) without copying.
|
|
753
|
-
|
|
754
|
-
Supports creating data assets from AWS S3, GCP Cloud Storage, computation
|
|
755
|
-
results, or combining existing data assets. Returns confirmation of creation
|
|
756
|
-
request validity, not success, as creation takes time. Use wait_until_ready()
|
|
757
|
-
to monitor creation progress.
|
|
758
|
-
|
|
759
|
-
"""
|
|
760
|
-
res = self.client.post("data_assets", json=data_asset_params.to_dict())
|
|
761
|
-
|
|
762
|
-
return DataAsset.from_dict(res.json())
|
|
763
|
-
|
|
764
|
-
def wait_until_ready(
|
|
765
|
-
self,
|
|
766
|
-
data_asset: DataAsset,
|
|
767
|
-
polling_interval: float = 5,
|
|
768
|
-
timeout: float | None = None,
|
|
769
|
-
) -> DataAsset:
|
|
770
|
-
"""
|
|
771
|
-
Poll a data asset until it reaches 'Ready' or 'Failed' state with configurable
|
|
772
|
-
timing.
|
|
773
|
-
|
|
774
|
-
Args:
|
|
775
|
-
data_asset: The data asset object to monitor
|
|
776
|
-
polling_interval: Time between status checks in seconds
|
|
777
|
-
(minimum 5 seconds)
|
|
778
|
-
timeout: Maximum time to wait in seconds, or None for no timeout
|
|
779
|
-
|
|
780
|
-
Returns:
|
|
781
|
-
Updated data asset object once ready or failed
|
|
782
|
-
|
|
783
|
-
Raises:
|
|
784
|
-
ValueError: If polling_interval < 5 or timeout constraints are violated
|
|
785
|
-
TimeoutError: If data asset doesn't become ready within timeout period
|
|
786
|
-
"""
|
|
787
|
-
if polling_interval < 5:
|
|
788
|
-
raise ValueError(
|
|
789
|
-
f"Polling interval {polling_interval} should be greater than or equal to 5"
|
|
790
|
-
)
|
|
791
|
-
if timeout is not None and timeout < polling_interval:
|
|
792
|
-
raise ValueError(
|
|
793
|
-
f"Timeout {timeout} should be greater than or equal to polling interval {polling_interval}"
|
|
794
|
-
)
|
|
795
|
-
if timeout is not None and timeout < 0:
|
|
796
|
-
raise ValueError(
|
|
797
|
-
f"Timeout {timeout} should be greater than or equal to 0 (seconds), or None"
|
|
798
|
-
)
|
|
799
|
-
t0 = time()
|
|
800
|
-
while True:
|
|
801
|
-
da = self.get_data_asset(data_asset.id)
|
|
802
|
-
|
|
803
|
-
if da.state in [DataAssetState.Ready, DataAssetState.Failed]:
|
|
804
|
-
return da
|
|
805
|
-
|
|
806
|
-
if timeout is not None and (time() - t0) > timeout:
|
|
807
|
-
raise TimeoutError(
|
|
808
|
-
f"Data asset {data_asset.id} was not ready within {timeout} seconds"
|
|
809
|
-
)
|
|
810
|
-
|
|
811
|
-
sleep(polling_interval)
|
|
812
|
-
|
|
813
|
-
def delete_data_asset(self, data_asset_id: str):
|
|
814
|
-
"""Delete a data asset permanently."""
|
|
815
|
-
self.client.delete(f"data_assets/{data_asset_id}")
|
|
816
|
-
|
|
817
|
-
def update_permissions(self, data_asset_id: str, permissions: Permissions):
|
|
818
|
-
"""Update permissions for a data asset to control user and group access."""
|
|
819
|
-
self.client.post(
|
|
820
|
-
f"data_assets/{data_asset_id}/permissions",
|
|
821
|
-
json=permissions.to_dict(),
|
|
822
|
-
)
|
|
823
|
-
|
|
824
|
-
def archive_data_asset(self, data_asset_id: str, archive: bool):
|
|
825
|
-
"""Archive or unarchive a data asset to control its visibility and accessibility."""
|
|
826
|
-
self.client.patch(
|
|
827
|
-
f"data_assets/{data_asset_id}/archive",
|
|
828
|
-
params={"archive": archive},
|
|
829
|
-
)
|
|
830
|
-
|
|
831
|
-
def search_data_assets(self, search_params: DataAssetSearchParams) -> DataAssetSearchResults:
|
|
832
|
-
"""Search for data assets with filtering, sorting, and pagination options."""
|
|
833
|
-
res = self.client.post("data_assets/search", json=search_params.to_dict())
|
|
834
|
-
|
|
835
|
-
return DataAssetSearchResults.from_dict(res.json())
|
|
836
|
-
|
|
837
|
-
def search_data_assets_iterator(self, search_params: DataAssetSearchParams) -> Iterator[DataAsset]:
|
|
838
|
-
"""
|
|
839
|
-
Iterate through all data assets matching search criteria with
|
|
840
|
-
automatic pagination.
|
|
841
|
-
"""
|
|
842
|
-
params = search_params.to_dict()
|
|
843
|
-
while True:
|
|
844
|
-
response = self.search_data_assets(
|
|
845
|
-
search_params=DataAssetSearchParams(**params),
|
|
846
|
-
)
|
|
847
|
-
|
|
848
|
-
for result in response.results:
|
|
849
|
-
yield result
|
|
850
|
-
|
|
851
|
-
if not response.has_more:
|
|
852
|
-
return
|
|
853
|
-
|
|
854
|
-
params["next_token"] = response.next_token
|
|
855
|
-
|
|
856
|
-
def list_data_asset_files(self, data_asset_id: str, path: str = "") -> Folder:
|
|
857
|
-
"""
|
|
858
|
-
List files and folders within an internal data asset at the specified path.
|
|
859
|
-
Empty path retrieves root level contents.
|
|
860
|
-
"""
|
|
861
|
-
data = {
|
|
862
|
-
"path": path,
|
|
863
|
-
}
|
|
864
|
-
|
|
865
|
-
res = self.client.post(f"data_assets/{data_asset_id}/files", json=data)
|
|
866
|
-
|
|
867
|
-
return Folder.from_dict(res.json())
|
|
868
|
-
|
|
869
|
-
def get_data_asset_file_download_url(self, data_asset_id: str, path: str) -> DownloadFileURL:
|
|
870
|
-
"""(Deprecated) Generate a download URL for a specific file from an internal data asset.
|
|
871
|
-
|
|
872
|
-
Deprecated: Use get_data_asset_file_urls instead.
|
|
873
|
-
"""
|
|
874
|
-
warn(
|
|
875
|
-
"get_data_asset_file_download_url is deprecated and will be removed in a future release. "
|
|
876
|
-
"Use get_data_asset_file_urls instead.",
|
|
877
|
-
DeprecationWarning,
|
|
878
|
-
stacklevel=2,
|
|
879
|
-
)
|
|
880
|
-
res = self.client.get(
|
|
881
|
-
f"data_assets/{data_asset_id}/files/download_url",
|
|
882
|
-
params={"path": path},
|
|
883
|
-
)
|
|
884
|
-
|
|
885
|
-
return DownloadFileURL.from_dict(res.json())
|
|
886
|
-
|
|
887
|
-
def get_data_asset_file_urls(self, data_asset_id: str, path: str) -> FileURLs:
|
|
888
|
-
"""Generate view and download URLs for a specific file from an internal data asset."""
|
|
889
|
-
res = self.client.get(
|
|
890
|
-
f"data_assets/{data_asset_id}/files/urls",
|
|
891
|
-
params={"path": path},
|
|
892
|
-
)
|
|
893
|
-
|
|
894
|
-
return FileURLs.from_dict(res.json())
|
|
895
|
-
|
|
896
|
-
def transfer_data_asset(self, data_asset_id: str, transfer_params: TransferDataParams):
|
|
897
|
-
"""
|
|
898
|
-
Transfer a data asset's files to a different S3 storage location (Admin only).
|
|
899
|
-
|
|
900
|
-
Can convert internal data assets to external or change storage location of
|
|
901
|
-
external data assets. Maintains provenance for result data assets. Use
|
|
902
|
-
force=True when transferring data assets used by release pipelines.
|
|
903
|
-
"""
|
|
904
|
-
self.client.post(
|
|
905
|
-
f"data_assets/{data_asset_id}/transfer",
|
|
906
|
-
json=transfer_params.to_dict(),
|
|
907
|
-
)
|
|
@@ -4,16 +4,16 @@ from dataclasses import dataclass, field
|
|
|
4
4
|
from typing import Iterator
|
|
5
5
|
from requests_toolbelt.sessions import BaseUrlSession
|
|
6
6
|
|
|
7
|
-
from codeocean.capsule import
|
|
7
|
+
from codeocean.capsule import Capsules
|
|
8
|
+
from codeocean.models.capsule import (
|
|
8
9
|
Capsule,
|
|
9
|
-
Capsules,
|
|
10
10
|
CapsuleSearchParams,
|
|
11
11
|
CapsuleSearchResults,
|
|
12
12
|
AppPanel,
|
|
13
13
|
)
|
|
14
|
-
from codeocean.components import Permissions
|
|
15
|
-
from codeocean.computation import Computation
|
|
16
|
-
from codeocean.data_asset import DataAssetAttachParams, DataAssetAttachResults
|
|
14
|
+
from codeocean.models.components import Permissions
|
|
15
|
+
from codeocean.models.computation import Computation
|
|
16
|
+
from codeocean.models.data_asset import DataAssetAttachParams, DataAssetAttachResults
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
@dataclass
|
|
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
|
|
File without changes
|