diff --git a/src/runloop_api_client/resources/devboxes/devboxes.py b/src/runloop_api_client/resources/devboxes/devboxes.py index 65d0dcbd3..addb31c79 100644 --- a/src/runloop_api_client/resources/devboxes/devboxes.py +++ b/src/runloop_api_client/resources/devboxes/devboxes.py @@ -497,30 +497,41 @@ def create_and_await_running( PollingTimeout: If polling times out before devbox is running RunloopError: If devbox enters a non-running terminal state """ - # Pass all create_args to the underlying create method - devbox = self.create( - blueprint_id=blueprint_id, - blueprint_name=blueprint_name, - code_mounts=code_mounts, - entrypoint=entrypoint, - environment_variables=environment_variables, - file_mounts=file_mounts, - gateways=gateways, - launch_parameters=launch_parameters, - mcp=mcp, - metadata=metadata, - mounts=mounts, - name=name, - secrets=secrets, - snapshot_id=snapshot_id, - tunnel=tunnel, - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - idempotency_key=idempotency_key, + devbox = self._post( + "/v1/devboxes/create_and_await_running", + body=maybe_transform( + { + "blueprint_id": blueprint_id, + "blueprint_name": blueprint_name, + "code_mounts": code_mounts, + "entrypoint": entrypoint, + "environment_variables": environment_variables, + "file_mounts": file_mounts, + "gateways": gateways, + "launch_parameters": launch_parameters, + "mcp": mcp, + "metadata": metadata, + "mounts": mounts, + "name": name, + "secrets": secrets, + "snapshot_id": snapshot_id, + "tunnel": tunnel, + }, + devbox_create_params.DevboxCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=DevboxView, ) + if devbox.status == "running": + return devbox + return self.await_running( devbox.id, polling_config=polling_config, @@ -2068,30 +2079,41 @@ async def create_and_await_running( RunloopError: If devbox enters a non-running terminal state """ - # Pass all create_args, relevant request args to the underlying create method - devbox = await self.create( - blueprint_id=blueprint_id, - blueprint_name=blueprint_name, - code_mounts=code_mounts, - entrypoint=entrypoint, - environment_variables=environment_variables, - file_mounts=file_mounts, - gateways=gateways, - launch_parameters=launch_parameters, - mcp=mcp, - metadata=metadata, - mounts=mounts, - name=name, - secrets=secrets, - snapshot_id=snapshot_id, - tunnel=tunnel, - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - idempotency_key=idempotency_key, + devbox = await self._post( + "/v1/devboxes/create_and_await_running", + body=await async_maybe_transform( + { + "blueprint_id": blueprint_id, + "blueprint_name": blueprint_name, + "code_mounts": code_mounts, + "entrypoint": entrypoint, + "environment_variables": environment_variables, + "file_mounts": file_mounts, + "gateways": gateways, + "launch_parameters": launch_parameters, + "mcp": mcp, + "metadata": metadata, + "mounts": mounts, + "name": name, + "secrets": secrets, + "snapshot_id": snapshot_id, + "tunnel": tunnel, + }, + devbox_create_params.DevboxCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=DevboxView, ) + if devbox.status == "running": + return devbox + return await self.await_running( devbox.id, polling_config=polling_config, diff --git a/tests/api_resources/test_devboxes.py b/tests/api_resources/test_devboxes.py index 0d9495f80..490448dc4 100644 --- a/tests/api_resources/test_devboxes.py +++ b/tests/api_resources/test_devboxes.py @@ -4,7 +4,7 @@ import os from typing import Any, cast -from unittest.mock import Mock, patch +from unittest.mock import Mock, AsyncMock, patch import httpx import pytest @@ -1452,9 +1452,9 @@ def test_method_create_and_await_running_success(self, client: Runloop) -> None: state_transitions=[], ) - with patch.object(client.devboxes, "create") as mock_create: + with patch.object(client.devboxes, "_post") as mock_post: with patch.object(client.devboxes, "await_running") as mock_await: - mock_create.return_value = mock_devbox_creating + mock_post.return_value = mock_devbox_creating mock_await.return_value = mock_devbox_running result = client.devboxes.create_and_await_running( @@ -1463,9 +1463,34 @@ def test_method_create_and_await_running_success(self, client: Runloop) -> None: assert result.id == "test_id" assert result.status == "running" - mock_create.assert_called_once() + assert mock_post.call_args.args[0] == "/v1/devboxes/create_and_await_running" + assert mock_post.call_args.kwargs["body"]["name"] == "test" mock_await.assert_called_once_with("test_id", polling_config=None) + @parametrize + def test_method_create_and_await_running_optimistic_success(self, client: Runloop) -> None: + """Test create_and_await_running returns a running response without polling.""" + + mock_devbox_running = DevboxView( + id="test_id", + status="running", + capabilities=[], + create_time_ms=1234567890, + launch_parameters=LaunchParameters(resource_size_request="X_SMALL"), + metadata={}, + state_transitions=[], + ) + + with patch.object(client.devboxes, "_post") as mock_post: + with patch.object(client.devboxes, "await_running") as mock_await: + mock_post.return_value = mock_devbox_running + + result = client.devboxes.create_and_await_running(name="test") + + assert result.status == "running" + assert mock_post.call_args.args[0] == "/v1/devboxes/create_and_await_running" + mock_await.assert_not_called() + @parametrize def test_method_create_and_await_running_with_config(self, client: Runloop) -> None: """Test create_and_await_running with custom polling configuration""" @@ -1492,9 +1517,9 @@ def test_method_create_and_await_running_with_config(self, client: Runloop) -> N config = PollingConfig(interval_seconds=0.1, max_attempts=10) - with patch.object(client.devboxes, "create") as mock_create: + with patch.object(client.devboxes, "_post") as mock_post: with patch.object(client.devboxes, "await_running") as mock_await: - mock_create.return_value = mock_devbox_creating + mock_post.return_value = mock_devbox_creating mock_await.return_value = mock_devbox_running result = client.devboxes.create_and_await_running( @@ -1514,8 +1539,8 @@ def test_method_create_and_await_running_create_failure(self, client: Runloop) - mock_response.status_code = 400 mock_error = APIStatusError("Bad request", response=mock_response, body=None) - with patch.object(client.devboxes, "create") as mock_create: - mock_create.side_effect = mock_error + with patch.object(client.devboxes, "_post") as mock_post: + mock_post.side_effect = mock_error with pytest.raises(APIStatusError, match="Bad request"): client.devboxes.create_and_await_running( @@ -1536,9 +1561,9 @@ def test_method_create_and_await_running_await_failure(self, client: Runloop) -> state_transitions=[], ) - with patch.object(client.devboxes, "create") as mock_create: + with patch.object(client.devboxes, "_post") as mock_post: with patch.object(client.devboxes, "await_running") as mock_await: - mock_create.return_value = mock_devbox_creating + mock_post.return_value = mock_devbox_creating mock_await.side_effect = RunloopError("Devbox entered non-running terminal state: failed") with pytest.raises(RunloopError, match="Devbox entered non-running terminal state: failed"): @@ -1739,6 +1764,51 @@ class TestAsyncDevboxes: "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] ) + @parametrize + async def test_method_create_and_await_running_success(self, async_client: AsyncRunloop) -> None: + mock_devbox_creating = DevboxView( + id="test_id", + status="provisioning", + capabilities=[], + create_time_ms=1234567890, + launch_parameters=LaunchParameters(resource_size_request="X_SMALL"), + metadata={}, + state_transitions=[], + ) + mock_devbox_running = mock_devbox_creating.model_copy(update={"status": "running"}) + + with patch.object(async_client.devboxes, "_post", new_callable=AsyncMock) as mock_post: + with patch.object(async_client.devboxes, "await_running", new_callable=AsyncMock) as mock_await: + mock_post.return_value = mock_devbox_creating + mock_await.return_value = mock_devbox_running + + result = await async_client.devboxes.create_and_await_running(name="test") + + assert result.status == "running" + assert mock_post.call_args.args[0] == "/v1/devboxes/create_and_await_running" + mock_await.assert_awaited_once_with("test_id", polling_config=None) + + @parametrize + async def test_method_create_and_await_running_optimistic_success(self, async_client: AsyncRunloop) -> None: + mock_devbox_running = DevboxView( + id="test_id", + status="running", + capabilities=[], + create_time_ms=1234567890, + launch_parameters=LaunchParameters(resource_size_request="X_SMALL"), + metadata={}, + state_transitions=[], + ) + + with patch.object(async_client.devboxes, "_post", new_callable=AsyncMock) as mock_post: + with patch.object(async_client.devboxes, "await_running", new_callable=AsyncMock) as mock_await: + mock_post.return_value = mock_devbox_running + + result = await async_client.devboxes.create_and_await_running(name="test") + + assert result.status == "running" + mock_await.assert_not_awaited() + @parametrize async def test_method_create(self, async_client: AsyncRunloop) -> None: devbox = await async_client.devboxes.create()