Skip to content

Commit e0cc854

Browse files
jake-valsamiszvikagartclaude
authored
feat: Code Ocean version 4.1 functionality (#64)
* 4.1 updates and pipelines route * fix formatting * refactor: Pipelines subclasses Capsules, move AppPanel back to capsule.py - Capsules now uses configurable _route for API paths - Pipelines extends Capsules with _route="pipelines" and method aliases - Moved AppPanel classes from components.py back to capsule.py - Added run_pipeline as alias for run_capsule in Computations Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor: use composition for Pipelines instead of inheritance Pipelines now holds an internal Capsules instance with _route="pipelines" and delegates to it. This avoids exposing Capsules methods on Pipelines. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Zvika Gart <zvikagart@gmail.com> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 6cdf8f6 commit e0cc854

File tree

6 files changed

+104
-31
lines changed

6 files changed

+104
-31
lines changed

examples/run_pipeline.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
],
3939
)
4040

41-
computation = client.computations.run_capsule(run_params)
41+
computation = client.computations.run_pipeline(run_params)
4242

4343
# Wait for pipeline to finish.
4444

src/codeocean/capsule.py

Lines changed: 10 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -470,33 +470,34 @@ class Capsules:
470470
"""Client for interacting with Code Ocean capsule APIs."""
471471

472472
client: BaseUrlSession
473+
_route: str = "capsules"
473474

474475
def get_capsule(self, capsule_id: str) -> Capsule:
475476
"""Retrieve metadata for a specific capsule by its ID."""
476-
res = self.client.get(f"capsules/{capsule_id}")
477+
res = self.client.get(f"{self._route}/{capsule_id}")
477478

478479
return Capsule.from_dict(res.json())
479480

480481
def delete_capsule(self, capsule_id: str):
481482
"""Delete a capsule permanently."""
482-
self.client.delete(f"capsules/{capsule_id}")
483+
self.client.delete(f"{self._route}/{capsule_id}")
483484

484485
def get_capsule_app_panel(self, capsule_id: str, version: Optional[int] = None) -> AppPanel:
485486
"""Retrieve app panel information for a specific capsule by its ID."""
486-
res = self.client.get(f"capsules/{capsule_id}/app_panel", params={"version": version} if version else None)
487+
res = self.client.get(f"{self._route}/{capsule_id}/app_panel", params={"version": version} if version else None)
487488

488489
return AppPanel.from_dict(res.json())
489490

490491
def list_computations(self, capsule_id: str) -> list[Computation]:
491492
"""Get all computations associated with a specific capsule."""
492-
res = self.client.get(f"capsules/{capsule_id}/computations")
493+
res = self.client.get(f"{self._route}/{capsule_id}/computations")
493494

494495
return [Computation.from_dict(c) for c in res.json()]
495496

496497
def update_permissions(self, capsule_id: str, permissions: Permissions):
497498
"""Update permissions for a capsule."""
498499
self.client.post(
499-
f"capsules/{capsule_id}/permissions",
500+
f"{self._route}/{capsule_id}/permissions",
500501
json=permissions.to_dict(),
501502
)
502503

@@ -507,7 +508,7 @@ def attach_data_assets(
507508
) -> list[DataAssetAttachResults]:
508509
"""Attach one or more data assets to a capsule with optional mount paths."""
509510
res = self.client.post(
510-
f"capsules/{capsule_id}/data_assets",
511+
f"{self._route}/{capsule_id}/data_assets",
511512
json=[j.to_dict() for j in attach_params],
512513
)
513514

@@ -516,21 +517,21 @@ def attach_data_assets(
516517
def detach_data_assets(self, capsule_id: str, data_assets: list[str]):
517518
"""Detach one or more data assets from a capsule by their IDs."""
518519
self.client.delete(
519-
f"capsules/{capsule_id}/data_assets/",
520+
f"{self._route}/{capsule_id}/data_assets/",
520521
json=data_assets,
521522
)
522523

523524
def archive_capsule(self, capsule_id: str, archive: bool):
524525
"""Archive or unarchive a capsule to control its visibility and accessibility."""
525526
self.client.patch(
526-
f"capsules/{capsule_id}/archive",
527+
f"{self._route}/{capsule_id}/archive",
527528
params={"archive": archive},
528529
)
529530

530531
def search_capsules(self, search_params: CapsuleSearchParams) -> CapsuleSearchResults:
531532
"""Search for capsules with filtering, sorting, and pagination
532533
options."""
533-
res = self.client.post("capsules/search", json=search_params.to_dict())
534+
res = self.client.post(f"{self._route}/search", json=search_params.to_dict())
534535

535536
return CapsuleSearchResults.from_dict(res.json())
536537

@@ -547,24 +548,3 @@ def search_capsules_iterator(self, search_params: CapsuleSearchParams) -> Iterat
547548
return
548549

549550
params["next_token"] = response.next_token
550-
551-
def search_pipelines(self, search_params: CapsuleSearchParams) -> CapsuleSearchResults:
552-
"""Search for pipelines with filtering, sorting, and pagination
553-
options."""
554-
res = self.client.post("pipelines/search", json=search_params.to_dict())
555-
556-
return CapsuleSearchResults.from_dict(res.json())
557-
558-
def search_pipelines_iterator(self, search_params: CapsuleSearchParams) -> Iterator[Capsule]:
559-
"""Iterate through all pipelines matching search criteria with automatic pagination."""
560-
params = search_params.to_dict()
561-
while True:
562-
response = self.search_pipelines(search_params=CapsuleSearchParams(**params))
563-
564-
for result in response.results:
565-
yield result
566-
567-
if not response.has_more:
568-
return
569-
570-
params["next_token"] = response.next_token

src/codeocean/client.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from codeocean.custom_metadata import CustomMetadataSchema
1313
from codeocean.data_asset import DataAssets
1414
from codeocean.error import Error
15+
from codeocean.pipeline import Pipelines
1516

1617

1718
@dataclass
@@ -55,6 +56,7 @@ def __post_init__(self):
5556
self.computations = Computations(client=self.session)
5657
self.custom_metadata = CustomMetadataSchema(client=self.session)
5758
self.data_assets = DataAssets(client=self.session)
59+
self.pipelines = Pipelines(client=self.session)
5860

5961
def _error_handler(self, response, *args, **kwargs):
6062
try:

src/codeocean/computation.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,9 @@ def run_capsule(self, run_params: RunParams) -> Computation:
274274

275275
return Computation.from_dict(res.json())
276276

277+
# Alias for run_capsule
278+
run_pipeline = run_capsule
279+
277280
def wait_until_completed(
278281
self,
279282
computation: Computation,

src/codeocean/data_asset.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,12 @@ class AWSS3Source:
300300
"description": "The S3 bucket from which the data asset will be created",
301301
},
302302
)
303+
endpoint_name: Optional[str] = field(
304+
default=None,
305+
metadata={
306+
"description": "The name of the custom S3 endpoint where the bucket is stored",
307+
},
308+
)
303309
prefix: Optional[str] = field(
304310
default=None,
305311
metadata={
@@ -318,6 +324,13 @@ class AWSS3Source:
318324
"description": "When true, Code Ocean will access the source bucket without credentials",
319325
},
320326
)
327+
use_input_bucket: Optional[bool] = field(
328+
default=None,
329+
metadata={
330+
"description": "When true, Code Ocean will try to create the dataset from an internal "
331+
"input bucket. All properties are ignored except for prefix. Only allowed to Admin users.",
332+
},
333+
)
321334

