diff --git a/sdks/sandbox/python/src/opensandbox/adapters/converter/sandbox_model_converter.py b/sdks/sandbox/python/src/opensandbox/adapters/converter/sandbox_model_converter.py index a25ddd698..e0129fcb4 100644 --- a/sdks/sandbox/python/src/opensandbox/adapters/converter/sandbox_model_converter.py +++ b/sdks/sandbox/python/src/opensandbox/adapters/converter/sandbox_model_converter.py @@ -128,11 +128,11 @@ def to_api_volume(volume: Volume): from opensandbox.api.lifecycle.types import UNSET api_host = UNSET - if volume.host is not None: + if volume.host is not None and not isinstance(volume.host, Unset): api_host = ApiHost(path=volume.host.path) api_pvc = UNSET - if volume.pvc is not None: + if volume.pvc is not None and not isinstance(volume.pvc, Unset): api_pvc = ApiPVC( claim_name=volume.pvc.claim_name, create_if_not_exists=volume.pvc.create_if_not_exists, @@ -143,7 +143,12 @@ def to_api_volume(volume: Volume): ) api_ossfs = UNSET - if volume.ossfs is not None and volume.ossfs.access_key_id is not None and volume.ossfs.access_key_secret is not None: + if ( + volume.ossfs is not None + and not isinstance(volume.ossfs, Unset) + and volume.ossfs.access_key_id is not None + and volume.ossfs.access_key_secret is not None + ): api_ossfs = ApiOSSFS( bucket=volume.ossfs.bucket, endpoint=volume.ossfs.endpoint, @@ -154,7 +159,7 @@ def to_api_volume(volume: Volume): ) api_sub_path = UNSET - if volume.sub_path is not None: + if volume.sub_path is not None and not isinstance(volume.sub_path, Unset): api_sub_path = volume.sub_path return ApiVolume( diff --git a/sdks/sandbox/python/tests/test_converters_and_error_handling.py b/sdks/sandbox/python/tests/test_converters_and_error_handling.py index cab7a4c33..6912ad1a1 100644 --- a/sdks/sandbox/python/tests/test_converters_and_error_handling.py +++ b/sdks/sandbox/python/tests/test_converters_and_error_handling.py @@ -684,6 +684,66 @@ def test_sandbox_model_converter_snapshot_restore_request() -> None: assert "image" not in dumped assert "entrypoint" not in dumped +def test_sandbox_model_converter_to_api_volume_skips_unset_fields() -> None: + from opensandbox.api.lifecycle.types import UNSET + from opensandbox.models.sandboxes import Volume + + # Inject UNSET into backend fields (bypassing Pydantic validation) to + # simulate a domain Volume carrying Unset values from an API round-trip. + volume = Volume.model_construct( + name="workdir", + mount_path="/mnt/work", + read_only=False, + host=UNSET, + pvc=UNSET, + ossfs=UNSET, + sub_path=UNSET, + ) + + api_volume = SandboxModelConverter.to_api_volume(volume) + dumped = api_volume.to_dict() + assert dumped == {"name": "workdir", "mountPath": "/mnt/work", "readOnly": False} + assert "host" not in dumped + assert "pvc" not in dumped + assert "ossfs" not in dumped + assert "subPath" not in dumped + +def test_sandbox_model_converter_to_api_volume_maps_backends() -> None: + from opensandbox.models.sandboxes import OSSFS, PVC, Host, Volume + + volume = Volume( + name="workdir", + host=Host(path="/data/opensandbox"), + mount_path="/mnt/work", + sub_path="sub", + ) + dumped = SandboxModelConverter.to_api_volume(volume).to_dict() + assert dumped["host"] == {"path": "/data/opensandbox"} + assert dumped["subPath"] == "sub" + assert "pvc" not in dumped + assert "ossfs" not in dumped + + pvc_volume = Volume( + name="models", + pvc=PVC(claim_name="shared-models-pvc"), + mount_path="/mnt/models", + read_only=True, + ) + pvc_dumped = SandboxModelConverter.to_api_volume(pvc_volume).to_dict() + assert pvc_dumped["pvc"]["claimName"] == "shared-models-pvc" + assert pvc_dumped["readOnly"] is True + + ossfs_volume = Volume( + name="oss", ossfs=OSSFS( + bucket="b", + endpoint="oss-cn-hangzhou.aliyuncs.com", + accessKeyId="ak", + accessKeySecret="sk", + ), + mount_path="/mnt/oss", + ) + ossfs_dumped = SandboxModelConverter.to_api_volume(ossfs_volume).to_dict() + assert ossfs_dumped["ossfs"]["bucket"] == "b" def test_sandbox_model_converter_maps_platform_from_create_response() -> None: from opensandbox.api.lifecycle.models.create_sandbox_response import (