From f5844c263c4b28afe24ca98fc22477b005cebe1a Mon Sep 17 00:00:00 2001 From: jake-valsamis Date: Thu, 28 Aug 2025 22:25:27 -0400 Subject: [PATCH 1/3] added v3.9 API functionality --- src/codeocean/capsule.py | 114 ++++++++++++++++++++++++++++++- src/codeocean/client.py | 2 + src/codeocean/custom_metadata.py | 54 +++++++++++++++ 3 files changed, 169 insertions(+), 1 deletion(-) create mode 100644 src/codeocean/custom_metadata.py diff --git a/src/codeocean/capsule.py b/src/codeocean/capsule.py index 119afcc..52c6ed6 100644 --- a/src/codeocean/capsule.py +++ b/src/codeocean/capsule.py @@ -2,7 +2,7 @@ from dataclasses import dataclass, field as dataclass_field from dataclasses_json import dataclass_json -from typing import Optional, Iterator +from typing import Optional, Iterator, Any from requests_toolbelt.sessions import BaseUrlSession from codeocean.components import Ownership, SortOrder, SearchFilter, Permissions @@ -26,6 +26,18 @@ class CapsuleSortBy(StrEnum): Name = "name" +class AppPanelDataAssetKind(StrEnum): + Internal = "internal" + External = "external" + Combined = "combined" + + +class AppPanelParameterType(StrEnum): + Text = "text" + List = "list" + File = "file" + + @dataclass_json @dataclass(frozen=True) class OriginalCapsuleInfo: @@ -81,6 +93,10 @@ class Capsule: slug: str = dataclass_field( metadata={"description": "Alternate capsule ID (URL-friendly identifier)"}, ) + last_accessed: Optional[int] = dataclass_field( + default=None, + metadata={"description": "Capsule last accessed time (int64 timestamp)"}, + ) article: Optional[dict] = dataclass_field( default=None, metadata={ @@ -214,6 +230,85 @@ class CapsuleSearchResults: ) +@dataclass_json +@dataclass(frozen=True) +class AppPanelCategories: + id: str + name: str + description: Optional[str] = None + help_text: Optional[str] = None + + +@dataclass_json +@dataclass(frozen=True) +class AppPanelParameters: + name: str + type: AppPanelParameterType + category: Optional[str] = None + param_name: Optional[str] = None + description: Optional[str] = None + help_text: Optional[str] = None + value_type: Optional[str] = None + default_value: Optional[str] = None + required: Optional[bool] = None + hidden: Optional[bool] = None + minimum: Optional[float] = None + maximum: Optional[float] = None + pattern: Optional[str] = None + value_options: Optional[Any] = None + + +@dataclass_json +@dataclass(frozen=True) +class AppPanelGeneral: + title: Optional[str] = None + instructions: Optional[str] = None + help_text: Optional[str] = None + + +@dataclass_json +@dataclass(frozen=True) +class AppPanelDataAsset: + id: str + mount: str + name: str + kind: AppPanelDataAssetKind + accessible: bool + description: Optional[str] = None + help_text: Optional[str] = None + + +@dataclass_json +@dataclass(frozen=True) +class AppPanelResult: + file_name: str + + +@dataclass_json +@dataclass(frozen=True) +class AppPanelProcess: + name: str + categories: Optional[AppPanelCategories] = None + parameters: Optional[AppPanelParameters] = None + + +@dataclass_json +@dataclass(frozen=True) +class AppPanel: + general: Optional[AppPanelGeneral] = None + data_assets: Optional[list[AppPanelDataAsset]] = None + categories: Optional[list[AppPanelCategories]] = None + parameters: Optional[list[AppPanelParameters]] = None + results: Optional[list[AppPanelResult]] = None + processes: Optional[list[AppPanelProcess]] = None + + +@dataclass_json +@dataclass(frozen=True) +class AppPanelParams: + version: Optional[int] = None + + @dataclass class Capsules: """Client for interacting with Code Ocean capsule APIs.""" @@ -279,3 +374,20 @@ def search_capsules_iterator(self, search_params: CapsuleSearchParams) -> Iterat return params["next_token"] = response.next_token + + def get_capsule_app_panel(self, capsule_id: str, version: Optional[AppPanelParams] = None) -> AppPanel: + """Retrieve app panel information for a specific capsule by its ID.""" + res = self.client.get(f"capsules/{capsule_id}/parameters", params=version.to_dict() if version else None) + + return AppPanel.from_dict(res.json()) + + def archive_capsule(self, capsule_id: str, archive: bool): + """Archive or unarchive a capsule to control its visibility and accessibility.""" + self.client.patch( + f"capsules/{capsule_id}/archive", + params={"archive": archive}, + ) + + def delete_capsule(self, capsule_id: str): + """Delete a capsule permanently.""" + self.client.delete(f"capsules/{capsule_id}") diff --git a/src/codeocean/client.py b/src/codeocean/client.py index 33a42ba..ef4641e 100644 --- a/src/codeocean/client.py +++ b/src/codeocean/client.py @@ -9,6 +9,7 @@ from codeocean.capsule import Capsules from codeocean.computation import Computations +from codeocean.custom_metadata import CustomMetadataSchema from codeocean.data_asset import DataAssets from codeocean.error import Error @@ -52,6 +53,7 @@ def __post_init__(self): self.capsules = Capsules(client=self.session) self.computations = Computations(client=self.session) + self.custom_metadata = CustomMetadataSchema(client=self.session) self.data_assets = DataAssets(client=self.session) def _error_handler(self, response, *args, **kwargs): diff --git a/src/codeocean/custom_metadata.py b/src/codeocean/custom_metadata.py new file mode 100644 index 0000000..35abc14 --- /dev/null +++ b/src/codeocean/custom_metadata.py @@ -0,0 +1,54 @@ +from __future__ import annotations + +from dataclasses import dataclass +from dataclasses_json import dataclass_json +from typing import Optional, Union +from requests_toolbelt.sessions import BaseUrlSession + +from codeocean.enum import StrEnum + + +class CustomMetadataFieldType(StrEnum): + String = "string" + Number = "number" + Date = "date" + + +@dataclass_json +@dataclass(frozen=True) +class CustomMetadataFieldRange: + min: Optional[float] = None + max: Optional[float] = None + + +@dataclass_json +@dataclass(frozen=True) +class CustomMetadataField: + name: str + type: CustomMetadataFieldType + range: Optional[CustomMetadataFieldRange] = None + allowed_values: Optional[Union[list[str], list[float]]] = None + multiple: Optional[bool] = None + units: Optional[str] = None + category: Optional[str] = None + required: Optional[bool] = None + + +@dataclass_json +@dataclass(frozen=True) +class CustomMetadata: + fields: Optional[list[CustomMetadataField]] = None + categories: Optional[list[str]] = None + + +@dataclass +class CustomMetadataSchema: + """Client for getting the Code Ocean custom metadata schema.""" + + client: BaseUrlSession + + def get_custom_metadata(self) -> CustomMetadata: + """Retrieve metadata for a specific capsule by its ID.""" + res = self.client.get("custom_metadata") + + return CustomMetadata.from_dict(res.json()) From 5c2cddabd51dcc1c5c2311e24957b9ed0ca3020a Mon Sep 17 00:00:00 2001 From: jake-valsamis Date: Fri, 29 Aug 2025 14:00:01 -0400 Subject: [PATCH 2/3] fix issues, add class and field metadata, bump min CO version --- src/codeocean/capsule.py | 242 +++++++++++++++++++++++-------- src/codeocean/client.py | 2 +- src/codeocean/custom_metadata.py | 66 +++++++-- 3 files changed, 234 insertions(+), 76 deletions(-) diff --git a/src/codeocean/capsule.py b/src/codeocean/capsule.py index 52c6ed6..6cf4b4d 100644 --- a/src/codeocean/capsule.py +++ b/src/codeocean/capsule.py @@ -27,12 +27,24 @@ class CapsuleSortBy(StrEnum): class AppPanelDataAssetKind(StrEnum): + """The kind of data asset displayed in an app panel. + + - 'Internal' → Data stored inside Code Ocean. + - 'External' → Data stored external to Code Ocean. + - 'Combined' → Data containing multiple external data assets. + + In pipelines, a data asset can only be replaced with one of the same kind. + + """ + Internal = "internal" External = "external" Combined = "combined" class AppPanelParameterType(StrEnum): + """The type of parameter displayed in an app panel.""" + Text = "text" List = "list" File = "file" @@ -233,80 +245,188 @@ class CapsuleSearchResults: @dataclass_json @dataclass(frozen=True) class AppPanelCategories: - id: str - name: str - description: Optional[str] = None - help_text: Optional[str] = None + """ Categories for a capsule's App Panel parameters.""" + + id: str = dataclass_field( + metadata={"description": "Unique identifier for the category."}, + ) + name: str = dataclass_field( + metadata={"description": "Human-readable name of the category."}, + ) + description: Optional[str] = dataclass_field( + default=None, + metadata={"description": "Optional detailed description of the category."}, + ) + help_text: Optional[str] = dataclass_field( + default=None, + metadata={"description": "Optional help text providing guidance or additional information about the category."}, + ) @dataclass_json @dataclass(frozen=True) class AppPanelParameters: - name: str - type: AppPanelParameterType - category: Optional[str] = None - param_name: Optional[str] = None - description: Optional[str] = None - help_text: Optional[str] = None - value_type: Optional[str] = None - default_value: Optional[str] = None - required: Optional[bool] = None - hidden: Optional[bool] = None - minimum: Optional[float] = None - maximum: Optional[float] = None - pattern: Optional[str] = None - value_options: Optional[Any] = None + """Parameters for a capsule's App Panel.""" + name: str = dataclass_field( + metadata={"description": "Parameter label/display name."} + ) + type: AppPanelParameterType = dataclass_field( + metadata={"description": "Type of the parameter (text, list, file)."} + ) + category: Optional[str] = dataclass_field( + default=None, + metadata={"description": "ID of category the parameter belongs to."} + ) + param_name: Optional[str] = dataclass_field( + default=None, + metadata={"description": "The parameter name/argument key"} + ) + description: Optional[str] = dataclass_field( + default=None, + metadata={"description": "Description of the parameter."} + ) + help_text: Optional[str] = dataclass_field( + default=None, + metadata={"description": "Help text for the parameter."} + ) + value_type: Optional[str] = dataclass_field( + default=None, + metadata={"description": "Value type of the parameter."} + ) + default_value: Optional[str] = dataclass_field( + default=None, + metadata={"description": "Default value of the parameter."} + ) + required: Optional[bool] = dataclass_field( + default=None, + metadata={"description": "Indicates if the parameter is required."} + ) + hidden: Optional[bool] = dataclass_field( + default=None, + metadata={"description": "Indicates if the parameter is hidden."} + ) + minimum: Optional[float] = dataclass_field( + default=None, + metadata={"description": "Minimum value for the parameter."} + ) + maximum: Optional[float] = dataclass_field( + default=None, + metadata={"description": "Maximum value for the parameter."} + ) + pattern: Optional[str] = dataclass_field( + default=None, + metadata={"description": "Regular expression pattern for the parameter."} + ) + value_options: Optional[Any] = dataclass_field( + default=None, + metadata={"description": "Allowed values for the parameter."} + ) @dataclass_json @dataclass(frozen=True) class AppPanelGeneral: - title: Optional[str] = None - instructions: Optional[str] = None - help_text: Optional[str] = None + """General information about a capsule's App Panel.""" + title: Optional[str] = dataclass_field( + default=None, + metadata={"description": "Title of the App Panel."} + ) + instructions: Optional[str] = dataclass_field( + default=None, + metadata={"description": "Instructions for using the App Panel."} + ) + help_text: Optional[str] = dataclass_field( + default=None, + metadata={"description": "Help text for the App Panel."} + ) @dataclass_json @dataclass(frozen=True) class AppPanelDataAsset: - id: str - mount: str - name: str - kind: AppPanelDataAssetKind - accessible: bool - description: Optional[str] = None - help_text: Optional[str] = None + """Data asset parameter for the App Panel.""" + id: str = dataclass_field( + metadata={"description": "Unique identifier for the data asset."} + ) + mount: str = dataclass_field( + metadata={"description": "Mount path of the data asset within the capsule. " + "Use this mount path to replace the currently attached data asset with your own"} + ) + name: str = dataclass_field( + metadata={"description": "Display name of the data asset."} + ) + kind: AppPanelDataAssetKind = dataclass_field( + metadata={"description": "Kind of the data asset (internal, external, combined)."} + ) + accessible: bool = dataclass_field( + metadata={"description": "Indicates if the data asset is accessible to the user."} + ) + description: Optional[str] = dataclass_field( + default=None, + metadata={"description": "Optional description of the data asset parameter."} + ) + help_text: Optional[str] = dataclass_field( + default=None, + metadata={"description": "Optional help text for the data asset parameter."} + ) @dataclass_json @dataclass(frozen=True) class AppPanelResult: - file_name: str + """Selected result files to display once the computation is complete.""" + file_name: str = dataclass_field( + metadata={"description": "Name of the result file."} + ) @dataclass_json @dataclass(frozen=True) class AppPanelProcess: - name: str - categories: Optional[AppPanelCategories] = None - parameters: Optional[AppPanelParameters] = None + """Pipeline process name and its corresponding app panel (for pipelines of capsules only)""" + name: str = dataclass_field( + metadata={"description": "Name of the pipeline process."} + ) + categories: Optional[AppPanelCategories] = dataclass_field( + default=None, + metadata={"description": "Categories for the pipeline process's app panel parameters."} + ) + parameters: Optional[AppPanelParameters] = dataclass_field( + default=None, + metadata={"description": "Parameters for the pipeline process's app panel."} + ) @dataclass_json @dataclass(frozen=True) class AppPanel: - general: Optional[AppPanelGeneral] = None - data_assets: Optional[list[AppPanelDataAsset]] = None - categories: Optional[list[AppPanelCategories]] = None - parameters: Optional[list[AppPanelParameters]] = None - results: Optional[list[AppPanelResult]] = None - processes: Optional[list[AppPanelProcess]] = None - - -@dataclass_json -@dataclass(frozen=True) -class AppPanelParams: - version: Optional[int] = None + """App Panel configuration for a capsule or pipeline, including general info, data assets, + categories, parameters, and results. + """ + general: Optional[AppPanelGeneral] = dataclass_field( + default=None, + metadata={"description": "General information about the App Panel."} + ) + data_assets: Optional[list[AppPanelDataAsset]] = dataclass_field( + default=None, + metadata={"description": "List of data assets used in the App Panel."} + ) + categories: Optional[list[AppPanelCategories]] = dataclass_field( + default=None, + metadata={"description": "Categories for organizing App Panel parameters."} + ) + parameters: Optional[list[AppPanelParameters]] = dataclass_field( + default=None, + metadata={"description": "Parameters for the App Panel."} + ) + results: Optional[list[AppPanelResult]] = dataclass_field( + default=None, + metadata={"description": "Result files to display after computation."} + ) + processes: Optional[list[AppPanelProcess]] = dataclass_field( + default=None, + metadata={"description": "Pipeline processes and their App Panels."} + ) @dataclass @@ -321,6 +441,16 @@ def get_capsule(self, capsule_id: str) -> Capsule: return Capsule.from_dict(res.json()) + def delete_capsule(self, capsule_id: str): + """Delete a capsule permanently.""" + self.client.delete(f"capsules/{capsule_id}") + + def get_capsule_app_panel(self, capsule_id: str, version: Optional[int] = None) -> AppPanel: + """Retrieve app panel information for a specific capsule by its ID.""" + res = self.client.get(f"capsules/{capsule_id}/parameters", params={"version": version} if version else None) + + return AppPanel.from_dict(res.json()) + def list_computations(self, capsule_id: str) -> list[Computation]: """Get all computations associated with a specific capsule.""" res = self.client.get(f"capsules/{capsule_id}/computations") @@ -354,6 +484,13 @@ def detach_data_assets(self, capsule_id: str, data_assets: list[str]): json=data_assets, ) + def archive_capsule(self, capsule_id: str, archive: bool): + """Archive or unarchive a capsule to control its visibility and accessibility.""" + self.client.patch( + f"capsules/{capsule_id}/archive", + params={"archive": archive}, + ) + def search_capsules(self, search_params: CapsuleSearchParams) -> CapsuleSearchResults: """Search for capsules with filtering, sorting, and pagination options.""" @@ -374,20 +511,3 @@ def search_capsules_iterator(self, search_params: CapsuleSearchParams) -> Iterat return params["next_token"] = response.next_token - - def get_capsule_app_panel(self, capsule_id: str, version: Optional[AppPanelParams] = None) -> AppPanel: - """Retrieve app panel information for a specific capsule by its ID.""" - res = self.client.get(f"capsules/{capsule_id}/parameters", params=version.to_dict() if version else None) - - return AppPanel.from_dict(res.json()) - - def archive_capsule(self, capsule_id: str, archive: bool): - """Archive or unarchive a capsule to control its visibility and accessibility.""" - self.client.patch( - f"capsules/{capsule_id}/archive", - params={"archive": archive}, - ) - - def delete_capsule(self, capsule_id: str): - """Delete a capsule permanently.""" - self.client.delete(f"capsules/{capsule_id}") diff --git a/src/codeocean/client.py b/src/codeocean/client.py index ef4641e..5658c8c 100644 --- a/src/codeocean/client.py +++ b/src/codeocean/client.py @@ -37,7 +37,7 @@ class CodeOcean: agent_id: Optional[str] = None # Minimum server version required by this SDK - MIN_SERVER_VERSION = "3.6.0" + MIN_SERVER_VERSION = "3.9.0" def __post_init__(self): self.session = BaseUrlSession(base_url=f"{self.domain}/api/v1/") diff --git a/src/codeocean/custom_metadata.py b/src/codeocean/custom_metadata.py index 35abc14..397b282 100644 --- a/src/codeocean/custom_metadata.py +++ b/src/codeocean/custom_metadata.py @@ -1,6 +1,6 @@ from __future__ import annotations -from dataclasses import dataclass +from dataclasses import dataclass, field as dataclass_field from dataclasses_json import dataclass_json from typing import Optional, Union from requests_toolbelt.sessions import BaseUrlSession @@ -9,6 +9,7 @@ class CustomMetadataFieldType(StrEnum): + """ Type of the custom metadata field value. """ String = "string" Number = "number" Date = "date" @@ -17,28 +18,65 @@ class CustomMetadataFieldType(StrEnum): @dataclass_json @dataclass(frozen=True) class CustomMetadataFieldRange: - min: Optional[float] = None - max: Optional[float] = None + """ Range of valid values for a custom metadata field. """ + min: Optional[float] = dataclass_field( + default=None, + metadata={"description": "Minimum valid value"} + ) + max: Optional[float] = dataclass_field( + default=None, + metadata={"description": "Maximum valid value"} + ) @dataclass_json @dataclass(frozen=True) class CustomMetadataField: - name: str - type: CustomMetadataFieldType - range: Optional[CustomMetadataFieldRange] = None - allowed_values: Optional[Union[list[str], list[float]]] = None - multiple: Optional[bool] = None - units: Optional[str] = None - category: Optional[str] = None - required: Optional[bool] = None + """ Represents a custom metadata field in the Code Ocean platform. """ + name: str = dataclass_field( + metadata={"description": "Name of the custom metadata field"} + ) + type: CustomMetadataFieldType = dataclass_field( + metadata={"description": "Type of the custom metadata field value (string, number, date)"} + ) + range: Optional[CustomMetadataFieldRange] = dataclass_field( + default=None, + metadata={"description": "Range of valid values for the field"} + ) + allowed_values: Optional[Union[list[str], list[float]]] = dataclass_field( + default=None, + metadata={"description": "Allowed values for the field (item type according to field type)"} + ) + multiple: Optional[bool] = dataclass_field( + default=None, + metadata={"description": "Whether multiple values are allowed"} + ) + units: Optional[str] = dataclass_field( + default=None, + metadata={"description": "Units of the field value"} + ) + category: Optional[str] = dataclass_field( + default=None, + metadata={"description": "Category of the field"} + ) + required: Optional[bool] = dataclass_field( + default=None, + metadata={"description": "Whether the field is required"} + ) @dataclass_json @dataclass(frozen=True) class CustomMetadata: - fields: Optional[list[CustomMetadataField]] = None - categories: Optional[list[str]] = None + """ Represents the custom metadata schema in the Code Ocean platform. """ + fields: Optional[list[CustomMetadataField]] = dataclass_field( + default=None, + metadata={"description": "List of custom metadata fields"} + ) + categories: Optional[list[str]] = dataclass_field( + default=None, + metadata={"description": "List of categories for custom metadata fields"} + ) @dataclass @@ -48,7 +86,7 @@ class CustomMetadataSchema: client: BaseUrlSession def get_custom_metadata(self) -> CustomMetadata: - """Retrieve metadata for a specific capsule by its ID.""" + """Retrieve the Code Ocean deployment's custom metadata schema.""" res = self.client.get("custom_metadata") return CustomMetadata.from_dict(res.json()) From 3ae35d5e75e660ebd30f7bf44b52a0a5fd427c02 Mon Sep 17 00:00:00 2001 From: jake-valsamis Date: Sun, 31 Aug 2025 10:23:32 -0400 Subject: [PATCH 3/3] fixes and formatting --- src/codeocean/capsule.py | 15 ++++++++++----- src/codeocean/custom_metadata.py | 4 ++++ 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/codeocean/capsule.py b/src/codeocean/capsule.py index 6cf4b4d..726ed1f 100644 --- a/src/codeocean/capsule.py +++ b/src/codeocean/capsule.py @@ -2,7 +2,7 @@ from dataclasses import dataclass, field as dataclass_field from dataclasses_json import dataclass_json -from typing import Optional, Iterator, Any +from typing import Optional, Iterator from requests_toolbelt.sessions import BaseUrlSession from codeocean.components import Ownership, SortOrder, SearchFilter, Permissions @@ -34,7 +34,6 @@ class AppPanelDataAssetKind(StrEnum): - 'Combined' → Data containing multiple external data assets. In pipelines, a data asset can only be replaced with one of the same kind. - """ Internal = "internal" @@ -245,7 +244,7 @@ class CapsuleSearchResults: @dataclass_json @dataclass(frozen=True) class AppPanelCategories: - """ Categories for a capsule's App Panel parameters.""" + """Categories for a capsule's App Panel parameters.""" id: str = dataclass_field( metadata={"description": "Unique identifier for the category."}, @@ -267,6 +266,7 @@ class AppPanelCategories: @dataclass(frozen=True) class AppPanelParameters: """Parameters for a capsule's App Panel.""" + name: str = dataclass_field( metadata={"description": "Parameter label/display name."} ) @@ -317,7 +317,7 @@ class AppPanelParameters: default=None, metadata={"description": "Regular expression pattern for the parameter."} ) - value_options: Optional[Any] = dataclass_field( + value_options: Optional[list[str]] = dataclass_field( default=None, metadata={"description": "Allowed values for the parameter."} ) @@ -327,6 +327,7 @@ class AppPanelParameters: @dataclass(frozen=True) class AppPanelGeneral: """General information about a capsule's App Panel.""" + title: Optional[str] = dataclass_field( default=None, metadata={"description": "Title of the App Panel."} @@ -345,6 +346,7 @@ class AppPanelGeneral: @dataclass(frozen=True) class AppPanelDataAsset: """Data asset parameter for the App Panel.""" + id: str = dataclass_field( metadata={"description": "Unique identifier for the data asset."} ) @@ -375,6 +377,7 @@ class AppPanelDataAsset: @dataclass(frozen=True) class AppPanelResult: """Selected result files to display once the computation is complete.""" + file_name: str = dataclass_field( metadata={"description": "Name of the result file."} ) @@ -384,6 +387,7 @@ class AppPanelResult: @dataclass(frozen=True) class AppPanelProcess: """Pipeline process name and its corresponding app panel (for pipelines of capsules only)""" + name: str = dataclass_field( metadata={"description": "Name of the pipeline process."} ) @@ -403,6 +407,7 @@ class AppPanel: """App Panel configuration for a capsule or pipeline, including general info, data assets, categories, parameters, and results. """ + general: Optional[AppPanelGeneral] = dataclass_field( default=None, metadata={"description": "General information about the App Panel."} @@ -447,7 +452,7 @@ def delete_capsule(self, capsule_id: str): def get_capsule_app_panel(self, capsule_id: str, version: Optional[int] = None) -> AppPanel: """Retrieve app panel information for a specific capsule by its ID.""" - res = self.client.get(f"capsules/{capsule_id}/parameters", params={"version": version} if version else None) + res = self.client.get(f"capsules/{capsule_id}/app_panel", params={"version": version} if version else None) return AppPanel.from_dict(res.json()) diff --git a/src/codeocean/custom_metadata.py b/src/codeocean/custom_metadata.py index 397b282..238747a 100644 --- a/src/codeocean/custom_metadata.py +++ b/src/codeocean/custom_metadata.py @@ -10,6 +10,7 @@ class CustomMetadataFieldType(StrEnum): """ Type of the custom metadata field value. """ + String = "string" Number = "number" Date = "date" @@ -19,6 +20,7 @@ class CustomMetadataFieldType(StrEnum): @dataclass(frozen=True) class CustomMetadataFieldRange: """ Range of valid values for a custom metadata field. """ + min: Optional[float] = dataclass_field( default=None, metadata={"description": "Minimum valid value"} @@ -33,6 +35,7 @@ class CustomMetadataFieldRange: @dataclass(frozen=True) class CustomMetadataField: """ Represents a custom metadata field in the Code Ocean platform. """ + name: str = dataclass_field( metadata={"description": "Name of the custom metadata field"} ) @@ -69,6 +72,7 @@ class CustomMetadataField: @dataclass(frozen=True) class CustomMetadata: """ Represents the custom metadata schema in the Code Ocean platform. """ + fields: Optional[list[CustomMetadataField]] = dataclass_field( default=None, metadata={"description": "List of custom metadata fields"}