diff --git a/MIGRATION.md b/MIGRATION.md new file mode 100644 index 00000000..d0a3ad7f --- /dev/null +++ b/MIGRATION.md @@ -0,0 +1,70 @@ +# Migration Guide + +## Control-Plane API Error Handling (Breaking Change) + +### What changed + +Control-plane API functions (under `blaxel.core.client.api`) now **raise exceptions** on error responses (4xx/5xx) instead of returning `Union[Model, Error]`. This aligns the control-plane client with the Go SDK and the existing Python domain wrappers (`SandboxAPIError`, `DriveAPIError`, `VolumeAPIError`). + +### New exception hierarchy + +``` +BlaxelAPIError # base class (blaxel.core.errors) +├── ControlPlaneError # control-plane 4xx/5xx (blaxel.core.client.errors) +├── SandboxAPIError # sandbox domain errors +├── DriveAPIError # drive domain errors +└── VolumeAPIError # volume domain errors +``` + +All domain errors now inherit from `BlaxelAPIError`, so you can catch every API error with a single `except BlaxelAPIError`. + +### Before (old pattern) + +```python +from blaxel.core.client.api.agents.get_agent import sync as get_agent +from blaxel.core.client.models.error import Error + +result = get_agent(agent_name="my-agent", client=client) +if isinstance(result, Error): + print(f"Error {result.code}: {result.message}") +else: + print(result.metadata.name) +``` + +### After (new pattern) + +```python +from blaxel.core.client.api.agents.get_agent import sync as get_agent +from blaxel.core import ControlPlaneError + +try: + result = get_agent(agent_name="my-agent", client=client) + print(result.metadata.name) +except ControlPlaneError as e: + print(f"Error {e.status_code}: {e}") + # e.error_model contains the original Error data model if needed +``` + +### Opting out + +If you prefer the old union-return behaviour, set `raise_on_error=False` on the client: + +```python +from blaxel.core.client.client import Client + +client = Client(base_url="...", raise_on_error=False) +``` + +With this flag, the functions continue to return `Union[Model, Error]` as before. + +### Catching all Blaxel errors + +```python +from blaxel.core import BlaxelAPIError + +try: + # any SDK operation + ... +except BlaxelAPIError as e: + print(f"Blaxel API error ({e.status_code}): {e}") +``` diff --git a/src/blaxel/core/__init__.py b/src/blaxel/core/__init__.py index 219d1b23..2a1cce4c 100644 --- a/src/blaxel/core/__init__.py +++ b/src/blaxel/core/__init__.py @@ -4,6 +4,7 @@ from .authentication import BlaxelAuth, auth, get_credentials from .cache import find_from_cache from .client.client import client +from .client.errors import ControlPlaneError from .common import ( autoload, env, @@ -12,6 +13,7 @@ verify_webhook_signature, ) from .drive import DriveAPIError, DriveCreateConfiguration, DriveInstance, SyncDriveInstance +from .errors import BlaxelAPIError from .image import ImageBuildContext, ImageInstance, LocalFile from .jobs import BlJobWrapper from .mcp import BlaxelMcpServerTransport, websocket_client @@ -35,6 +37,8 @@ from .volume import SyncVolumeInstance, VolumeCreateConfiguration, VolumeInstance __all__ = [ + "BlaxelAPIError", + "ControlPlaneError", "BlAgent", "bl_agent", "BlaxelAuth", diff --git a/src/blaxel/core/client/api/agents/create_agent.py b/src/blaxel/core/client/api/agents/create_agent.py index 1663054e..5b4497fa 100644 --- a/src/blaxel/core/client/api/agents/create_agent.py +++ b/src/blaxel/core/client/api/agents/create_agent.py @@ -41,22 +41,32 @@ def _parse_response(*, client: Client, response: httpx.Response) -> Union[Agent, if response.status_code == 400: response_400 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_400, response) return response_400 if response.status_code == 401: response_401 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_401, response) return response_401 if response.status_code == 403: response_403 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_403, response) return response_403 if response.status_code == 409: response_409 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_409, response) return response_409 if response.status_code == 500: response_500 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_500, response) return response_500 if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) diff --git a/src/blaxel/core/client/api/agents/delete_agent.py b/src/blaxel/core/client/api/agents/delete_agent.py index 8b0c9882..caa42244 100644 --- a/src/blaxel/core/client/api/agents/delete_agent.py +++ b/src/blaxel/core/client/api/agents/delete_agent.py @@ -29,18 +29,26 @@ def _parse_response(*, client: Client, response: httpx.Response) -> Union[Agent, if response.status_code == 401: response_401 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_401, response) return response_401 if response.status_code == 403: response_403 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_403, response) return response_403 if response.status_code == 404: response_404 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_404, response) return response_404 if response.status_code == 500: response_500 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_500, response) return response_500 if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) diff --git a/src/blaxel/core/client/api/agents/get_agent.py b/src/blaxel/core/client/api/agents/get_agent.py index f24565ac..59905490 100644 --- a/src/blaxel/core/client/api/agents/get_agent.py +++ b/src/blaxel/core/client/api/agents/get_agent.py @@ -38,18 +38,26 @@ def _parse_response(*, client: Client, response: httpx.Response) -> Union[Agent, if response.status_code == 401: response_401 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_401, response) return response_401 if response.status_code == 403: response_403 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_403, response) return response_403 if response.status_code == 404: response_404 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_404, response) return response_404 if response.status_code == 500: response_500 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_500, response) return response_500 if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) diff --git a/src/blaxel/core/client/api/agents/list_agents.py b/src/blaxel/core/client/api/agents/list_agents.py index 487c47a2..419f2e00 100644 --- a/src/blaxel/core/client/api/agents/list_agents.py +++ b/src/blaxel/core/client/api/agents/list_agents.py @@ -34,14 +34,20 @@ def _parse_response( if response.status_code == 401: response_401 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_401, response) return response_401 if response.status_code == 403: response_403 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_403, response) return response_403 if response.status_code == 500: response_500 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_500, response) return response_500 if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) diff --git a/src/blaxel/core/client/api/agents/update_agent.py b/src/blaxel/core/client/api/agents/update_agent.py index 70987adf..76ee5df8 100644 --- a/src/blaxel/core/client/api/agents/update_agent.py +++ b/src/blaxel/core/client/api/agents/update_agent.py @@ -42,22 +42,32 @@ def _parse_response(*, client: Client, response: httpx.Response) -> Union[Agent, if response.status_code == 400: response_400 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_400, response) return response_400 if response.status_code == 401: response_401 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_401, response) return response_401 if response.status_code == 403: response_403 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_403, response) return response_403 if response.status_code == 404: response_404 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_404, response) return response_404 if response.status_code == 500: response_500 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_500, response) return response_500 if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) diff --git a/src/blaxel/core/client/api/compute/delete_sandbox.py b/src/blaxel/core/client/api/compute/delete_sandbox.py index c16dcbd8..c037b5b4 100644 --- a/src/blaxel/core/client/api/compute/delete_sandbox.py +++ b/src/blaxel/core/client/api/compute/delete_sandbox.py @@ -29,18 +29,26 @@ def _parse_response(*, client: Client, response: httpx.Response) -> Union[Error, if response.status_code == 401: response_401 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_401, response) return response_401 if response.status_code == 403: response_403 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_403, response) return response_403 if response.status_code == 404: response_404 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_404, response) return response_404 if response.status_code == 500: response_500 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_500, response) return response_500 if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) diff --git a/src/blaxel/core/client/api/compute/get_sandbox.py b/src/blaxel/core/client/api/compute/get_sandbox.py index 4e12abf3..e43bacb0 100644 --- a/src/blaxel/core/client/api/compute/get_sandbox.py +++ b/src/blaxel/core/client/api/compute/get_sandbox.py @@ -38,18 +38,26 @@ def _parse_response(*, client: Client, response: httpx.Response) -> Union[Error, if response.status_code == 401: response_401 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_401, response) return response_401 if response.status_code == 403: response_403 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_403, response) return response_403 if response.status_code == 404: response_404 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_404, response) return response_404 if response.status_code == 500: response_500 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_500, response) return response_500 if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) diff --git a/src/blaxel/core/client/api/compute/list_sandboxes.py b/src/blaxel/core/client/api/compute/list_sandboxes.py index 655e6877..394679f1 100644 --- a/src/blaxel/core/client/api/compute/list_sandboxes.py +++ b/src/blaxel/core/client/api/compute/list_sandboxes.py @@ -34,14 +34,20 @@ def _parse_response( if response.status_code == 401: response_401 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_401, response) return response_401 if response.status_code == 403: response_403 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_403, response) return response_403 if response.status_code == 500: response_500 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_500, response) return response_500 if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) diff --git a/src/blaxel/core/client/api/compute/update_sandbox.py b/src/blaxel/core/client/api/compute/update_sandbox.py index f015bec3..fc05a885 100644 --- a/src/blaxel/core/client/api/compute/update_sandbox.py +++ b/src/blaxel/core/client/api/compute/update_sandbox.py @@ -42,22 +42,32 @@ def _parse_response(*, client: Client, response: httpx.Response) -> Union[Error, if response.status_code == 400: response_400 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_400, response) return response_400 if response.status_code == 401: response_401 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_401, response) return response_401 if response.status_code == 403: response_403 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_403, response) return response_403 if response.status_code == 404: response_404 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_404, response) return response_404 if response.status_code == 500: response_500 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_500, response) return response_500 if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) diff --git a/src/blaxel/core/client/api/feature_flags/test_feature_flag.py b/src/blaxel/core/client/api/feature_flags/test_feature_flag.py index 81ebcef6..69e88742 100644 --- a/src/blaxel/core/client/api/feature_flags/test_feature_flag.py +++ b/src/blaxel/core/client/api/feature_flags/test_feature_flag.py @@ -40,14 +40,20 @@ def _parse_response( if response.status_code == 401: response_401 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_401, response) return response_401 if response.status_code == 403: response_403 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_403, response) return response_403 if response.status_code == 404: response_404 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_404, response) return response_404 if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) diff --git a/src/blaxel/core/client/api/functions/create_function.py b/src/blaxel/core/client/api/functions/create_function.py index c39f70f6..0b09bdb9 100644 --- a/src/blaxel/core/client/api/functions/create_function.py +++ b/src/blaxel/core/client/api/functions/create_function.py @@ -41,22 +41,32 @@ def _parse_response(*, client: Client, response: httpx.Response) -> Union[Error, if response.status_code == 400: response_400 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_400, response) return response_400 if response.status_code == 401: response_401 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_401, response) return response_401 if response.status_code == 403: response_403 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_403, response) return response_403 if response.status_code == 409: response_409 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_409, response) return response_409 if response.status_code == 500: response_500 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_500, response) return response_500 if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) diff --git a/src/blaxel/core/client/api/functions/delete_function.py b/src/blaxel/core/client/api/functions/delete_function.py index 52086dfc..222ec4a9 100644 --- a/src/blaxel/core/client/api/functions/delete_function.py +++ b/src/blaxel/core/client/api/functions/delete_function.py @@ -29,18 +29,26 @@ def _parse_response(*, client: Client, response: httpx.Response) -> Union[Error, if response.status_code == 401: response_401 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_401, response) return response_401 if response.status_code == 403: response_403 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_403, response) return response_403 if response.status_code == 404: response_404 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_404, response) return response_404 if response.status_code == 500: response_500 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_500, response) return response_500 if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) diff --git a/src/blaxel/core/client/api/functions/get_function.py b/src/blaxel/core/client/api/functions/get_function.py index 95708768..d23afbc4 100644 --- a/src/blaxel/core/client/api/functions/get_function.py +++ b/src/blaxel/core/client/api/functions/get_function.py @@ -38,18 +38,26 @@ def _parse_response(*, client: Client, response: httpx.Response) -> Union[Error, if response.status_code == 401: response_401 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_401, response) return response_401 if response.status_code == 403: response_403 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_403, response) return response_403 if response.status_code == 404: response_404 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_404, response) return response_404 if response.status_code == 500: response_500 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_500, response) return response_500 if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) diff --git a/src/blaxel/core/client/api/functions/list_functions.py b/src/blaxel/core/client/api/functions/list_functions.py index 264e9f39..875ca89a 100644 --- a/src/blaxel/core/client/api/functions/list_functions.py +++ b/src/blaxel/core/client/api/functions/list_functions.py @@ -34,14 +34,20 @@ def _parse_response( if response.status_code == 401: response_401 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_401, response) return response_401 if response.status_code == 403: response_403 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_403, response) return response_403 if response.status_code == 500: response_500 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_500, response) return response_500 if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) diff --git a/src/blaxel/core/client/api/functions/update_function.py b/src/blaxel/core/client/api/functions/update_function.py index 388e0a8d..1a32c30f 100644 --- a/src/blaxel/core/client/api/functions/update_function.py +++ b/src/blaxel/core/client/api/functions/update_function.py @@ -42,22 +42,32 @@ def _parse_response(*, client: Client, response: httpx.Response) -> Union[Error, if response.status_code == 400: response_400 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_400, response) return response_400 if response.status_code == 401: response_401 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_401, response) return response_401 if response.status_code == 403: response_403 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_403, response) return response_403 if response.status_code == 404: response_404 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_404, response) return response_404 if response.status_code == 500: response_500 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_500, response) return response_500 if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) diff --git a/src/blaxel/core/client/api/integrations/create_integration_connection.py b/src/blaxel/core/client/api/integrations/create_integration_connection.py index d786cf86..9ec6e93b 100644 --- a/src/blaxel/core/client/api/integrations/create_integration_connection.py +++ b/src/blaxel/core/client/api/integrations/create_integration_connection.py @@ -43,22 +43,32 @@ def _parse_response( if response.status_code == 400: response_400 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_400, response) return response_400 if response.status_code == 401: response_401 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_401, response) return response_401 if response.status_code == 403: response_403 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_403, response) return response_403 if response.status_code == 409: response_409 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_409, response) return response_409 if response.status_code == 500: response_500 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_500, response) return response_500 if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) diff --git a/src/blaxel/core/client/api/integrations/delete_integration_connection.py b/src/blaxel/core/client/api/integrations/delete_integration_connection.py index a2b3d68f..db1b75dc 100644 --- a/src/blaxel/core/client/api/integrations/delete_integration_connection.py +++ b/src/blaxel/core/client/api/integrations/delete_integration_connection.py @@ -31,22 +31,32 @@ def _parse_response( if response.status_code == 401: response_401 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_401, response) return response_401 if response.status_code == 403: response_403 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_403, response) return response_403 if response.status_code == 404: response_404 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_404, response) return response_404 if response.status_code == 409: response_409 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_409, response) return response_409 if response.status_code == 500: response_500 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_500, response) return response_500 if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) diff --git a/src/blaxel/core/client/api/integrations/get_integration_connection.py b/src/blaxel/core/client/api/integrations/get_integration_connection.py index 2ce11216..4efcae34 100644 --- a/src/blaxel/core/client/api/integrations/get_integration_connection.py +++ b/src/blaxel/core/client/api/integrations/get_integration_connection.py @@ -31,18 +31,26 @@ def _parse_response( if response.status_code == 401: response_401 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_401, response) return response_401 if response.status_code == 403: response_403 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_403, response) return response_403 if response.status_code == 404: response_404 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_404, response) return response_404 if response.status_code == 500: response_500 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_500, response) return response_500 if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) diff --git a/src/blaxel/core/client/api/integrations/list_integration_connections.py b/src/blaxel/core/client/api/integrations/list_integration_connections.py index 86114f18..7bc104b1 100644 --- a/src/blaxel/core/client/api/integrations/list_integration_connections.py +++ b/src/blaxel/core/client/api/integrations/list_integration_connections.py @@ -34,14 +34,20 @@ def _parse_response( if response.status_code == 401: response_401 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_401, response) return response_401 if response.status_code == 403: response_403 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_403, response) return response_403 if response.status_code == 500: response_500 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_500, response) return response_500 if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) diff --git a/src/blaxel/core/client/api/integrations/update_integration_connection.py b/src/blaxel/core/client/api/integrations/update_integration_connection.py index ba2d5baf..45f04358 100644 --- a/src/blaxel/core/client/api/integrations/update_integration_connection.py +++ b/src/blaxel/core/client/api/integrations/update_integration_connection.py @@ -44,22 +44,32 @@ def _parse_response( if response.status_code == 400: response_400 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_400, response) return response_400 if response.status_code == 401: response_401 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_401, response) return response_401 if response.status_code == 403: response_403 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_403, response) return response_403 if response.status_code == 404: response_404 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_404, response) return response_404 if response.status_code == 500: response_500 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_500, response) return response_500 if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) diff --git a/src/blaxel/core/client/api/models/create_model.py b/src/blaxel/core/client/api/models/create_model.py index 5757caf1..bda2b559 100644 --- a/src/blaxel/core/client/api/models/create_model.py +++ b/src/blaxel/core/client/api/models/create_model.py @@ -41,22 +41,32 @@ def _parse_response(*, client: Client, response: httpx.Response) -> Union[Error, if response.status_code == 400: response_400 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_400, response) return response_400 if response.status_code == 401: response_401 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_401, response) return response_401 if response.status_code == 403: response_403 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_403, response) return response_403 if response.status_code == 409: response_409 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_409, response) return response_409 if response.status_code == 500: response_500 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_500, response) return response_500 if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) diff --git a/src/blaxel/core/client/api/models/delete_model.py b/src/blaxel/core/client/api/models/delete_model.py index 2d5ba884..35746340 100644 --- a/src/blaxel/core/client/api/models/delete_model.py +++ b/src/blaxel/core/client/api/models/delete_model.py @@ -29,18 +29,26 @@ def _parse_response(*, client: Client, response: httpx.Response) -> Union[Error, if response.status_code == 401: response_401 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_401, response) return response_401 if response.status_code == 403: response_403 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_403, response) return response_403 if response.status_code == 404: response_404 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_404, response) return response_404 if response.status_code == 500: response_500 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_500, response) return response_500 if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) diff --git a/src/blaxel/core/client/api/models/get_model.py b/src/blaxel/core/client/api/models/get_model.py index 619325e1..ec2edecb 100644 --- a/src/blaxel/core/client/api/models/get_model.py +++ b/src/blaxel/core/client/api/models/get_model.py @@ -29,18 +29,26 @@ def _parse_response(*, client: Client, response: httpx.Response) -> Union[Error, if response.status_code == 401: response_401 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_401, response) return response_401 if response.status_code == 403: response_403 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_403, response) return response_403 if response.status_code == 404: response_404 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_404, response) return response_404 if response.status_code == 500: response_500 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_500, response) return response_500 if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) diff --git a/src/blaxel/core/client/api/models/list_models.py b/src/blaxel/core/client/api/models/list_models.py index 790712f0..7daf12e2 100644 --- a/src/blaxel/core/client/api/models/list_models.py +++ b/src/blaxel/core/client/api/models/list_models.py @@ -34,14 +34,20 @@ def _parse_response( if response.status_code == 401: response_401 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_401, response) return response_401 if response.status_code == 403: response_403 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_403, response) return response_403 if response.status_code == 500: response_500 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_500, response) return response_500 if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) diff --git a/src/blaxel/core/client/api/models/update_model.py b/src/blaxel/core/client/api/models/update_model.py index b5d5c73b..0b8e69ed 100644 --- a/src/blaxel/core/client/api/models/update_model.py +++ b/src/blaxel/core/client/api/models/update_model.py @@ -42,22 +42,32 @@ def _parse_response(*, client: Client, response: httpx.Response) -> Union[Error, if response.status_code == 400: response_400 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_400, response) return response_400 if response.status_code == 401: response_401 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_401, response) return response_401 if response.status_code == 403: response_403 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_403, response) return response_403 if response.status_code == 404: response_404 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_404, response) return response_404 if response.status_code == 500: response_500 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_500, response) return response_500 if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) diff --git a/src/blaxel/core/client/api/volumes/create_volume.py b/src/blaxel/core/client/api/volumes/create_volume.py index 99e8393b..9f3e4029 100644 --- a/src/blaxel/core/client/api/volumes/create_volume.py +++ b/src/blaxel/core/client/api/volumes/create_volume.py @@ -41,22 +41,32 @@ def _parse_response(*, client: Client, response: httpx.Response) -> Union[Error, if response.status_code == 400: response_400 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_400, response) return response_400 if response.status_code == 401: response_401 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_401, response) return response_401 if response.status_code == 403: response_403 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_403, response) return response_403 if response.status_code == 409: response_409 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_409, response) return response_409 if response.status_code == 500: response_500 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_500, response) return response_500 if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) diff --git a/src/blaxel/core/client/api/volumes/delete_volume.py b/src/blaxel/core/client/api/volumes/delete_volume.py index dbe8ce9b..17084874 100644 --- a/src/blaxel/core/client/api/volumes/delete_volume.py +++ b/src/blaxel/core/client/api/volumes/delete_volume.py @@ -29,22 +29,32 @@ def _parse_response(*, client: Client, response: httpx.Response) -> Union[Error, if response.status_code == 401: response_401 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_401, response) return response_401 if response.status_code == 403: response_403 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_403, response) return response_403 if response.status_code == 404: response_404 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_404, response) return response_404 if response.status_code == 409: response_409 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_409, response) return response_409 if response.status_code == 500: response_500 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_500, response) return response_500 if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) diff --git a/src/blaxel/core/client/api/volumes/get_volume.py b/src/blaxel/core/client/api/volumes/get_volume.py index ef54243f..7b714ad8 100644 --- a/src/blaxel/core/client/api/volumes/get_volume.py +++ b/src/blaxel/core/client/api/volumes/get_volume.py @@ -29,18 +29,26 @@ def _parse_response(*, client: Client, response: httpx.Response) -> Union[Error, if response.status_code == 401: response_401 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_401, response) return response_401 if response.status_code == 403: response_403 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_403, response) return response_403 if response.status_code == 404: response_404 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_404, response) return response_404 if response.status_code == 500: response_500 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_500, response) return response_500 if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) diff --git a/src/blaxel/core/client/api/volumes/list_volumes.py b/src/blaxel/core/client/api/volumes/list_volumes.py index 23cfe6d6..d9e3d811 100644 --- a/src/blaxel/core/client/api/volumes/list_volumes.py +++ b/src/blaxel/core/client/api/volumes/list_volumes.py @@ -34,14 +34,20 @@ def _parse_response( if response.status_code == 401: response_401 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_401, response) return response_401 if response.status_code == 403: response_403 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_403, response) return response_403 if response.status_code == 500: response_500 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_500, response) return response_500 if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) diff --git a/src/blaxel/core/client/api/workspaces/create_workspace.py b/src/blaxel/core/client/api/workspaces/create_workspace.py index b3d8487d..63b68e5f 100644 --- a/src/blaxel/core/client/api/workspaces/create_workspace.py +++ b/src/blaxel/core/client/api/workspaces/create_workspace.py @@ -41,22 +41,32 @@ def _parse_response(*, client: Client, response: httpx.Response) -> Union[Error, if response.status_code == 400: response_400 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_400, response) return response_400 if response.status_code == 401: response_401 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_401, response) return response_401 if response.status_code == 403: response_403 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_403, response) return response_403 if response.status_code == 409: response_409 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_409, response) return response_409 if response.status_code == 500: response_500 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_500, response) return response_500 if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) diff --git a/src/blaxel/core/client/api/workspaces/delete_workspace.py b/src/blaxel/core/client/api/workspaces/delete_workspace.py index 0cef4fab..c2c79cbe 100644 --- a/src/blaxel/core/client/api/workspaces/delete_workspace.py +++ b/src/blaxel/core/client/api/workspaces/delete_workspace.py @@ -29,18 +29,26 @@ def _parse_response(*, client: Client, response: httpx.Response) -> Union[Error, if response.status_code == 401: response_401 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_401, response) return response_401 if response.status_code == 403: response_403 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_403, response) return response_403 if response.status_code == 404: response_404 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_404, response) return response_404 if response.status_code == 500: response_500 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_500, response) return response_500 if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) diff --git a/src/blaxel/core/client/api/workspaces/get_workspace.py b/src/blaxel/core/client/api/workspaces/get_workspace.py index d8d1c6c7..ea2223e9 100644 --- a/src/blaxel/core/client/api/workspaces/get_workspace.py +++ b/src/blaxel/core/client/api/workspaces/get_workspace.py @@ -29,18 +29,26 @@ def _parse_response(*, client: Client, response: httpx.Response) -> Union[Error, if response.status_code == 401: response_401 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_401, response) return response_401 if response.status_code == 403: response_403 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_403, response) return response_403 if response.status_code == 404: response_404 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_404, response) return response_404 if response.status_code == 500: response_500 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_500, response) return response_500 if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) diff --git a/src/blaxel/core/client/api/workspaces/get_workspace_features.py b/src/blaxel/core/client/api/workspaces/get_workspace_features.py index 38f24c8c..386e5b6d 100644 --- a/src/blaxel/core/client/api/workspaces/get_workspace_features.py +++ b/src/blaxel/core/client/api/workspaces/get_workspace_features.py @@ -29,14 +29,20 @@ def _parse_response( if response.status_code == 401: response_401 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_401, response) return response_401 if response.status_code == 403: response_403 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_403, response) return response_403 if response.status_code == 404: response_404 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_404, response) return response_404 if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) diff --git a/src/blaxel/core/client/api/workspaces/list_workspaces.py b/src/blaxel/core/client/api/workspaces/list_workspaces.py index 3cb3d820..1f38885f 100644 --- a/src/blaxel/core/client/api/workspaces/list_workspaces.py +++ b/src/blaxel/core/client/api/workspaces/list_workspaces.py @@ -34,10 +34,14 @@ def _parse_response( if response.status_code == 401: response_401 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_401, response) return response_401 if response.status_code == 500: response_500 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_500, response) return response_500 if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) diff --git a/src/blaxel/core/client/api/workspaces/update_workspace.py b/src/blaxel/core/client/api/workspaces/update_workspace.py index 281c46f9..d1bae65c 100644 --- a/src/blaxel/core/client/api/workspaces/update_workspace.py +++ b/src/blaxel/core/client/api/workspaces/update_workspace.py @@ -42,22 +42,32 @@ def _parse_response(*, client: Client, response: httpx.Response) -> Union[Error, if response.status_code == 400: response_400 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_400, response) return response_400 if response.status_code == 401: response_401 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_401, response) return response_401 if response.status_code == 403: response_403 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_403, response) return response_403 if response.status_code == 404: response_404 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_404, response) return response_404 if response.status_code == 500: response_500 = Error.from_dict(response.json()) + if client.raise_on_error: + raise errors.ControlPlaneError(response_500, response) return response_500 if client.raise_on_unexpected_status: raise errors.UnexpectedStatus(response.status_code, response.content) diff --git a/src/blaxel/core/client/client.py b/src/blaxel/core/client/client.py index 00f5eb33..57aeeaaf 100644 --- a/src/blaxel/core/client/client.py +++ b/src/blaxel/core/client/client.py @@ -39,6 +39,7 @@ class Client: """ raise_on_unexpected_status: bool = field(default=True, kw_only=True) + raise_on_error: bool = field(default=True, kw_only=True) _base_url: str = field(alias="base_url", default="") _cookies: dict[str, str] = field(factory=dict, kw_only=True, alias="cookies") _headers: dict[str, str] = field(factory=dict, kw_only=True, alias="headers") diff --git a/src/blaxel/core/client/errors.py b/src/blaxel/core/client/errors.py index 5f92e76a..de98ba1f 100644 --- a/src/blaxel/core/client/errors.py +++ b/src/blaxel/core/client/errors.py @@ -1,5 +1,11 @@ """Contains shared errors types that can be raised from API functions""" +import httpx + +from ..errors import BlaxelAPIError +from .models.error import Error +from .types import UNSET + class UnexpectedStatus(Exception): """Raised by api functions when the response status an undocumented status and Client.raise_on_unexpected_status is True""" @@ -13,4 +19,25 @@ def __init__(self, status_code: int, content: bytes): ) -__all__ = ["UnexpectedStatus"] +class ControlPlaneError(BlaxelAPIError): + """Raised when a control-plane API endpoint returns an error response (4xx/5xx). + + The original ``Error`` data model is preserved on ``error_model`` so + callers can inspect structured fields if needed. + """ + + def __init__(self, error: Error, response: httpx.Response): + message = error.message if not isinstance(error.message, type(UNSET)) else error.error + status_code = ( + error.code if not isinstance(error.code, type(UNSET)) else response.status_code + ) + super().__init__( + message=str(message), + status_code=int(status_code) if status_code is not None else response.status_code, + error_code=error.error, + response=response, + ) + self.error_model = error + + +__all__ = ["UnexpectedStatus", "ControlPlaneError"] diff --git a/src/blaxel/core/drive/drive.py b/src/blaxel/core/drive/drive.py index f9cb0f9f..d3e3316b 100644 --- a/src/blaxel/core/drive/drive.py +++ b/src/blaxel/core/drive/drive.py @@ -15,19 +15,19 @@ from ..client.api.drives.update_drive import asyncio as update_drive from ..client.api.drives.update_drive import sync as update_drive_sync from ..client.client import client -from ..client.errors import UnexpectedStatus +from ..client.errors import ControlPlaneError, UnexpectedStatus from ..client.models import Drive, DriveSpec, Metadata from ..client.models.error import Error from ..client.types import UNSET from ..common.settings import settings +from ..errors import BlaxelAPIError -class DriveAPIError(Exception): +class DriveAPIError(BlaxelAPIError): """Exception raised when drive API returns an error.""" def __init__(self, message: str, status_code: int | None = None, code: str | None = None): - super().__init__(message) - self.status_code = status_code + super().__init__(message=message, status_code=status_code, error_code=code) self.code = code @@ -226,7 +226,10 @@ async def create( stacklevel=2, ) - response = await create_drive(client=client, body=drive) + try: + response = await create_drive(client=client, body=drive) + except ControlPlaneError as e: + raise DriveAPIError(str(e), status_code=e.status_code, code=e.error_code) from e if isinstance(response, Error): status_code = int(response.code) if response.code is not UNSET else None message = response.message if response.message is not UNSET else response.error @@ -235,7 +238,10 @@ async def create( @classmethod async def get(cls, drive_name: str) -> "DriveInstance": - response = await get_drive(drive_name=drive_name, client=client) + try: + response = await get_drive(drive_name=drive_name, client=client) + except ControlPlaneError as e: + raise DriveAPIError(str(e), status_code=e.status_code, code=e.error_code) from e if response is None: raise DriveAPIError( f"Drive '{drive_name}' not found", status_code=404, code="NOT_FOUND" @@ -376,7 +382,10 @@ def create( stacklevel=2, ) - response = create_drive_sync(client=client, body=drive) + try: + response = create_drive_sync(client=client, body=drive) + except ControlPlaneError as e: + raise DriveAPIError(str(e), status_code=e.status_code, code=e.error_code) from e if isinstance(response, Error): status_code = int(response.code) if response.code is not UNSET else None message = response.message if response.message is not UNSET else response.error @@ -386,7 +395,10 @@ def create( @classmethod def get(cls, drive_name: str) -> "SyncDriveInstance": """Get a drive by name synchronously.""" - response = get_drive_sync(drive_name=drive_name, client=client) + try: + response = get_drive_sync(drive_name=drive_name, client=client) + except ControlPlaneError as e: + raise DriveAPIError(str(e), status_code=e.status_code, code=e.error_code) from e if response is None: raise DriveAPIError( f"Drive '{drive_name}' not found", status_code=404, code="NOT_FOUND" @@ -509,7 +521,10 @@ async def _update_drive_by_name( spec=merged_spec, ) - response = await update_drive(drive_name=drive_name, client=client, body=body) + try: + response = await update_drive(drive_name=drive_name, client=client, body=body) + except ControlPlaneError as e: + raise DriveAPIError(str(e), status_code=e.status_code, code=e.error_code) from e if isinstance(response, Error): status_code = int(response.code) if response.code is not UNSET else None message = response.message if response.message is not UNSET else response.error @@ -582,7 +597,10 @@ def _update_drive_by_name_sync( spec=merged_spec, ) - response = update_drive_sync(drive_name=drive_name, client=client, body=body) + try: + response = update_drive_sync(drive_name=drive_name, client=client, body=body) + except ControlPlaneError as e: + raise DriveAPIError(str(e), status_code=e.status_code, code=e.error_code) from e if isinstance(response, Error): status_code = int(response.code) if response.code is not UNSET else None message = response.message if response.message is not UNSET else response.error diff --git a/src/blaxel/core/errors.py b/src/blaxel/core/errors.py new file mode 100644 index 00000000..216e7969 --- /dev/null +++ b/src/blaxel/core/errors.py @@ -0,0 +1,32 @@ +"""Unified error hierarchy for Blaxel SDK. + +All domain-specific API errors (ControlPlaneError, SandboxAPIError, +DriveAPIError, VolumeAPIError) inherit from BlaxelAPIError, giving +callers a single base class they can catch. +""" + +import httpx + + +class BlaxelAPIError(Exception): + """Base exception for all Blaxel API errors. + + Attributes: + message: Human-readable error description. + status_code: HTTP status code of the error response, if available. + error_code: Machine-readable error code, if available. + response: The raw ``httpx.Response`` that triggered the error, + if available. + """ + + def __init__( + self, + message: str, + status_code: int | None = None, + error_code: str | None = None, + response: httpx.Response | None = None, + ): + super().__init__(message) + self.status_code = status_code + self.error_code = error_code + self.response = response diff --git a/src/blaxel/core/sandbox/default/sandbox.py b/src/blaxel/core/sandbox/default/sandbox.py index 78ec28c9..daba0e2d 100644 --- a/src/blaxel/core/sandbox/default/sandbox.py +++ b/src/blaxel/core/sandbox/default/sandbox.py @@ -12,6 +12,7 @@ from ...client.api.compute.list_sandboxes import asyncio as list_sandboxes from ...client.api.compute.update_sandbox import asyncio as update_sandbox from ...client.client import client +from ...client.errors import ControlPlaneError from ...client.models import ( Metadata, MetadataLabels, @@ -28,6 +29,7 @@ from ...client.models.sandbox_error import SandboxError from ...client.types import UNSET from ...common.settings import settings +from ...errors import BlaxelAPIError from ..types import ( SandboxConfiguration, SandboxCreateConfiguration, @@ -44,12 +46,11 @@ from .system import SandboxSystem -class SandboxAPIError(Exception): +class SandboxAPIError(BlaxelAPIError): """Exception raised when sandbox API returns an error.""" def __init__(self, message: str, status_code: int | None = None, code: str | None = None): - super().__init__(message) - self.status_code = status_code + super().__init__(message=message, status_code=status_code, error_code=code) self.code = code @@ -282,12 +283,15 @@ async def create( sandbox.spec.runtime.image = sandbox.spec.runtime.image or default_image sandbox.spec.runtime.memory = sandbox.spec.runtime.memory or default_memory - response = await create_sandbox( - client=client, - body=sandbox, - ) + try: + response = await create_sandbox( + client=client, + body=sandbox, + ) + except ControlPlaneError as e: + raise SandboxAPIError(str(e), status_code=e.status_code, code=e.error_code) from e - # Check if response is an error + # Fallback for raise_on_error=False if isinstance(response, SandboxError): status_code = response.status_code if response.status_code is not UNSET else None code = response.code if response.code else None @@ -306,12 +310,15 @@ async def create( @classmethod async def get(cls, sandbox_name: str) -> "SandboxInstance": - response = await get_sandbox( - sandbox_name, - client=client, - ) + try: + response = await get_sandbox( + sandbox_name, + client=client, + ) + except ControlPlaneError as e: + raise SandboxAPIError(str(e), status_code=e.status_code, code=e.error_code) from e - # Check if response is an error + # Fallback for raise_on_error=False if isinstance(response, Error): status_code = response.code if response.code is not UNSET else None message = response.message if response.message is not UNSET else response.error @@ -371,11 +378,14 @@ async def update_metadata( updated_sandbox.metadata.display_name = metadata.display_name # Call the update API - response = await update_sandbox( - sandbox_name=sandbox_name, - client=client, - body=updated_sandbox, - ) + try: + response = await update_sandbox( + sandbox_name=sandbox_name, + client=client, + body=updated_sandbox, + ) + except ControlPlaneError as e: + raise SandboxAPIError(str(e), status_code=e.status_code, code=e.error_code) from e # Return new instance with updated sandbox return cls(response) @@ -404,11 +414,14 @@ async def update_ttl(cls, sandbox_name: str, ttl: str) -> "SandboxInstance": updated_sandbox.spec.runtime.ttl = ttl # Call the update API - response = await update_sandbox( - sandbox_name=sandbox_name, - client=client, - body=updated_sandbox, - ) + try: + response = await update_sandbox( + sandbox_name=sandbox_name, + client=client, + body=updated_sandbox, + ) + except ControlPlaneError as e: + raise SandboxAPIError(str(e), status_code=e.status_code, code=e.error_code) from e return cls(response) @@ -438,11 +451,14 @@ async def update_lifecycle( updated_sandbox.spec.lifecycle = lifecycle # Call the update API - response = await update_sandbox( - sandbox_name=sandbox_name, - client=client, - body=updated_sandbox, - ) + try: + response = await update_sandbox( + sandbox_name=sandbox_name, + client=client, + body=updated_sandbox, + ) + except ControlPlaneError as e: + raise SandboxAPIError(str(e), status_code=e.status_code, code=e.error_code) from e return cls(response) @@ -509,10 +525,13 @@ async def from_session( async def _delete_sandbox_by_name(sandbox_name: str) -> Sandbox: """Delete a sandbox by name.""" - response = await delete_sandbox( - sandbox_name, - client=client, - ) + try: + response = await delete_sandbox( + sandbox_name, + client=client, + ) + except ControlPlaneError as e: + raise SandboxAPIError(str(e), status_code=e.status_code, code=e.error_code) from e if response is None: raise ValueError(f"Sandbox {sandbox_name} not found") return response diff --git a/src/blaxel/core/sandbox/sync/sandbox.py b/src/blaxel/core/sandbox/sync/sandbox.py index 65280ce7..aba06f89 100644 --- a/src/blaxel/core/sandbox/sync/sandbox.py +++ b/src/blaxel/core/sandbox/sync/sandbox.py @@ -12,6 +12,7 @@ from ...client.api.compute.list_sandboxes import sync as list_sandboxes from ...client.api.compute.update_sandbox import sync as update_sandbox from ...client.client import client +from ...client.errors import ControlPlaneError from ...client.models import ( Metadata, Sandbox, @@ -248,12 +249,15 @@ def create( sandbox.spec.runtime = SandboxRuntime(image=default_image, memory=default_memory) sandbox.spec.runtime.image = sandbox.spec.runtime.image or default_image sandbox.spec.runtime.memory = sandbox.spec.runtime.memory or default_memory - response = create_sandbox( - client=client, - body=sandbox, - ) + try: + response = create_sandbox( + client=client, + body=sandbox, + ) + except ControlPlaneError as e: + raise SandboxAPIError(str(e), status_code=e.status_code, code=e.error_code) from e - # Check if response is an error + # Fallback for raise_on_error=False if isinstance(response, SandboxError): status_code = response.status_code if response.status_code is not UNSET else None code = response.code if response.code else None @@ -270,12 +274,15 @@ def create( @classmethod def get(cls, sandbox_name: str) -> "SyncSandboxInstance": - response = get_sandbox( - sandbox_name, - client=client, - ) + try: + response = get_sandbox( + sandbox_name, + client=client, + ) + except ControlPlaneError as e: + raise SandboxAPIError(str(e), status_code=e.status_code, code=e.error_code) from e - # Check if response is an error + # Fallback for raise_on_error=False if isinstance(response, Error): status_code = response.code if response.code is not UNSET else None message = response.message if response.message is not UNSET else response.error @@ -312,11 +319,14 @@ def update_metadata( updated_sandbox.metadata.labels.update(metadata.labels) if metadata.display_name is not None: updated_sandbox.metadata.display_name = metadata.display_name - response = update_sandbox( - sandbox_name=sandbox_name, - client=client, - body=updated_sandbox, - ) + try: + response = update_sandbox( + sandbox_name=sandbox_name, + client=client, + body=updated_sandbox, + ) + except ControlPlaneError as e: + raise SandboxAPIError(str(e), status_code=e.status_code, code=e.error_code) from e return cls(response) @classmethod @@ -343,11 +353,14 @@ def update_ttl(cls, sandbox_name: str, ttl: str) -> "SyncSandboxInstance": updated_sandbox.spec.runtime.ttl = ttl # Call the update API - response = update_sandbox( - sandbox_name=sandbox_name, - client=client, - body=updated_sandbox, - ) + try: + response = update_sandbox( + sandbox_name=sandbox_name, + client=client, + body=updated_sandbox, + ) + except ControlPlaneError as e: + raise SandboxAPIError(str(e), status_code=e.status_code, code=e.error_code) from e return cls(response) @@ -377,11 +390,14 @@ def update_lifecycle( updated_sandbox.spec.lifecycle = lifecycle # Call the update API - response = update_sandbox( - sandbox_name=sandbox_name, - client=client, - body=updated_sandbox, - ) + try: + response = update_sandbox( + sandbox_name=sandbox_name, + client=client, + body=updated_sandbox, + ) + except ControlPlaneError as e: + raise SandboxAPIError(str(e), status_code=e.status_code, code=e.error_code) from e return cls(response) @@ -432,10 +448,15 @@ def from_session( def _delete_sandbox_by_name(sandbox_name: str) -> Sandbox: """Delete a sandbox by name.""" - response = delete_sandbox( - sandbox_name, - client=client, - ) + try: + response = delete_sandbox( + sandbox_name, + client=client, + ) + except ControlPlaneError as e: + raise SandboxAPIError(str(e), status_code=e.status_code, code=e.error_code) from e + if response is None: + raise ValueError(f"Sandbox {sandbox_name} not found") return response diff --git a/src/blaxel/core/volume/volume.py b/src/blaxel/core/volume/volume.py index 0ecfe5ca..c6884862 100644 --- a/src/blaxel/core/volume/volume.py +++ b/src/blaxel/core/volume/volume.py @@ -15,18 +15,19 @@ from ..client.api.volumes.update_volume import asyncio as update_volume from ..client.api.volumes.update_volume import sync as update_volume_sync from ..client.client import client +from ..client.errors import ControlPlaneError from ..client.models import Metadata, Volume, VolumeSpec from ..client.models.error import Error from ..client.types import UNSET from ..common.settings import settings +from ..errors import BlaxelAPIError -class VolumeAPIError(Exception): +class VolumeAPIError(BlaxelAPIError): """Exception raised when volume API returns an error.""" def __init__(self, message: str, status_code: int | None = None, code: str | None = None): - super().__init__(message) - self.status_code = status_code + super().__init__(message=message, status_code=status_code, error_code=code) self.code = code @@ -237,7 +238,10 @@ async def create( stacklevel=2, ) - response = await create_volume(client=client, body=volume) + try: + response = await create_volume(client=client, body=volume) + except ControlPlaneError as e: + raise VolumeAPIError(str(e), status_code=e.status_code, code=e.error_code) from e if isinstance(response, Error): status_code = int(response.code) if response.code is not UNSET else None message = response.message if response.message is not UNSET else response.error @@ -246,7 +250,10 @@ async def create( @classmethod async def get(cls, volume_name: str) -> "VolumeInstance": - response = await get_volume(volume_name=volume_name, client=client) + try: + response = await get_volume(volume_name=volume_name, client=client) + except ControlPlaneError as e: + raise VolumeAPIError(str(e), status_code=e.status_code, code=e.error_code) from e if isinstance(response, Error): status_code = int(response.code) if response.code is not UNSET else None message = response.message if response.message is not UNSET else response.error @@ -389,7 +396,10 @@ def create( stacklevel=2, ) - response = create_volume_sync(client=client, body=volume) + try: + response = create_volume_sync(client=client, body=volume) + except ControlPlaneError as e: + raise VolumeAPIError(str(e), status_code=e.status_code, code=e.error_code) from e if isinstance(response, Error): status_code = int(response.code) if response.code is not UNSET else None message = response.message if response.message is not UNSET else response.error @@ -399,7 +409,10 @@ def create( @classmethod def get(cls, volume_name: str) -> "SyncVolumeInstance": """Get a volume by name synchronously.""" - response = get_volume_sync(volume_name=volume_name, client=client) + try: + response = get_volume_sync(volume_name=volume_name, client=client) + except ControlPlaneError as e: + raise VolumeAPIError(str(e), status_code=e.status_code, code=e.error_code) from e if isinstance(response, Error): status_code = int(response.code) if response.code is not UNSET else None message = response.message if response.message is not UNSET else response.error @@ -520,7 +533,10 @@ async def _update_volume_by_name( spec=merged_spec, ) - response = await update_volume(volume_name=volume_name, client=client, body=body) + try: + response = await update_volume(volume_name=volume_name, client=client, body=body) + except ControlPlaneError as e: + raise VolumeAPIError(str(e), status_code=e.status_code, code=e.error_code) from e if isinstance(response, Error): status_code = int(response.code) if response.code is not UNSET else None message = response.message if response.message is not UNSET else response.error @@ -598,7 +614,10 @@ def _update_volume_by_name_sync( spec=merged_spec, ) - response = update_volume_sync(volume_name=volume_name, client=client, body=body) + try: + response = update_volume_sync(volume_name=volume_name, client=client, body=body) + except ControlPlaneError as e: + raise VolumeAPIError(str(e), status_code=e.status_code, code=e.error_code) from e if isinstance(response, Error): status_code = int(response.code) if response.code is not UNSET else None message = response.message if response.message is not UNSET else response.error diff --git a/templates/endpoint_module.py.jinja b/templates/endpoint_module.py.jinja index 1a657f2f..cc896938 100644 --- a/templates/endpoint_module.py.jinja +++ b/templates/endpoint_module.py.jinja @@ -79,6 +79,10 @@ def _parse_response(*, client: Client, response: httpx.Response) -> {{ return_st {% else %} {{ response.prop.python_name }} = cast({{ response.prop.get_type_string() }}, {{ response.source.attribute }}) {% endif %} + {% if response.prop.get_type_string() == "Error" %} + if client.raise_on_error: + raise errors.ControlPlaneError({{ response.prop.python_name }}, response) + {% endif %} return {{ response.prop.python_name }} {% else %} return None @@ -149,4 +153,3 @@ async def asyncio( return (await asyncio_detailed( {{ kwargs(endpoint) }} )).parsed -{% endif %}