From dc353a45b4a9b616b49eb376479499411e02e39a Mon Sep 17 00:00:00 2001 From: jtyoung84 <104453205+jtyoung84@users.noreply.github.com> Date: Wed, 6 Dec 2023 18:55:10 -0800 Subject: [PATCH] feat: updates api to v2.15 compatibility --- src/aind_codeocean_api/codeocean.py | 194 ++-------- src/aind_codeocean_api/models/__init__.py | 1 + .../models/basic_request.py | 31 ++ .../models/computations_requests.py | 46 +++ .../models/data_assets_requests.py | 85 ++++ tests/test_codeocean_requests.py | 362 +++++++++++++----- 6 files changed, 457 insertions(+), 262 deletions(-) create mode 100644 src/aind_codeocean_api/models/__init__.py create mode 100644 src/aind_codeocean_api/models/basic_request.py create mode 100644 src/aind_codeocean_api/models/computations_requests.py create mode 100644 src/aind_codeocean_api/models/data_assets_requests.py diff --git a/src/aind_codeocean_api/codeocean.py b/src/aind_codeocean_api/codeocean.py index 4d167b3..35e0e0d 100644 --- a/src/aind_codeocean_api/codeocean.py +++ b/src/aind_codeocean_api/codeocean.py @@ -4,11 +4,15 @@ import logging from enum import Enum from inspect import signature -from typing import Dict, List, Optional +from typing import Dict, List, Optional, Union import requests from aind_codeocean_api.credentials import CodeOceanCredentials +from aind_codeocean_api.models.computations_requests import RunCapsuleRequest +from aind_codeocean_api.models.data_assets_requests import ( + CreateDataAssetRequest, +) class CodeOceanClient: @@ -278,132 +282,28 @@ def search_all_data_assets( ) return all_response - def register_data_asset( - self, - asset_name: str, - mount: str, - bucket: str, - prefix: str, - access_key_id: Optional[str] = None, - secret_access_key: Optional[str] = None, - tags: Optional[List[str]] = None, - asset_description: Optional[str] = "", - keep_on_external_storage: Optional[bool] = True, - index_data: Optional[bool] = True, - custom_metadata: Optional[dict] = None, + def create_data_asset( + self, request: Union[dict, CreateDataAssetRequest] ) -> requests.models.Response: """ + Create a data asset. The request can either be a CreateDataAssetRequest + class or a dictionary with the same shape. It's possible to create a + data asset from: aws bucket/prefix, gcp bucket/prefix, computation id. + More details about the other parameters can be found in the + CreateDataAssetRequest documentation. Parameters - --------------- - asset_name : string - Name of the asset - mount : string - Mount point - bucket : string - Bucket name. Currently only aws buckets are allowed. - prefix : string - The object prefix in the bucket - access_key_id : Optional[str] - AWS access key. It's not necessary for public buckets. - Default None (not provided). - secret_access_key : Optional[str] - AWS secret access key. It's not necessary for public buckets. - Default None (not provided). - tags : Optional[List[str]] - A list of tags to attach to the data asset. - Default None (empty list). - asset_description : Optional[str] - A description of the data asset. Default blanks. - keep_on_external_storage : Optional[bool] - Keep data asset on external storage. Defaults to True. - index_data : Optional[bool] - Whether to index the data asset. Defaults to True. - custom_metadata : Optional[dict] - What key:value metadata tags to apply to the asset. - Returns - --------------- - requests.models.Response - """ - - tags_to_attach = [] if tags is None else tags - json_data = { - self._Fields.NAME.value: asset_name, - self._Fields.DESCRIPTION.value: asset_description, - self._Fields.MOUNT.value: mount, - self._Fields.TAGS.value: tags_to_attach, - self._Fields.SOURCE.value: { - self._Fields.AWS.value: { - self._Fields.BUCKET.value: bucket, - self._Fields.PREFIX.value: prefix, - self._Fields.KEEP_ON_EXTERNAL_STORAGE.value: ( - keep_on_external_storage - ), - self._Fields.INDEX_DATA.value: index_data, - } - }, - self._Fields.CUSTOM_METADATA.value: custom_metadata, - } - - if access_key_id and secret_access_key: - json_data[self._Fields.SOURCE.value][self._Fields.AWS.value][ - self._Fields.ACCESS_KEY_ID.value - ] = access_key_id - json_data[self._Fields.SOURCE.value][self._Fields.AWS.value][ - self._Fields.SECRET_ACCESS_KEY.value - ] = secret_access_key - - response = requests.post( - self.asset_url, json=json_data, auth=(self.token, "") - ) - return response + ---------- + request : Union[dict, CreateDataAssetRequest] - def register_result_as_data_asset( - self, - computation_id: str, - asset_name: str, - asset_description: Optional[str] = "", - mount: Optional[str] = None, - tags: Optional[List] = None, - custom_metadata: Optional[dict] = None, - ) -> requests.models.Response: - """ - Parameters - --------------- - computation_id : string - Computation id - asset_name : string - Name of the data asset. - asset_description : Optional[str] - A description of the data asset. Default blanks. - mount : string - Mount point. Default None (Mount point equal to the asset name) - tags : Optional[List[str]] - A list of tags to attach to the data asset. - Default None (empty list). - custom_metadata : Optional[dict] - What key:value metadata tags to apply to the asset. Returns - --------------- + ------- requests.models.Response - """ - tags_to_attach = [] if tags is None else tags - - if mount is None: - mount = asset_name - - json_data = { - self._Fields.NAME.value: asset_name, - self._Fields.DESCRIPTION.value: asset_description, - self._Fields.MOUNT.value: mount, - self._Fields.TAGS.value: tags_to_attach, - self._Fields.SOURCE.value: { - self._Fields.COMPUTATION.value: { - self._Fields.ID.value: computation_id - } - }, - self._Fields.CUSTOM_METADATA.value: custom_metadata, - } + """ + if isinstance(request, dict): + json_data = request + else: + json_data = json.loads(request.json_string) response = requests.post( self.asset_url, json=json_data, auth=(self.token, "") @@ -463,56 +363,30 @@ def update_data_asset( return response def run_capsule( - self, - capsule_id: str, - data_assets: List[Dict], - version: Optional[int] = None, - parameters: Optional[List] = None, + self, request: Union[dict, RunCapsuleRequest] ) -> requests.models.Response: """ - This will run a capsule/pipeline using a POST request to Code Ocean - API. - + Run a capsule or pipeline. The request can either be a + RunCapsuleRequest class or a dictionary with the same shape. More + details about the other parameters can be found in the + RunCapsuleRequest documentation. Parameters - --------------- - capsule_id : string - ID of the capsule - data_assets : List[dict] - List of dictionaries containing the following keys: 'id' which - refers to the data asset id in Code Ocean and 'mount' which - refers to the data asset mount folder. - version : Optional[int] - Capsule version to be run. Defaults to None. - parameters : List - Parameters given to the capsule. Default None which means - the capsule runs with no parameters. - The parameters should match in order to the parameters given in the - capsule, e.g. - 'parameters': [ - 'input_folder', - 'output_folder', - 'bucket_name' - ] - where position one refers to the parameter #1 ('input_folder'), - parameter #2 ('output_folder'), and parameter #3 ('bucket_name') + ---------- + request : Union[dict, RunCapsuleRequest] Returns - --------------- + ------- requests.models.Response - """ - data = { - self._Fields.CAPSULE_ID.value: capsule_id, - self._Fields.DATA_ASSETS.value: data_assets, - } + """ - if parameters: - data[self._Fields.PARAMETERS.value] = parameters - if version: - data[self._Fields.VERSION.value] = version + if isinstance(request, dict): + json_data = request + else: + json_data = json.loads(request.json_string) response = requests.post( - url=self.computation_url, json=data, auth=(self.token, "") + url=self.computation_url, json=json_data, auth=(self.token, "") ) return response diff --git a/src/aind_codeocean_api/models/__init__.py b/src/aind_codeocean_api/models/__init__.py new file mode 100644 index 0000000..1434e9e --- /dev/null +++ b/src/aind_codeocean_api/models/__init__.py @@ -0,0 +1 @@ +"""Package for models used by api""" diff --git a/src/aind_codeocean_api/models/basic_request.py b/src/aind_codeocean_api/models/basic_request.py new file mode 100644 index 0000000..f65fe35 --- /dev/null +++ b/src/aind_codeocean_api/models/basic_request.py @@ -0,0 +1,31 @@ +"""Module for common methods used by all subclasses""" + +import json +from dataclasses import asdict, dataclass + + +@dataclass +class BasicRequest: + """Class for basic request methods""" + + def __clean_nones(self, value): + """ + Recursively remove all None values from dictionaries and lists, and + returns the result as a new dictionary or list. Modified from + https://stackoverflow.com/a/60124334 + """ + if isinstance(value, list): + return [self.__clean_nones(x) for x in value if x is not None] + elif isinstance(value, dict): + return { + key: self.__clean_nones(val) + for key, val in value.items() + if val is not None + } + else: + return value + + @property + def json_string(self) -> str: + """Render dataclass as json object with null values removed.""" + return json.dumps(self.__clean_nones(asdict(self))) diff --git a/src/aind_codeocean_api/models/computations_requests.py b/src/aind_codeocean_api/models/computations_requests.py new file mode 100644 index 0000000..747d6a9 --- /dev/null +++ b/src/aind_codeocean_api/models/computations_requests.py @@ -0,0 +1,46 @@ +"""Module for class to send a request to the computation endpoint""" + +from dataclasses import dataclass +from typing import List, Optional + +from aind_codeocean_api.models.basic_request import BasicRequest + + +@dataclass +class ComputationDataAsset: + """The data asset input has a different structure than in other classes""" + + id: str + mount: str + + +@dataclass +class ComputationNamedParameter: + """Named parameters can be input into a request, but look like: + {"param_name": "key", "value": "value"}""" + + param_name: str + value: str + + +@dataclass +class ComputationProcess: + """Computation process""" + + name: str + parameters: Optional[List[str]] = None + named_parameters: Optional[List[ComputationNamedParameter]] = None + + +@dataclass +class RunCapsuleRequest(BasicRequest): + """Request used to run a capsule or a pipeline.""" + + capsule_id: Optional[str] = None + pipeline_id: Optional[str] = None + version: Optional[int] = None + resume_run_id: Optional[str] = None + data_assets: Optional[List[ComputationDataAsset]] = None + parameters: Optional[List[str]] = None + named_parameters: Optional[List[ComputationNamedParameter]] = None + processes: Optional[List[ComputationProcess]] = None diff --git a/src/aind_codeocean_api/models/data_assets_requests.py b/src/aind_codeocean_api/models/data_assets_requests.py new file mode 100644 index 0000000..071cee9 --- /dev/null +++ b/src/aind_codeocean_api/models/data_assets_requests.py @@ -0,0 +1,85 @@ +"""Module for class to send a request to the data assets endpoint""" + +from dataclasses import dataclass +from typing import Any, Dict, List, Optional + +from aind_codeocean_api.models.basic_request import BasicRequest + + +class Sources: + """Currently, three different sources can be defined. AWS, GCP, and a + Computation from a capsule""" + + @dataclass + class AWS: + """Fields required to create a data asset from aws""" + + bucket: str + prefix: Optional[str] = None + keep_on_external_storage: Optional[bool] = None + public: Optional[bool] = None + + @dataclass + class GCP: + """Fields required to create a data asset from gcp""" + + bucket: str + prefix: Optional[str] = None + client_id: Optional[str] = None + client_secret: Optional[str] = None + + @dataclass + class Computation: + """Fields required to create a data asset from a computation""" + + id: str + path: Optional[str] = None + + +@dataclass +class Source: + """Source needs to be one of aws, gcp, or computation""" + + aws: Optional[Sources.AWS] = None + gcp: Optional[Sources.GCP] = None + computation: Optional[Sources.Computation] = None + + def __post_init__(self): + """Basic validation on fields to verify at least one is set""" + nones = [ + a for a in [self.aws, self.gcp, self.computation] if a is None + ] + if len(nones) == 3: + raise Exception("At least one source is required") + + +class Targets: + """Targets to send data to""" + + @dataclass + class AWS: + """AWS bucket and prefix""" + + bucket: str + prefix: Optional[str] = None + + +@dataclass +class Target: + """Target field needs to be aws with potential to expand in the future""" + + aws: Targets.AWS + + +@dataclass +class CreateDataAssetRequest(BasicRequest): + """Request used to create a data asset from an external source or a + capsule computation.""" + + name: str + tags: List[str] + mount: str + description: Optional[str] = None + source: Optional[Source] = None + target: Optional[Target] = None + custom_metadata: Optional[Dict[str, Any]] = None diff --git a/tests/test_codeocean_requests.py b/tests/test_codeocean_requests.py index 5bcfda5..8e1ac10 100644 --- a/tests/test_codeocean_requests.py +++ b/tests/test_codeocean_requests.py @@ -9,6 +9,15 @@ from aind_codeocean_api.codeocean import CodeOceanClient from aind_codeocean_api.credentials import CodeOceanCredentials +from aind_codeocean_api.models.computations_requests import ( + ComputationDataAsset, + RunCapsuleRequest, +) +from aind_codeocean_api.models.data_assets_requests import ( + CreateDataAssetRequest, + Source, + Sources, +) class MockResponse: @@ -84,8 +93,16 @@ def request_put_response(url: str, json: dict) -> MockResponse: else: return request_put_response + def test_source_raise_exception(self): + """Tests and exception is raised if Source is missing fields""" + with self.assertRaises(Exception) as e: + Source() + self.assertEqual( + "Exception('At least one source is required')", repr(e.exception) + ) + @mock.patch("requests.post") - def test_register_data_asset( + def test_register_aws_data_asset( self, mock_api_post: unittest.mock.MagicMock ) -> None: """Tests the response of registering a data asset""" @@ -93,32 +110,104 @@ def test_register_data_asset( mount = "MOUNT_NAME" bucket = "BUCKET_NAME" prefix = "PREFIX_NAME" - access_key_id = "AWS_ACCESS_KEY" - secret_access_key = "AWS_SECRET_KEY" + tags = ["tag1", "tag2"] + custom_metadata = {"modality": "ecephys", "subject id": "567890"} + aws_source = Sources.AWS( + bucket=bucket, + prefix=prefix, + keep_on_external_storage=True, + public=True, + ) + source = Source(aws=aws_source) + create_data_asset_request = CreateDataAssetRequest( + name=asset_name, + tags=tags, + mount=mount, + source=source, + custom_metadata=custom_metadata, + ) - input_json_data = { - "name": asset_name, + input_json_data = json.loads(create_data_asset_request.json_string) + + def map_to_success_message(input_json: dict) -> dict: + """Map to a success message""" + success_message = { + "created": 1641420832, + "description": "", + "files": 0, + "id": "44ec16c3-cb5a-45f5-93d1-cba8be800c24", + "lastUsed": 0, + "name": input_json["name"], + "sizeInBytes": 0, + "state": "DATA_ASSET_STATE_DRAFT", + "tags": input_json["tags"], + "type": "DATA_ASSET_TYPE_DATASET", + } + return success_message + + expected_request_response = { + "created": 1641420832, "description": "", - "mount": mount, - "tags": [], - "source": { - "aws": { - "bucket": bucket, - "prefix": prefix, - "keep_on_external_storage": True, - "index_data": True, - "access_key_id": access_key_id, - "secret_access_key": secret_access_key, - } - }, - "custom_metadata": None, + "files": 0, + "id": "44ec16c3-cb5a-45f5-93d1-cba8be800c24", + "lastUsed": 0, + "name": input_json_data["name"], + "sizeInBytes": 0, + "state": "DATA_ASSET_STATE_DRAFT", + "tags": input_json_data["tags"], + "type": "DATA_ASSET_TYPE_DATASET", } + mocked_success_post = self.mock_success_response( + map_to_success_message, req_type="post" + ) + mock_api_post.return_value = mocked_success_post(json=input_json_data) + + # Input request as dict + response = self.co_client.create_data_asset(request=input_json_data) + self.assertEqual(response.content, expected_request_response) + self.assertEqual(response.status_code, 200) + + # Input request as class + response2 = self.co_client.create_data_asset( + request=create_data_asset_request + ) + self.assertEqual(response2.content, expected_request_response) + self.assertEqual(response2.status_code, 200) + + @mock.patch("requests.post") + def test_register_gcp_data_asset( + self, mock_api_post: unittest.mock.MagicMock + ) -> None: + """Tests the response of registering a data asset""" + asset_name = "ASSET_NAME" + mount = "MOUNT_NAME" + bucket = "BUCKET_NAME" + prefix = "PREFIX_NAME" + tags = ["tag1", "tag2"] + custom_metadata = {"modality": "ecephys", "subject id": "567890"} + gcp_source = Sources.GCP( + bucket=bucket, + prefix=prefix, + client_id="GCP_CLIENT_ID", + client_secret="GCP_SECRET", + ) + source = Source(gcp=gcp_source) + create_data_asset_request = CreateDataAssetRequest( + name=asset_name, + tags=tags, + mount=mount, + source=source, + custom_metadata=custom_metadata, + ) + + input_json_data = json.loads(create_data_asset_request.json_string) + def map_to_success_message(input_json: dict) -> dict: """Map to a success message""" success_message = { "created": 1641420832, - "description": input_json["description"], + "description": "", "files": 0, "id": "44ec16c3-cb5a-45f5-93d1-cba8be800c24", "lastUsed": 0, @@ -132,7 +221,7 @@ def map_to_success_message(input_json: dict) -> dict: expected_request_response = { "created": 1641420832, - "description": input_json_data["description"], + "description": "", "files": 0, "id": "44ec16c3-cb5a-45f5-93d1-cba8be800c24", "lastUsed": 0, @@ -148,17 +237,86 @@ def map_to_success_message(input_json: dict) -> dict: ) mock_api_post.return_value = mocked_success_post(json=input_json_data) - response = self.co_client.register_data_asset( - asset_name=asset_name, + # Input request as dict + response = self.co_client.create_data_asset(request=input_json_data) + self.assertEqual(response.content, expected_request_response) + self.assertEqual(response.status_code, 200) + + # Input request as class + response2 = self.co_client.create_data_asset( + request=create_data_asset_request + ) + self.assertEqual(response2.content, expected_request_response) + self.assertEqual(response2.status_code, 200) + + @mock.patch("requests.post") + def test_register_computation_data_asset( + self, mock_api_post: unittest.mock.MagicMock + ) -> None: + """Tests the response of registering a data asset""" + asset_name = "ASSET_NAME" + mount = "MOUNT_NAME" + computation_id = "12345-abcdef" + tags = ["tag1", "tag2"] + custom_metadata = {"modality": "ecephys", "subject id": "567890"} + computation_source = Sources.Computation(id=computation_id) + source = Source(computation=computation_source) + create_data_asset_request = CreateDataAssetRequest( + name=asset_name, + tags=tags, mount=mount, - bucket=bucket, - prefix=prefix, - access_key_id=access_key_id, - secret_access_key=secret_access_key, + source=source, + custom_metadata=custom_metadata, + ) + + input_json_data = json.loads(create_data_asset_request.json_string) + + def map_to_success_message(input_json: dict) -> dict: + """Map to a success message""" + success_message = { + "created": 1641420832, + "description": "", + "files": 0, + "id": "44ec16c3-cb5a-45f5-93d1-cba8be800c24", + "lastUsed": 0, + "name": input_json["name"], + "sizeInBytes": 0, + "state": "DATA_ASSET_STATE_DRAFT", + "tags": input_json["tags"], + "type": "DATA_ASSET_TYPE_DATASET", + } + return success_message + + expected_request_response = { + "created": 1641420832, + "description": "", + "files": 0, + "id": "44ec16c3-cb5a-45f5-93d1-cba8be800c24", + "lastUsed": 0, + "name": input_json_data["name"], + "sizeInBytes": 0, + "state": "DATA_ASSET_STATE_DRAFT", + "tags": input_json_data["tags"], + "type": "DATA_ASSET_TYPE_DATASET", + } + + mocked_success_post = self.mock_success_response( + map_to_success_message, req_type="post" ) + mock_api_post.return_value = mocked_success_post(json=input_json_data) + + # Input request as dict + response = self.co_client.create_data_asset(request=input_json_data) self.assertEqual(response.content, expected_request_response) self.assertEqual(response.status_code, 200) + # Input request as class + response2 = self.co_client.create_data_asset( + request=create_data_asset_request + ) + self.assertEqual(response2.content, expected_request_response) + self.assertEqual(response2.status_code, 200) + def test_create_from_credentials(self): """Tests that the client can be constructed from a CodeOceanCredentials object""" @@ -430,47 +588,107 @@ def map_to_success_message(url: str, json: dict) -> dict: @mock.patch("requests.post") def test_run_capsule(self, mock_api_post: unittest.mock.MagicMock) -> None: - """Tests run_capsule method.""" + """Tests run_capsule with capsule id method.""" def map_to_success_message(input_json: dict) -> dict: """Map to a success message""" - + input_id = input_json.get("capsule_id") success_message = { "created": 1646943238, "has_results": False, - "id": input_json["capsule_id"], + "id": input_id, "name": "Run 6943238", "run_time": 1, "state": "initializing", } return success_message - example_capsule_id = "648473aa-791e-4372-bd25-205cc587ec56" - input_json_data = {"capsule_id": example_capsule_id, "data_assets": []} + capsule_id = "xyz-890" + data_assets = [ + ComputationDataAsset(id="12345-abcdef", mount="SOME_MOUNT") + ] + run_capsule_request = RunCapsuleRequest( + capsule_id=capsule_id, data_assets=data_assets + ) + run_capsule_request_json = json.loads(run_capsule_request.json_string) + mocked_success_post = self.mock_success_response( + map_to_success_message, req_type="post" + ) + + mock_api_post.return_value = mocked_success_post( + json=run_capsule_request_json + ) + + run_capsule_response1 = self.co_client.run_capsule( + request=run_capsule_request_json + ) + run_capsule_response2 = self.co_client.run_capsule( + request=run_capsule_request + ) + expected_capsule_response = { + "created": 1646943238, + "has_results": False, + "id": capsule_id, + "name": "Run 6943238", + "run_time": 1, + "state": "initializing", + } + self.assertEqual( + expected_capsule_response, run_capsule_response1.content + ) + self.assertEqual(run_capsule_response1.status_code, 200) + self.assertEqual( + expected_capsule_response, run_capsule_response2.content + ) + self.assertEqual(run_capsule_response2.status_code, 200) + + @mock.patch("requests.post") + def test_run_pipeline( + self, mock_api_post: unittest.mock.MagicMock + ) -> None: + """Tests run_capsule with pipeline id method.""" + + def map_to_success_message(input_json: dict) -> dict: + """Map to a success message""" + input_id = input_json.get("pipeline_id") + success_message = { + "created": 1646943238, + "has_results": False, + "id": input_id, + "name": "Run 6943238", + "run_time": 1, + "state": "initializing", + } + return success_message + pipeline_id = "p-54524-adfnjkdbf" + run_pipeline_request = RunCapsuleRequest(pipeline_id=pipeline_id) + run_pipeline_request_json = json.loads( + run_pipeline_request.json_string + ) mocked_success_post = self.mock_success_response( map_to_success_message, req_type="post" ) - mock_api_post.return_value = mocked_success_post(json=input_json_data) - response = self.co_client.run_capsule( - capsule_id=example_capsule_id, data_assets=[], parameters=["FOO"] + mock_api_post.return_value = mocked_success_post( + json=run_pipeline_request_json ) - response2 = self.co_client.run_capsule( - capsule_id=example_capsule_id, data_assets=[], version=1 + + run_pipeline_response1 = self.co_client.run_capsule( + request=run_pipeline_request_json ) - expected_response = { + expected_pipeline_response = { "created": 1646943238, "has_results": False, - "id": example_capsule_id, + "id": pipeline_id, "name": "Run 6943238", "run_time": 1, "state": "initializing", } - self.assertEqual(expected_response, response.content) - self.assertEqual(response.status_code, 200) - self.assertEqual(expected_response, response2.content) - self.assertEqual(response2.status_code, 200) + self.assertEqual( + expected_pipeline_response, run_pipeline_response1.content + ) + self.assertEqual(run_pipeline_response1.status_code, 200) @mock.patch("requests.get") def test_get_capsule(self, mock_api_get: unittest.mock.MagicMock) -> None: @@ -701,66 +919,6 @@ def map_to_success_message(_) -> dict: self.assertEqual(response.content, expected_response) self.assertEqual(response.status_code, 200) - @mock.patch("requests.post") - def test_register_result_as_data_asset( - self, mock_api_post: unittest.mock.MagicMock - ) -> None: - """Tests the response of registering a result as data asset""" - asset_name = "ASSET_NAME" - mount = "MOUNT_NAME" - - input_json_data = { - "name": asset_name, - "description": "", - "mount": mount, - "tags": [], - "custom_metadata": None, - "source": { - "computation": {"id": "44ec16c3-cb5a-4000-93d1-cba8be800c00"} - }, - } - - def map_to_success_message(input_json: dict) -> dict: - """Map to a success message""" - tags_to_attach = ( - None if not len(input_json["tags"]) else input_json["tags"] - ) - - success_message = { - "created": 1668529000, - "description": "", - "id": "cefd51ae-35b1-45b9-b82b-2de14f000z000", - "last_used": 0, - "name": "", - "state": "draft", - "tags": tags_to_attach, - "type": "result", - } - return success_message - - expected_request_response = { - "created": 1668529000, - "description": "", - "id": "cefd51ae-35b1-45b9-b82b-2de14f000z000", - "last_used": 0, - "name": "", - "state": "draft", - "tags": None, - "type": "result", - } - - mocked_success_post = self.mock_success_response( - map_to_success_message, req_type="post" - ) - mock_api_post.return_value = mocked_success_post(json=input_json_data) - - computation_id = "83dc2b36-b2e0-459c-8d9e-9381b000w00e0" - response = self.co_client.register_result_as_data_asset( - computation_id=computation_id, asset_name=asset_name - ) - self.assertEqual(response.content, expected_request_response) - self.assertEqual(response.status_code, 200) - @mock.patch("requests.post") def test_update_permissions( self, mock_api_post: unittest.mock.MagicMock