diff --git a/sdks/sandbox/python/src/opensandbox/adapters/converter/exception_converter.py b/sdks/sandbox/python/src/opensandbox/adapters/converter/exception_converter.py index 88e43f7d..0a4ac817 100644 --- a/sdks/sandbox/python/src/opensandbox/adapters/converter/exception_converter.py +++ b/sdks/sandbox/python/src/opensandbox/adapters/converter/exception_converter.py @@ -30,6 +30,19 @@ import logging from typing import Any +from httpx import ( + ConnectError, + HTTPStatusError, + NetworkError, + ReadTimeout, + TimeoutException, + WriteTimeout, +) + +from opensandbox.api.execd.errors import UnexpectedStatus as ExecdUnexpectedStatus +from opensandbox.api.lifecycle.errors import ( + UnexpectedStatus as LifecycleUnexpectedStatus, +) from opensandbox.exceptions import ( InvalidArgumentException, SandboxApiException, @@ -40,6 +53,15 @@ logger = logging.getLogger(__name__) +UNEXPECTED_STATUS_TYPES = (LifecycleUnexpectedStatus, ExecdUnexpectedStatus) +HTTPX_NETWORK_ERROR_TYPES = ( + ConnectError, + TimeoutException, + NetworkError, + ReadTimeout, + WriteTimeout, +) + class ExceptionConverter: """ @@ -124,24 +146,17 @@ def to_sandbox_exception(e: Exception) -> SandboxException: def _is_unexpected_status_error(e: Exception) -> bool: """Check if exception is an openapi-python-client UnexpectedStatus error.""" - return type(e).__name__ == "UnexpectedStatus" + return isinstance(e, UNEXPECTED_STATUS_TYPES) def _is_httpx_status_error(e: Exception) -> bool: """Check if exception is an httpx HTTPStatusError.""" - return type(e).__name__ == "HTTPStatusError" + return isinstance(e, HTTPStatusError) def _is_httpx_network_error(e: Exception) -> bool: """Check if exception is an httpx network-related error.""" - error_types = ( - "ConnectError", - "TimeoutException", - "NetworkError", - "ReadTimeout", - "WriteTimeout", - ) - return type(e).__name__ in error_types + return isinstance(e, HTTPX_NETWORK_ERROR_TYPES) def _convert_unexpected_status_to_api_exception(e: Exception) -> SandboxApiException: 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 c4650603..fd5be5f4 100644 --- a/sdks/sandbox/python/tests/test_converters_and_error_handling.py +++ b/sdks/sandbox/python/tests/test_converters_and_error_handling.py @@ -18,6 +18,7 @@ from datetime import datetime, timedelta import pytest +from httpx import HTTPStatusError, Request, Response from opensandbox.adapters.converter.exception_converter import ( ExceptionConverter, @@ -36,6 +37,7 @@ from opensandbox.adapters.converter.sandbox_model_converter import ( SandboxModelConverter, ) +from opensandbox.api.lifecycle.errors import UnexpectedStatus from opensandbox.exceptions import ( InvalidArgumentException, SandboxApiException, @@ -96,6 +98,34 @@ def test_exception_converter_maps_common_types() -> None: assert isinstance(se2, SandboxInternalException) +def test_exception_converter_maps_generated_unexpected_status_to_api_exception() -> ( + None +): + err = UnexpectedStatus(400, b'{"code":"X","message":"bad"}') + + converted = ExceptionConverter.to_sandbox_exception(err) + + assert isinstance(converted, SandboxApiException) + assert converted.status_code == 400 + assert converted.error is not None + assert converted.error.code == "X" + + +def test_exception_converter_maps_httpx_status_error_to_api_exception() -> None: + request = Request("GET", "https://example.test") + response = Response( + 502, request=request, content=b'{"code":"UPSTREAM","message":"gateway"}' + ) + err = HTTPStatusError("bad gateway", request=request, response=response) + + converted = ExceptionConverter.to_sandbox_exception(err) + + assert isinstance(converted, SandboxApiException) + assert converted.status_code == 502 + assert converted.error is not None + assert converted.error.code == "UPSTREAM" + + def test_execution_converter_to_api_run_command_request() -> None: from opensandbox.api.execd.types import UNSET