Skip to content

Commit a8faf3f

Browse files
zvikagartclaude
andcommitted
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>
1 parent 295ee41 commit a8faf3f

File tree

5 files changed

+241
-575
lines changed

5 files changed

+241
-575
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: 227 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,7 @@
55
from typing import Optional, Iterator
66
from requests_toolbelt.sessions import BaseUrlSession
77

8-
from codeocean.components import (
9-
Ownership,
10-
SortOrder,
11-
SearchFilter,
12-
Permissions,
13-
AppPanel,
14-
)
8+
from codeocean.components import Ownership, SortOrder, SearchFilter, Permissions
159
from codeocean.computation import Computation
1610
from codeocean.data_asset import DataAssetAttachParams, DataAssetAttachResults
1711
from codeocean.enum import StrEnum
@@ -32,6 +26,29 @@ class CapsuleSortBy(StrEnum):
3226
Name = "name"
3327

3428

29+
class AppPanelDataAssetKind(StrEnum):
30+
"""The kind of data asset displayed in an app panel.
31+
32+
- 'Internal' → Data stored inside Code Ocean.
33+
- 'External' → Data stored external to Code Ocean.
34+
- 'Combined' → Data containing multiple external data assets.
35+
36+
In pipelines, a data asset can only be replaced with one of the same kind.
37+
"""
38+
39+
Internal = "internal"
40+
External = "external"
41+
Combined = "combined"
42+
43+
44+
class AppPanelParameterType(StrEnum):
45+
"""The type of parameter displayed in an app panel."""
46+
47+
Text = "text"
48+
List = "list"
49+
File = "file"
50+
51+
3552
@dataclass_json
3653
@dataclass(frozen=True)
3754
class OriginalCapsuleInfo:
@@ -255,38 +272,232 @@ class CapsuleSearchResults:
255272
)
256273

257274

275+
@dataclass_json
276+
@dataclass(frozen=True)
277+
class AppPanelCategories:
278+
"""Categories for a capsule's App Panel parameters."""
279+
280+
id: str = dataclass_field(
281+
metadata={"description": "Unique identifier for the category."},
282+
)
283+
name: str = dataclass_field(
284+
metadata={"description": "Human-readable name of the category."},
285+
)
286+
description: Optional[str] = dataclass_field(
287+
default=None,
288+
metadata={"description": "Optional detailed description of the category."},
289+
)
290+
help_text: Optional[str] = dataclass_field(
291+
default=None,
292+
metadata={"description": "Optional help text providing guidance or additional information about the category."},
293+
)
294+
295+
296+
@dataclass_json
297+
@dataclass(frozen=True)
298+
class AppPanelParameters:
299+
"""Parameters for a capsule's App Panel."""
300+
301+
name: str = dataclass_field(
302+
metadata={"description": "Parameter label/display name."}
303+
)
304+
type: AppPanelParameterType = dataclass_field(
305+
metadata={"description": "Type of the parameter (text, list, file)."}
306+
)
307+
category: Optional[str] = dataclass_field(
308+
default=None,
309+
metadata={"description": "ID of category the parameter belongs to."}
310+
)
311+
param_name: Optional[str] = dataclass_field(
312+
default=None,
313+
metadata={"description": "The parameter name/argument key"}
314+
)
315+
description: Optional[str] = dataclass_field(
316+
default=None,
317+
metadata={"description": "Description of the parameter."}
318+
)
319+
help_text: Optional[str] = dataclass_field(
320+
default=None,
321+
metadata={"description": "Help text for the parameter."}
322+
)
323+
value_type: Optional[str] = dataclass_field(
324+
default=None,
325+
metadata={"description": "Value type of the parameter."}
326+
)
327+
default_value: Optional[str] = dataclass_field(
328+
default=None,
329+
metadata={"description": "Default value of the parameter."}
330+
)
331+
required: Optional[bool] = dataclass_field(
332+
default=None,
333+
metadata={"description": "Indicates if the parameter is required."}
334+
)
335+
hidden: Optional[bool] = dataclass_field(
336+
default=None,
337+
metadata={"description": "Indicates if the parameter is hidden."}
338+
)
339+
minimum: Optional[float] = dataclass_field(
340+
default=None,
341+
metadata={"description": "Minimum value for the parameter."}
342+
)
343+
maximum: Optional[float] = dataclass_field(
344+
default=None,
345+
metadata={"description": "Maximum value for the parameter."}
346+
)
347+
pattern: Optional[str] = dataclass_field(
348+
default=None,
349+
metadata={"description": "Regular expression pattern for the parameter."}
350+
)
351+
value_options: Optional[list[str]] = dataclass_field(
352+
default=None,
353+
metadata={"description": "Allowed values for the parameter."}
354+
)
355+
356+
357+
@dataclass_json
358+
@dataclass(frozen=True)
359+
class AppPanelGeneral:
360+
"""General information about a capsule's App Panel."""
361+
362+
title: Optional[str] = dataclass_field(
363+
default=None,
364+
metadata={"description": "Title of the App Panel."}
365+
)
366+
instructions: Optional[str] = dataclass_field(
367+
default=None,
368+
metadata={"description": "Instructions for using the App Panel."}
369+
)
370+
help_text: Optional[str] = dataclass_field(
371+
default=None,
372+
metadata={"description": "Help text for the App Panel."}
373+
)
374+
375+
376+
@dataclass_json
377+
@dataclass(frozen=True)
378+
class AppPanelDataAsset:
379+
"""Data asset parameter for the App Panel."""
380+
381+
id: str = dataclass_field(
382+
metadata={"description": "Unique identifier for the data asset."}
383+
)
384+
mount: str = dataclass_field(
385+
metadata={"description": "Mount path of the data asset within the capsule. "
386+
"Use this mount path to replace the currently attached data asset with your own"}
387+
)
388+
name: str = dataclass_field(
389+
metadata={"description": "Display name of the data asset."}
390+
)
391+
kind: AppPanelDataAssetKind = dataclass_field(
392+
metadata={"description": "Kind of the data asset (internal, external, combined)."}
393+
)
394+
accessible: bool = dataclass_field(
395+
metadata={"description": "Indicates if the data asset is accessible to the user."}
396+
)
397+
description: Optional[str] = dataclass_field(
398+
default=None,
399+
metadata={"description": "Optional description of the data asset parameter."}
400+
)
401+
help_text: Optional[str] = dataclass_field(
402+
default=None,
403+
metadata={"description": "Optional help text for the data asset parameter."}
404+
)
405+
406+
407+
@dataclass_json
408+
@dataclass(frozen=True)
409+
class AppPanelResult:
410+
"""Selected result files to display once the computation is complete."""
411+
412+
file_name: str = dataclass_field(
413+
metadata={"description": "Name of the result file."}
414+
)
415+
416+
417+
@dataclass_json
418+
@dataclass(frozen=True)
419+
class AppPanelProcess:
420+
"""Pipeline process name and its corresponding app panel (for pipelines of capsules only)"""
421+
422+
name: str = dataclass_field(
423+
metadata={"description": "Name of the pipeline process."}
424+
)
425+
categories: Optional[AppPanelCategories] = dataclass_field(
426+
default=None,
427+
metadata={"description": "Categories for the pipeline process's app panel parameters."}
428+
)
429+
parameters: Optional[AppPanelParameters] = dataclass_field(
430+
default=None,
431+
metadata={"description": "Parameters for the pipeline process's app panel."}
432+
)
433+
434+
435+
@dataclass_json
436+
@dataclass(frozen=True)
437+
class AppPanel:
438+
"""App Panel configuration for a capsule or pipeline, including general info, data assets,
439+
categories, parameters, and results.
440+
"""
441+
442+
general: Optional[AppPanelGeneral] = dataclass_field(
443+
default=None,
444+
metadata={"description": "General information about the App Panel."}
445+
)
446+
data_assets: Optional[list[AppPanelDataAsset]] = dataclass_field(
447+
default=None,
448+
metadata={"description": "List of data assets used in the App Panel."}
449+
)
450+
categories: Optional[list[AppPanelCategories]] = dataclass_field(
451+
default=None,
452+
metadata={"description": "Categories for organizing App Panel parameters."}
453+
)
454+
parameters: Optional[list[AppPanelParameters]] = dataclass_field(
455+
default=None,
456+
metadata={"description": "Parameters for the App Panel."}
457+
)
458+
results: Optional[list[AppPanelResult]] = dataclass_field(
459+
default=None,
460+
metadata={"description": "Result files to display after computation."}
461+
)
462+
processes: Optional[list[AppPanelProcess]] = dataclass_field(
463+
default=None,
464+
metadata={"description": "Pipeline processes and their App Panels."}
465+
)
466+
467+
258468
@dataclass
259469
class Capsules:
260470
"""Client for interacting with Code Ocean capsule APIs."""
261471