322335

323336
@dataclass_json
@@ -396,6 +409,10 @@ class AWSS3Target:
396409
bucket: str = field(
397410
metadata={"description": "The S3 bucket where the data asset will be stored"},
398411
)
412+
endpoint_name: Optional[str] = field(
413+
default=None,
414+
metadata={"description": "The name of the custom S3 endpoint where the bucket is stored"},
415+
)
399416
prefix: Optional[str] = field(
400417
default=None,
401418
metadata={

src/codeocean/pipeline.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
from __future__ import annotations
2+
3+
from dataclasses import dataclass, field
4+
from typing import Iterator
5+
from requests_toolbelt.sessions import BaseUrlSession
6+
7+
from codeocean.capsule import (
8+
Capsule,
9+
Capsules,
10+
CapsuleSearchParams,
11+
CapsuleSearchResults,
12+
AppPanel,
13+
)
14+
from codeocean.components import Permissions
15+
from codeocean.computation import Computation
16+
from codeocean.data_asset import DataAssetAttachParams, DataAssetAttachResults
17+
18+
19+
@dataclass
20+
class Pipelines:
21+
"""Client for interacting with Code Ocean pipeline APIs."""
22+
23+
client: BaseUrlSession
24+
_capsules: Capsules = field(init=False, repr=False)
25+
26+
def __post_init__(self):
27+
self._capsules = Capsules(client=self.client, _route="pipelines")
28+
29+
def get_pipeline(self, pipeline_id: str) -> Capsule:
30+
"""Retrieve metadata for a specific pipeline by its ID."""
31+
return self._capsules.get_capsule(pipeline_id)
32+
33+
def delete_pipeline(self, pipeline_id: str):
34+
"""Delete a pipeline permanently."""
35+
return self._capsules.delete_capsule(pipeline_id)
36+
37+
def get_pipeline_app_panel(self, pipeline_id: str, version: int | None = None) -> AppPanel:
38+
"""Retrieve app panel information for a specific pipeline by its ID."""
39+
return self._capsules.get_capsule_app_panel(pipeline_id, version)
40+
41+
def list_computations(self, pipeline_id: str) -> list[Computation]:
42+
"""Get all computations associated with a specific pipeline."""
43+
return self._capsules.list_computations(pipeline_id)
44+
45+
def update_permissions(self, pipeline_id: str, permissions: Permissions):
46+
"""Update permissions for a pipeline."""
47+
return self._capsules.update_permissions(pipeline_id, permissions)
48+
49+
def attach_data_assets(
50+
self,
51+
pipeline_id: str,
52+
attach_params: list[DataAssetAttachParams],
53+
) -> list[DataAssetAttachResults]:
54+
"""Attach one or more data assets to a pipeline with optional mount paths."""
55+
return self._capsules.attach_data_assets(pipeline_id, attach_params)
56+
57+
def detach_data_assets(self, pipeline_id: str, data_assets: list[str]):
58+
"""Detach one or more data assets from a pipeline by their IDs."""
59+
return self._capsules.detach_data_assets(pipeline_id, data_assets)
60+
61+
def archive_pipeline(self, pipeline_id: str, archive: bool):
62+
"""Archive or unarchive a pipeline to control its visibility and accessibility."""
63+
return self._capsules.archive_capsule(pipeline_id, archive)
64+
65+
def search_pipelines(self, search_params: CapsuleSearchParams) -> CapsuleSearchResults:
66+
"""Search for pipelines with filtering, sorting, and pagination options."""
67+
return self._capsules.search_capsules(search_params)
68+
69+
def search_pipelines_iterator(self, search_params: CapsuleSearchParams) -> Iterator[Capsule]:
70+
"""Iterate through all pipelines matching search criteria with automatic pagination."""
71+
return self._capsules.search_capsules_iterator(search_params)

0 commit comments

Comments
 (0)