Skip to content

Commit 59306a0

Browse files
feat: Code Ocean version 3.9 functionality (#54)
1 parent d99fb05 commit 59306a0

File tree

3 files changed

+336
-1
lines changed

3 files changed

+336
-1
lines changed

src/codeocean/capsule.py

Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,29 @@ class CapsuleSortBy(StrEnum):
2626
Name = "name"
2727

2828

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+
2952
@dataclass_json
3053
@dataclass(frozen=True)
3154
class OriginalCapsuleInfo:
@@ -81,6 +104,10 @@ class Capsule:
81104
slug: str = dataclass_field(
82105
metadata={"description": "Alternate capsule ID (URL-friendly identifier)"},
83106
)
107+
last_accessed: Optional[int] = dataclass_field(
108+
default=None,
109+
metadata={"description": "Capsule last accessed time (int64 timestamp)"},
110+
)
84111
article: Optional[dict] = dataclass_field(
85112
default=None,
86113
metadata={
@@ -245,6 +272,199 @@ class CapsuleSearchResults:
245272
)
246273

247274

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+
248468
@dataclass
249469
class Capsules:
250470
"""Client for interacting with Code Ocean capsule APIs."""
@@ -257,6 +477,16 @@ def get_capsule(self, capsule_id: str) -> Capsule:
257477

258478
return Capsule.from_dict(res.json())
259479

480+
def delete_capsule(self, capsule_id: str):
481+
"""Delete a capsule permanently."""
482+
self.client.delete(f"capsules/{capsule_id}")
483+
484+
def get_capsule_app_panel(self, capsule_id: str, version: Optional[int] = None) -> AppPanel:
485+
"""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+
488+
return AppPanel.from_dict(res.json())
489+
260490
def list_computations(self, capsule_id: str) -> list[Computation]:
261491
"""Get all computations associated with a specific capsule."""
262492
res = self.client.get(f"capsules/{capsule_id}/computations")
@@ -290,6 +520,13 @@ def detach_data_assets(self, capsule_id: str, data_assets: list[str]):
290520
json=data_assets,
291521
)
292522

523+
def archive_capsule(self, capsule_id: str, archive: bool):
524+
"""Archive or unarchive a capsule to control its visibility and accessibility."""
525+
self.client.patch(
526+
f"capsules/{capsule_id}/archive",
527+
params={"archive": archive},
528+
)
529+
293530
def search_capsules(self, search_params: CapsuleSearchParams) -> CapsuleSearchResults:
294531
"""Search for capsules with filtering, sorting, and pagination
295532
options."""

src/codeocean/client.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
from codeocean.capsule import Capsules
1111
from codeocean.computation import Computations
12+
from codeocean.custom_metadata import CustomMetadataSchema
1213
from codeocean.data_asset import DataAssets
1314
from codeocean.error import Error
1415

@@ -36,7 +37,7 @@ class CodeOcean:
3637
agent_id: Optional[str] = None
3738

3839
# Minimum server version required by this SDK
39-
MIN_SERVER_VERSION = "3.6.0"
40+
MIN_SERVER_VERSION = "3.9.0"
4041

4142
def __post_init__(self):
4243
self.session = BaseUrlSession(base_url=f"{self.domain}/api/v1/")
@@ -52,6 +53,7 @@ def __post_init__(self):
5253

5354
self.capsules = Capsules(client=self.session)
5455
self.computations = Computations(client=self.session)
56+
self.custom_metadata = CustomMetadataSchema(client=self.session)
5557
self.data_assets = DataAssets(client=self.session)
5658

5759
def _error_handler(self, response, *args, **kwargs):

src/codeocean/custom_metadata.py

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
from __future__ import annotations
2+
3+
from dataclasses import dataclass, field as dataclass_field
4+
from dataclasses_json import dataclass_json
5+
from typing import Optional, Union
6+
from requests_toolbelt.sessions import BaseUrlSession
7+
8+
from codeocean.enum import StrEnum
9+
10+
11+
class CustomMetadataFieldType(StrEnum):
12+
""" Type of the custom metadata field value. """
13+
14+
String = "string"
15+
Number = "number"
16+
Date = "date"
17+
18+
19+
@dataclass_json
20+
@dataclass(frozen=True)
21+
class CustomMetadataFieldRange:
22+
""" Range of valid values for a custom metadata field. """
23+
24+
min: Optional[float] = dataclass_field(
25+
default=None,
26+
metadata={"description": "Minimum valid value"}
27+
)
28+
max: Optional[float] = dataclass_field(
29+
default=None,
30+
metadata={"description": "Maximum valid value"}
31+
)
32+
33+
34+
@dataclass_json
35+
@dataclass(frozen=True)
36+
class CustomMetadataField:
37+
""" Represents a custom metadata field in the Code Ocean platform. """
38+
39+
name: str = dataclass_field(
40+
metadata={"description": "Name of the custom metadata field"}
41+
)
42+
type: CustomMetadataFieldType = dataclass_field(
43+
metadata={"description": "Type of the custom metadata field value (string, number, date)"}
44+
)
45+
range: Optional[CustomMetadataFieldRange] = dataclass_field(
46+
default=None,
47+
metadata={"description": "Range of valid values for the field"}
48+
)
49+
allowed_values: Optional[Union[list[str], list[float]]] = dataclass_field(
50+
default=None,
51+
metadata={"description": "Allowed values for the field (item type according to field type)"}
52+
)
53+
multiple: Optional[bool] = dataclass_field(
54+
default=None,
55+
metadata={"description": "Whether multiple values are allowed"}
56+
)
57+
units: Optional[str] = dataclass_field(
58+
default=None,
59+
metadata={"description": "Units of the field value"}
60+
)
61+
category: Optional[str] = dataclass_field(
62+
default=None,
63+
metadata={"description": "Category of the field"}
64+
)
65+
required: Optional[bool] = dataclass_field(
66+
default=None,
67+
metadata={"description": "Whether the field is required"}
68+
)
69+
70+
71+
@dataclass_json
72+
@dataclass(frozen=True)
73+
class CustomMetadata:
74+
""" Represents the custom metadata schema in the Code Ocean platform. """
75+
76+
fields: Optional[list[CustomMetadataField]] = dataclass_field(
77+
default=None,
78+
metadata={"description": "List of custom metadata fields"}
79+
)
80+
categories: Optional[list[str]] = dataclass_field(
81+
default=None,
82+
metadata={"description": "List of categories for custom metadata fields"}
83+
)
84+
85+
86+
@dataclass
87+
class CustomMetadataSchema:
88+
"""Client for getting the Code Ocean custom metadata schema."""
89+
90+
client: BaseUrlSession
91+
92+
def get_custom_metadata(self) -> CustomMetadata:
93+
"""Retrieve the Code Ocean deployment's custom metadata schema."""
94+
res = self.client.get("custom_metadata")
95+
96+
return CustomMetadata.from_dict(res.json())

0 commit comments

Comments
 (0)