262472
client: BaseUrlSession
473+
_route: str = "capsules"
263474

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

268479
return Capsule.from_dict(res.json())
269480

270481
def delete_capsule(self, capsule_id: str):
271482
"""Delete a capsule permanently."""
272-
self.client.delete(f"capsules/{capsule_id}")
483+
self.client.delete(f"{self._route}/{capsule_id}")
273484

274485
def get_capsule_app_panel(self, capsule_id: str, version: Optional[int] = None) -> AppPanel:
275486
"""Retrieve app panel information for a specific capsule by its ID."""
276-
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)
277488

278489
return AppPanel.from_dict(res.json())
279490

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

284495
return [Computation.from_dict(c) for c in res.json()]
285496

286497
def update_permissions(self, capsule_id: str, permissions: Permissions):
287498
"""Update permissions for a capsule."""
288499
self.client.post(
289-
f"capsules/{capsule_id}/permissions",
500+
f"{self._route}/{capsule_id}/permissions",
290501
json=permissions.to_dict(),
291502
)
292503

@@ -297,7 +508,7 @@ def attach_data_assets(
297508
) -> list[DataAssetAttachResults]:
298509
"""Attach one or more data assets to a capsule with optional mount paths."""
299510
res = self.client.post(
300-
f"capsules/{capsule_id}/data_assets",
511+
f"{self._route}/{capsule_id}/data_assets",
301512
json=[j.to_dict() for j in attach_params],
302513
)
303514

@@ -306,21 +517,21 @@ def attach_data_assets(
306517
def detach_data_assets(self, capsule_id: str, data_assets: list[str]):
307518
"""Detach one or more data assets from a capsule by their IDs."""
308519
self.client.delete(
309-
f"capsules/{capsule_id}/data_assets/",
520+
f"{self._route}/{capsule_id}/data_assets/",
310521
json=data_assets,
311522
)
312523

313524
def archive_capsule(self, capsule_id: str, archive: bool):
314525
"""Archive or unarchive a capsule to control its visibility and accessibility."""
315526
self.client.patch(
316-
f"capsules/{capsule_id}/archive",
527+
f"{self._route}/{capsule_id}/archive",
317528
params={"archive": archive},
318529
)
319530

320531
def search_capsules(self, search_params: CapsuleSearchParams) -> CapsuleSearchResults:
321532
"""Search for capsules with filtering, sorting, and pagination
322533
options."""
323-
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())
324535

325536
return CapsuleSearchResults.from_dict(res.json())
326537

0 commit comments

Comments
 (0)