Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 66 additions & 44 deletions src/runloop_api_client/resources/devboxes/devboxes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
90 changes: 80 additions & 10 deletions tests/api_resources/test_devboxes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand All @@ -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"""
Expand All @@ -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(
Expand All @@ -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(
Expand All @@ -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"):
Expand Down Expand Up @@ -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()
Expand Down