codeocean 0.1.0__py3-none-any.whl

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/__init__.py ADDED
@@ -0,0 +1 @@
1
+ from codeocean.client import CodeOcean # noqa: F401
codeocean/capsule.py ADDED
@@ -0,0 +1,77 @@
1
+ from dataclasses import dataclass
2
+ from dataclasses_json import dataclass_json
3
+ from enum import StrEnum
4
+ from typing import Optional
5
+ from requests_toolbelt.sessions import BaseUrlSession
6
+
7
+ from codeocean.computation import Computation
8
+ from codeocean.data_asset import DataAssetAttachParams, DataAssetAttachResults
9
+
10
+
11
+ class CapsuleStatus(StrEnum):
12
+ NonPublished = "non-published"
13
+ Submitted = "submitted"
14
+ Publishing = "publishing"
15
+ Published = "published"
16
+ Verified = "verified"
17
+
18
+
19
+ @dataclass_json
20
+ @dataclass(frozen=True)
21
+ class OriginalCapsuleInfo:
22
+ id: Optional[str] = None
23
+ major_version: Optional[int] = None
24
+ minor_version: Optional[int] = None
25
+ name: Optional[str] = None
26
+ created: Optional[int] = None
27
+ public: Optional[bool] = None
28
+
29
+
30
+ @dataclass_json
31
+ @dataclass(frozen=True)
32
+ class Capsule:
33
+ id: str
34
+ created: int
35
+ name: str
36
+ status: CapsuleStatus
37
+ owner: str
38
+ slug: str
39
+ article: Optional[dict] = None
40
+ cloned_from_url: Optional[str] = None
41
+ description: Optional[str] = None
42
+ field: Optional[str] = None
43
+ keywords: Optional[list[str]] = None
44
+ original_capsule: Optional[OriginalCapsuleInfo] = None
45
+ published_capsule: Optional[str] = None
46
+ submission: Optional[dict] = None
47
+ versions: Optional[list[dict]] = None
48
+
49
+
50
+ @dataclass
51
+ class Capsules:
52
+
53
+ client: BaseUrlSession
54
+
55
+ def get_capsule(self, capsule_id: str) -> Capsule:
56
+ res = self.client.get(f"capsules/{capsule_id}")
57
+
58
+ return Capsule.from_dict(res.json())
59
+
60
+ def list_computations(self, capsule_id: str) -> list[Computation]:
61
+ res = self.client.get(f"capsules/{capsule_id}/computations")
62
+
63
+ return [Computation.from_dict(c) for c in res.json()]
64
+
65
+ def attach_data_assets(self, capsule_id: str, attach_params: list[DataAssetAttachParams]) -> DataAssetAttachResults:
66
+ res = self.client.post(
67
+ f"capsules/{capsule_id}/data_assets",
68
+ json=[j.to_dict() for j in attach_params],
69
+ )
70
+
71
+ return DataAssetAttachResults.from_dict(res.json())
72
+
73
+ def detach_data_assets(self, capsule_id: str, data_assets: list[str]):
74
+ self.client.delete(
75
+ f"capsules/{capsule_id}/data_assets/",
76
+ json=data_assets,
77
+ )
codeocean/client.py ADDED
@@ -0,0 +1,25 @@
1
+ from requests_toolbelt.sessions import BaseUrlSession
2
+ from dataclasses import dataclass
3
+
4
+ from codeocean.capsule import Capsules
5
+ from codeocean.computation import Computations
6
+ from codeocean.data_asset import DataAssets
7
+
8
+
9
+ @dataclass
10
+ class CodeOcean:
11
+
12
+ domain: str
13
+ token: str
14
+
15
+ def __post_init__(self):
16
+ self.session = BaseUrlSession(base_url=f"{self.domain}/api/v1/")
17
+ self.session.auth = (self.token, "")
18
+ self.session.headers.update({"Content-Type": "application/json"})
19
+ self.session.hooks["response"] = [
20
+ lambda response, *args, **kwargs: response.raise_for_status()
21
+ ]
22
+
23
+ self.capsules = Capsules(client=self.session)
24
+ self.computations = Computations(client=self.session)
25
+ self.data_assets = DataAssets(client=self.session)
@@ -0,0 +1,68 @@
1
+ from dataclasses_json import dataclass_json
2
+ from dataclasses import dataclass
3
+ from enum import StrEnum
4
+ from typing import Optional
5
+
6
+
7
+ class UserRole(StrEnum):
8
+ Owner = "owner"
9
+ Editor = "editor"
10
+ Viewer = "viewer"
11
+
12
+
13
+ @dataclass_json
14
+ @dataclass(frozen=True)
15
+ class UserPermissions:
16
+ email: str
17
+ role: UserRole
18
+
19
+
20
+ class GroupRole(StrEnum):
21
+ Owner = "owner"
22
+ Editor = "editor"
23
+ Viewer = "viewer"
24
+ Discoverable = "discoverable"
25
+
26
+
27
+ @dataclass_json
28
+ @dataclass(frozen=True)
29
+ class GroupPermissions:
30
+ group: str
31
+ role: GroupRole
32
+
33
+
34
+ class EveryoneRole(StrEnum):
35
+ Viewer = "viewer"
36
+ Discoverable = "discoverable"
37
+ None_ = "none"
38
+
39
+
40
+ @dataclass_json
41
+ @dataclass(frozen=True)
42
+ class Permissions:
43
+ users: Optional[list[UserPermissions]] = None
44
+ groups: Optional[list[GroupPermissions]] = None
45
+ everyone: Optional[EveryoneRole] = None
46
+ share_assets: Optional[bool] = None
47
+
48
+
49
+ class SortOrder(StrEnum):
50
+ Ascending = "asc"
51
+ Descending = "desc"
52
+
53
+
54
+ @dataclass_json
55
+ @dataclass(frozen=True)
56
+ class SearchFilterRange:
57
+ min: float
58
+ max: float
59
+
60
+
61
+ @dataclass_json
62
+ @dataclass(frozen=True)
63
+ class SearchFilter:
64
+ key: str
65
+ value: Optional[str | float] = None
66
+ values: Optional[list[str | float]] = None
67
+ range: Optional[SearchFilterRange] = None
68
+ exclude: Optional[bool] = None
@@ -0,0 +1,171 @@
1
+ from dataclasses import dataclass
2
+ from dataclasses_json import dataclass_json
3
+ from enum import StrEnum
4
+ from requests_toolbelt.sessions import BaseUrlSession
5
+ from typing import Optional
6
+ from time import sleep
7
+
8
+
9
+ class ComputationState(StrEnum):
10
+ Initializing = "initializing"
11
+ Running = "running"
12
+ Finalizing = "finalizing"
13
+ Completed = "completed"
14
+ Failed = "failed"
15
+
16
+
17
+ class ComputationEndStatus(StrEnum):
18
+ Succeeded = "succeeded"
19
+ Failed = "failed"
20
+ Stopped = "stopped"
21
+
22
+
23
+ @dataclass_json
24
+ @dataclass(frozen=True)
25
+ class Param:
26
+ name: Optional[str] = None
27
+ param_name: Optional[str] = None
28
+ value: Optional[str] = None
29
+
30
+
31
+ @dataclass_json
32
+ @dataclass(frozen=True)
33
+ class PipelineProcess:
34
+ name: str
35
+ capsule_id: str
36
+ version: Optional[int] = None
37
+ public: Optional[bool] = None
38
+ parameters: Optional[list[Param]] = None
39
+
40
+
41
+ @dataclass_json
42
+ @dataclass(frozen=True)
43
+ class InputDataAsset:
44
+ id: str
45
+ mount: Optional[str] = None
46
+
47
+
48
+ @dataclass_json
49
+ @dataclass(frozen=True)
50
+ class Computation:
51
+ id: str
52
+ created: int
53
+ name: str
54
+ state: ComputationState
55
+ run_time: int
56
+ cloud_workstation: Optional[bool] = None
57
+ data_assets: Optional[list[InputDataAsset]] = None
58
+ end_status: Optional[ComputationEndStatus] = None
59
+ has_results: Optional[bool] = None
60
+ parameters: Optional[list[Param]] = None
61
+ processes: Optional[list[PipelineProcess]] = None
62
+
63
+
64
+ @dataclass_json
65
+ @dataclass(frozen=True)
66
+ class DataAssetsRunParam:
67
+ id: str
68
+ mount: str
69
+
70
+
71
+ @dataclass_json
72
+ @dataclass(frozen=True)
73
+ class NamedRunParam:
74
+ param_name: str
75
+ value: str
76
+
77
+
78
+ @dataclass_json
79
+ @dataclass(frozen=True)
80
+ class PipelineProcessParams:
81
+ name: str
82
+ parameters: Optional[list[str]] = None
83
+ named_parameters: Optional[list[NamedRunParam]] = None
84
+
85
+
86
+ @dataclass_json
87
+ @dataclass(frozen=True)
88
+ class RunParams:
89
+ capsule_id: Optional[str] = None
90
+ pipeline_id: Optional[str] = None
91
+ version: Optional[int] = None
92
+ resume_run_id: Optional[str] = None
93
+ data_assets: Optional[list[DataAssetsRunParam]] = None
94
+ parameters: Optional[list[str]] = None
95
+ named_parameters: Optional[list[NamedRunParam]] = None
96
+ processes: Optional[list[PipelineProcessParams]] = None
97
+
98
+
99
+ @dataclass_json
100
+ @dataclass(frozen=True)
101
+ class FolderItem:
102
+ name: str
103
+ path: str
104
+ type: str
105
+ size: Optional[int] = None
106
+
107
+
108
+ @dataclass_json
109
+ @dataclass(frozen=True)
110
+ class Folder:
111
+ items: list[FolderItem]
112
+
113
+
114
+ @dataclass_json
115
+ @dataclass(frozen=True)
116
+ class ListFolderParams:
117
+ path: str
118
+
119
+
120
+ @dataclass_json
121
+ @dataclass(frozen=True)
122
+ class DownloadFileURL:
123
+ url: str
124
+
125
+
126
+ @dataclass
127
+ class Computations:
128
+
129
+ client: BaseUrlSession
130
+
131
+ def get_computation(self, computation_id: str) -> Computation:
132
+ res = self.client.get(f"computations/{computation_id}")
133
+
134
+ return Computation.from_dict(res.json())
135
+
136
+ def run_capsule(self, run_params: RunParams) -> Computation:
137
+ res = self.client.post("computations", json=run_params.to_dict())
138
+
139
+ return Computation.from_dict(res.json())
140
+
141
+ def wait_until_completed(self, computation: Computation) -> Computation:
142
+ """
143
+ Polls the given computation until it reaches the 'Completed' or 'Failed' state.
144
+ """
145
+ while True:
146
+ comp = self.get_computation(computation.id)
147
+
148
+ if comp.state in [ComputationState.Completed, ComputationState.Failed]:
149
+ return comp
150
+
151
+ sleep(5)
152
+
153
+ def list_computation_results(self, computation_id: str, path: str = "") -> Folder:
154
+ data = {
155
+ "path": path,
156
+ }
157
+
158
+ res = self.client.post(f"computations/{computation_id}/results", json=data)
159
+
160
+ return Folder.from_dict(res.json())
161
+
162
+ def get_result_file_download_url(self, computation_id: str, path: str) -> DownloadFileURL:
163
+ res = self.client.get(
164
+ f"computations/{computation_id}/results/download_url",
165
+ params={"path": path},
166
+ )
167
+
168
+ return DownloadFileURL.from_dict(res.json())
169
+
170
+ def delete_computation(self, computation_id: str):
171
+ self.client.delete(f"computations/{computation_id}")
@@ -0,0 +1,252 @@
1
+ from dataclasses_json import dataclass_json
2
+ from dataclasses import dataclass
3
+ from enum import StrEnum
4
+ from requests_toolbelt.sessions import BaseUrlSession
5
+ from time import sleep
6
+ from typing import Optional
7
+
8
+ from codeocean.components import SortOrder, SearchFilter, Permissions
9
+
10
+
11
+ class DataAssetType(StrEnum):
12
+ Dataset = "dataset"
13
+ Result = "result"
14
+
15
+
16
+ class DataAssetState(StrEnum):
17
+ Draft = "draft"
18
+ Ready = "ready"
19
+ Failed = "failed"
20
+
21
+
22
+ @dataclass_json
23
+ @dataclass(frozen=True)
24
+ class Provenance:
25
+ commit: str
26
+ run_script: str
27
+ data_assets: str
28
+ docker_image: str
29
+ capsule: str
30
+
31
+
32
+ class DataAssetOrigin(StrEnum):
33
+ Local = "local"
34
+ AWS = "aws"
35
+ GCP = "gcp"
36
+
37
+
38
+ @dataclass_json
39
+ @dataclass(frozen=True)
40
+ class SourceBucket:
41
+ origin: DataAssetOrigin
42
+ bucket: str
43
+ prefix: str
44
+
45
+
46
+ @dataclass_json
47
+ @dataclass(frozen=True)
48
+ class AppParameter:
49
+ name: str
50
+ value: str
51
+
52
+
53
+ @dataclass_json
54
+ @dataclass(frozen=True)
55
+ class DataAsset:
56
+ id: str
57
+ created: int
58
+ name: str
59
+ mount: str
60
+ state: DataAssetState
61
+ type: DataAssetType
62
+ last_used: int
63
+ app_parameters: Optional[list[AppParameter]] = None
64
+ custom_metadata: Optional[dict] = None
65
+ description: Optional[str] = None
66
+ failure_reason: Optional[str] = None
67
+ files: Optional[int] = None
68
+ provenance: Optional[Provenance] = None
69
+ size: Optional[int] = None
70
+ source_bucket: Optional[SourceBucket] = None
71
+ tags: Optional[list[str]] = None
72
+
73
+
74
+ @dataclass_json
75
+ @dataclass(frozen=True)
76
+ class DataAssetUpdateParams:
77
+ name: str
78
+ description: str
79
+ tags: list[str]
80
+ mount: str
81
+ custom_metadata: Optional[dict] = None
82
+
83
+
84
+ @dataclass_json
85
+ @dataclass(frozen=True)
86
+ class AWSS3Source:
87
+ bucket: str
88
+ prefix: Optional[str] = None
89
+ keep_on_external_storage: Optional[bool] = None
90
+ public: Optional[bool] = None
91
+
92
+
93
+ @dataclass_json
94
+ @dataclass(frozen=True)
95
+ class GCPCloudStorageSource:
96
+ bucket: str
97
+ client_id: Optional[str] = None
98
+ client_secret: Optional[str] = None
99
+ prefix: Optional[str] = None
100
+
101
+
102
+ @dataclass_json
103
+ @dataclass(frozen=True)
104
+ class ComputationSource:
105
+ id: str
106
+ path: Optional[str] = None
107
+
108
+
109
+ @dataclass_json
110
+ @dataclass(frozen=True)
111
+ class Source:
112
+ aws: Optional[AWSS3Source] = None
113
+ gcp: Optional[GCPCloudStorageSource] = None
114
+ computation: Optional[ComputationSource] = None
115
+
116
+
117
+ @dataclass_json
118
+ @dataclass(frozen=True)
119
+ class AWSS3Target:
120
+ bucket: str
121
+ prefix: Optional[str] = None
122
+
123
+
124
+ @dataclass_json
125
+ @dataclass(frozen=True)
126
+ class Target:
127
+ aws: Optional[AWSS3Target] = None
128
+
129
+
130
+ @dataclass_json
131
+ @dataclass(frozen=True)
132
+ class DataAssetParams:
133
+ name: str
134
+ tags: list[str]
135
+ mount: str
136
+ description: Optional[str] = None
137
+ source: Optional[Source] = None
138
+ target: Optional[Target] = None
139
+ custom_metadata: Optional[dict] = None
140
+ data_asset_ids: Optional[list[str]] = None
141
+
142
+
143
+ @dataclass_json
144
+ @dataclass(frozen=True)
145
+ class DataAssetAttachParams:
146
+ id: str
147
+ mount: Optional[str] = None
148
+
149
+
150
+ @dataclass_json
151
+ @dataclass(frozen=True)
152
+ class DataAssetAttachResults:
153
+ id: str
154
+ mount: str
155
+ mount_state: str
156
+ job_id: str
157
+ external: bool
158
+ ready: bool
159
+
160
+
161
+ class DataAssetSortBy(StrEnum):
162
+ Created = "created"
163
+ Type = "type"
164
+ Name = "name"
165
+ Size = "size"
166
+
167
+
168
+ class DataAssetOwnership(StrEnum):
169
+ Private = "private"
170
+ Shared = "shared"
171
+ Created = "created"
172
+
173
+
174
+ class DataAssetSearchOrigin(StrEnum):
175
+ Internal = "internal"
176
+ External = "external"
177
+
178
+
179
+ @dataclass_json
180
+ @dataclass(frozen=True)
181
+ class DataAssetSearchParams:
182
+ limit: int
183
+ offset: int
184
+ archived: bool
185
+ favorite: bool
186
+ query: Optional[str] = None
187
+ sort_field: Optional[DataAssetSortBy] = None
188
+ sort_order: Optional[SortOrder] = None
189
+ type: Optional[DataAssetType] = None
190
+ ownership: Optional[DataAssetOwnership] = None
191
+ origin: Optional[DataAssetSearchOrigin] = None
192
+ filters: Optional[list[SearchFilter]] = None
193
+
194
+
195
+ @dataclass_json
196
+ @dataclass(frozen=True)
197
+ class DataAssetSearchResults:
198
+ has_more: bool
199
+ results: list[DataAsset]
200
+
201
+
202
+ @dataclass
203
+ class DataAssets:
204
+
205
+ client: BaseUrlSession
206
+
207
+ def get_data_asset(self, data_asset_id: str) -> DataAsset:
208
+ res = self.client.get(f"data_assets/{data_asset_id}")
209
+
210
+ return DataAsset.from_dict(res.json())
211
+
212
+ def update_metadata(self, data_asset_id: str, update_params: DataAssetUpdateParams) -> DataAsset:
213
+ res = self.client.put(f"data_assets/{data_asset_id}", json=update_params.to_dict())
214
+
215
+ return DataAsset.from_dict(res.json())
216
+
217
+ def create_data_asset(self, data_asset_params: DataAssetParams) -> DataAsset:
218
+ res = self.client.post("data_assets", json=data_asset_params.to_dict())
219
+
220
+ return DataAsset.from_dict(res.json())
221
+
222
+ def wait_until_ready(self, data_asset: DataAsset) -> DataAsset:
223
+ """
224
+ Polls the given data asset until it reaches the 'Ready' or 'Failed' state.
225
+ """
226
+ while True:
227
+ da = self.get_data_asset(data_asset.id)
228
+
229
+ if da.state in [DataAssetState.Ready, DataAssetState.Failed]:
230
+ return da
231
+
232
+ sleep(5)
233
+
234
+ def delete_data_asset(self, data_asset_id: str):
235
+ self.client.delete(f"data_assets/{data_asset_id}")
236
+
237
+ def update_permissions(self, data_asset_id: str, permissions: Permissions):
238
+ self.client.post(
239
+ f"data_assets/{data_asset_id}/permissions",
240
+ json=permissions.to_dict(),
241
+ )
242
+
243
+ def archive_data_asset(self, data_asset_id: str, archive: bool):
244
+ self.client.patch(
245
+ f"data_assets/{data_asset_id}/archive",
246
+ params={"archive": archive},
247
+ )
248
+
249
+ def search_data_assets(self, search_params: DataAssetSearchParams) -> DataAssetSearchResults:
250
+ res = self.client.post("data_assets/search", json=search_params.to_dict())
251
+
252
+ return DataAssetSearchResults.from_dict(res.json())
@@ -0,0 +1,44 @@
1
+ Metadata-Version: 2.3
2
+ Name: codeocean
3
+ Version: 0.1.0
4
+ Summary: Code Ocean Python SDK
5
+ Project-URL: Homepage, https://github.com/codeocean/codeocean-sdk-python
6
+ Project-URL: Issues, https://github.com/codeocean/codeocean-sdk-python/issues
7
+ Project-URL: Changelog, https://github.com/codeocean/codeocean-sdk-python/blob/main/CHANGELOG.md
8
+ Author-email: Code Ocean <dev@codeocean.com>
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Requires-Python: >=3.10
15
+ Requires-Dist: dataclasses
16
+ Requires-Dist: dataclasses-json
17
+ Requires-Dist: requests
18
+ Requires-Dist: requests-toolbelt
19
+ Provides-Extra: dev
20
+ Requires-Dist: flake8; extra == 'dev'
21
+ Provides-Extra: publish
22
+ Requires-Dist: hatch; extra == 'publish'
23
+ Description-Content-Type: text/markdown
24
+
25
+ ![Code Ocean Logo](https://raw.githubusercontent.com/codeocean/branding/main/logo/CO_logo_135x72.png)
26
+ # Code Ocean Python SDK
27
+ The Code Ocean Python SDK provides convenient access to the Code Ocean API from applications written in the Python language.
28
+ This SDK enables you to manage Code Ocean resources such as Capsules, Data Assets, and Computations in your Python applications.
29
+
30
+ ## Documentation
31
+
32
+ The Code Ocean REST API documentation can be found in the [Code Ocean User Guide](https://docs.codeocean.com/user-guide/code-ocean-api).
33
+
34
+ ## Installation
35
+
36
+ ```sh
37
+ pip install -U codeocean
38
+ ```
39
+
40
+ For development, install from source with:
41
+
42
+ ```sh
43
+ pip install -e .[dev] -U
44
+ ```
@@ -0,0 +1,10 @@
1
+ codeocean/__init__.py,sha256=U35lft7yuygFsrSPhmTVKDsNMPgcSA9JvzTAGo8XfVg,53
2
+ codeocean/capsule.py,sha256=W6PmWxAwiEVUxnm_W-_rNRGie8VcvafUtTTbbC40yYE,2314
3
+ codeocean/client.py,sha256=BzxKpMe3i49vL_rfGsgN-fob97pxRMlrjmg38y1g7TY,831
4
+ codeocean/components.py,sha256=uVVCa-A4nG7MFu0KiNhhXsv3G6ZK5mS76TLbbg8oqM8,1315
5
+ codeocean/computation.py,sha256=M0xcnIafZ9QoU61frBChxLkMbcvvMFkB_Y4hwdUURp8,4281
6
+ codeocean/data_asset.py,sha256=R1FkmtyeIRRNuTVxVRP3PFvMmybQhT6RZhE-Fwp38ko,6114
7
+ codeocean-0.1.0.dist-info/METADATA,sha256=1NuTafPBHeEtzSEkvhK8FdSKE3LRUPpuXgfYZrfWej8,1542
8
+ codeocean-0.1.0.dist-info/WHEEL,sha256=LL0B1KxSLwaTWceo7tT-0aDLd-qq9dmbnsnk1DnXlg8,87
9
+ codeocean-0.1.0.dist-info/licenses/LICENSE,sha256=_TRkqGNByemZnRbLIKXWkDssI_A7qGq3yAho-lvkdaw,1092
10
+ codeocean-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.22.2
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) [2024] [Code Ocean]
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.