Skip to content

Commit 660506c

Browse files
committed
feat(devboxes): use optimistic create endpoint
1 parent c63f011 commit 660506c

2 files changed

Lines changed: 146 additions & 54 deletions

File tree

src/runloop_api_client/resources/devboxes/devboxes.py

Lines changed: 66 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -497,30 +497,41 @@ def create_and_await_running(
497497
PollingTimeout: If polling times out before devbox is running
498498
RunloopError: If devbox enters a non-running terminal state
499499
"""
500-
# Pass all create_args to the underlying create method
501-
devbox = self.create(
502-
blueprint_id=blueprint_id,
503-
blueprint_name=blueprint_name,
504-
code_mounts=code_mounts,
505-
entrypoint=entrypoint,
506-
environment_variables=environment_variables,
507-
file_mounts=file_mounts,
508-
gateways=gateways,
509-
launch_parameters=launch_parameters,
510-
mcp=mcp,
511-
metadata=metadata,
512-
mounts=mounts,
513-
name=name,
514-
secrets=secrets,
515-
snapshot_id=snapshot_id,
516-
tunnel=tunnel,
517-
extra_headers=extra_headers,
518-
extra_query=extra_query,
519-
extra_body=extra_body,
520-
timeout=timeout,
521-
idempotency_key=idempotency_key,
500+
devbox = self._post(
501+
"/v1/devboxes/create_and_await_running",
502+
body=maybe_transform(
503+
{
504+
"blueprint_id": blueprint_id,
505+
"blueprint_name": blueprint_name,
506+
"code_mounts": code_mounts,
507+
"entrypoint": entrypoint,
508+
"environment_variables": environment_variables,
509+
"file_mounts": file_mounts,
510+
"gateways": gateways,
511+
"launch_parameters": launch_parameters,
512+
"mcp": mcp,
513+
"metadata": metadata,
514+
"mounts": mounts,
515+
"name": name,
516+
"secrets": secrets,
517+
"snapshot_id": snapshot_id,
518+
"tunnel": tunnel,
519+
},
520+
devbox_create_params.DevboxCreateParams,
521+
),
522+
options=make_request_options(
523+
extra_headers=extra_headers,
524+
extra_query=extra_query,
525+
extra_body=extra_body,
526+
timeout=timeout,
527+
idempotency_key=idempotency_key,
528+
),
529+
cast_to=DevboxView,
522530
)
523531

532+
if devbox.status == "running":
533+
return devbox
534+
524535
return self.await_running(
525536
devbox.id,
526537
polling_config=polling_config,
@@ -2068,30 +2079,41 @@ async def create_and_await_running(
20682079
RunloopError: If devbox enters a non-running terminal state
20692080
"""
20702081

2071-
# Pass all create_args, relevant request args to the underlying create method
2072-
devbox = await self.create(
2073-
blueprint_id=blueprint_id,
2074-
blueprint_name=blueprint_name,
2075-
code_mounts=code_mounts,
2076-
entrypoint=entrypoint,
2077-
environment_variables=environment_variables,
2078-
file_mounts=file_mounts,
2079-
gateways=gateways,
2080-
launch_parameters=launch_parameters,
2081-
mcp=mcp,
2082-
metadata=metadata,
2083-
mounts=mounts,
2084-
name=name,
2085-
secrets=secrets,
2086-
snapshot_id=snapshot_id,
2087-
tunnel=tunnel,
2088-
extra_headers=extra_headers,
2089-
extra_query=extra_query,
2090-
extra_body=extra_body,
2091-
timeout=timeout,
2092-
idempotency_key=idempotency_key,
2082+
devbox = await self._post(
2083+
"/v1/devboxes/create_and_await_running",
2084+
body=await async_maybe_transform(
2085+
{
2086+
"blueprint_id": blueprint_id,
2087+
"blueprint_name": blueprint_name,
2088+
"code_mounts": code_mounts,
2089+
"entrypoint": entrypoint,
2090+
"environment_variables": environment_variables,
2091+
"file_mounts": file_mounts,
2092+
"gateways": gateways,
2093+
"launch_parameters": launch_parameters,
2094+
"mcp": mcp,
2095+
"metadata": metadata,
2096+
"mounts": mounts,
2097+
"name": name,
2098+
"secrets": secrets,
2099+
"snapshot_id": snapshot_id,
2100+
"tunnel": tunnel,
2101+
},
2102+
devbox_create_params.DevboxCreateParams,
2103+
),
2104+
options=make_request_options(
2105+
extra_headers=extra_headers,
2106+
extra_query=extra_query,
2107+
extra_body=extra_body,
2108+
timeout=timeout,
2109+
idempotency_key=idempotency_key,
2110+
),
2111+
cast_to=DevboxView,
20932112
)
20942113

2114+
if devbox.status == "running":
2115+
return devbox
2116+
20952117
return await self.await_running(
20962118
devbox.id,
20972119
polling_config=polling_config,

tests/api_resources/test_devboxes.py

Lines changed: 80 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
import os
66
from typing import Any, cast
7-
from unittest.mock import Mock, patch
7+
from unittest.mock import Mock, AsyncMock, patch
88

99
import httpx
1010
import pytest
@@ -1452,9 +1452,9 @@ def test_method_create_and_await_running_success(self, client: Runloop) -> None:
14521452
state_transitions=[],
14531453
)
14541454

1455-
with patch.object(client.devboxes, "create") as mock_create:
1455+
with patch.object(client.devboxes, "_post") as mock_post:
14561456
with patch.object(client.devboxes, "await_running") as mock_await:
1457-
mock_create.return_value = mock_devbox_creating
1457+
mock_post.return_value = mock_devbox_creating
14581458
mock_await.return_value = mock_devbox_running
14591459

14601460
result = client.devboxes.create_and_await_running(
@@ -1463,9 +1463,34 @@ def test_method_create_and_await_running_success(self, client: Runloop) -> None:
14631463

14641464
assert result.id == "test_id"
14651465
assert result.status == "running"
1466-
mock_create.assert_called_once()
1466+
assert mock_post.call_args.args[0] == "/v1/devboxes/create_and_await_running"
1467+
assert mock_post.call_args.kwargs["body"]["name"] == "test"
14671468
mock_await.assert_called_once_with("test_id", polling_config=None)
14681469

1470+
@parametrize
1471+
def test_method_create_and_await_running_optimistic_success(self, client: Runloop) -> None:
1472+
"""Test create_and_await_running returns a running response without polling."""
1473+
1474+
mock_devbox_running = DevboxView(
1475+
id="test_id",
1476+
status="running",
1477+
capabilities=[],
1478+
create_time_ms=1234567890,
1479+
launch_parameters=LaunchParameters(resource_size_request="X_SMALL"),
1480+
metadata={},
1481+
state_transitions=[],
1482+
)
1483+
1484+
with patch.object(client.devboxes, "_post") as mock_post:
1485+
with patch.object(client.devboxes, "await_running") as mock_await:
1486+
mock_post.return_value = mock_devbox_running
1487+
1488+
result = client.devboxes.create_and_await_running(name="test")
1489+
1490+
assert result.status == "running"
1491+
assert mock_post.call_args.args[0] == "/v1/devboxes/create_and_await_running"
1492+
mock_await.assert_not_called()
1493+
14691494
@parametrize
14701495
def test_method_create_and_await_running_with_config(self, client: Runloop) -> None:
14711496
"""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
14921517

14931518
config = PollingConfig(interval_seconds=0.1, max_attempts=10)
14941519

1495-
with patch.object(client.devboxes, "create") as mock_create:
1520+
with patch.object(client.devboxes, "_post") as mock_post:
14961521
with patch.object(client.devboxes, "await_running") as mock_await:
1497-
mock_create.return_value = mock_devbox_creating
1522+
mock_post.return_value = mock_devbox_creating
14981523
mock_await.return_value = mock_devbox_running
14991524

15001525
result = client.devboxes.create_and_await_running(
@@ -1514,8 +1539,8 @@ def test_method_create_and_await_running_create_failure(self, client: Runloop) -
15141539
mock_response.status_code = 400
15151540
mock_error = APIStatusError("Bad request", response=mock_response, body=None)
15161541

1517-
with patch.object(client.devboxes, "create") as mock_create:
1518-
mock_create.side_effect = mock_error
1542+
with patch.object(client.devboxes, "_post") as mock_post:
1543+
mock_post.side_effect = mock_error
15191544

15201545
with pytest.raises(APIStatusError, match="Bad request"):
15211546
client.devboxes.create_and_await_running(
@@ -1536,9 +1561,9 @@ def test_method_create_and_await_running_await_failure(self, client: Runloop) ->
15361561
state_transitions=[],
15371562
)
15381563

1539-
with patch.object(client.devboxes, "create") as mock_create:
1564+
with patch.object(client.devboxes, "_post") as mock_post:
15401565
with patch.object(client.devboxes, "await_running") as mock_await:
1541-
mock_create.return_value = mock_devbox_creating
1566+
mock_post.return_value = mock_devbox_creating
15421567
mock_await.side_effect = RunloopError("Devbox entered non-running terminal state: failed")
15431568

15441569
with pytest.raises(RunloopError, match="Devbox entered non-running terminal state: failed"):
@@ -1739,6 +1764,51 @@ class TestAsyncDevboxes:
17391764
"async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"]
17401765
)
17411766

1767+
@parametrize
1768+
async def test_method_create_and_await_running_success(self, async_client: AsyncRunloop) -> None:
1769+
mock_devbox_creating = DevboxView(
1770+
id="test_id",
1771+
status="provisioning",
1772+
capabilities=[],
1773+
create_time_ms=1234567890,
1774+
launch_parameters=LaunchParameters(resource_size_request="X_SMALL"),
1775+
metadata={},
1776+
state_transitions=[],
1777+
)
1778+
mock_devbox_running = mock_devbox_creating.model_copy(update={"status": "running"})
1779+
1780+
with patch.object(async_client.devboxes, "_post", new_callable=AsyncMock) as mock_post:
1781+
with patch.object(async_client.devboxes, "await_running", new_callable=AsyncMock) as mock_await:
1782+
mock_post.return_value = mock_devbox_creating
1783+
mock_await.return_value = mock_devbox_running
1784+
1785+
result = await async_client.devboxes.create_and_await_running(name="test")
1786+
1787+
assert result.status == "running"
1788+
assert mock_post.call_args.args[0] == "/v1/devboxes/create_and_await_running"
1789+
mock_await.assert_awaited_once_with("test_id", polling_config=None)
1790+
1791+
@parametrize
1792+
async def test_method_create_and_await_running_optimistic_success(self, async_client: AsyncRunloop) -> None:
1793+
mock_devbox_running = DevboxView(
1794+
id="test_id",
1795+
status="running",
1796+
capabilities=[],
1797+
create_time_ms=1234567890,
1798+
launch_parameters=LaunchParameters(resource_size_request="X_SMALL"),
1799+
metadata={},
1800+
state_transitions=[],
1801+
)
1802+
1803+
with patch.object(async_client.devboxes, "_post", new_callable=AsyncMock) as mock_post:
1804+
with patch.object(async_client.devboxes, "await_running", new_callable=AsyncMock) as mock_await:
1805+
mock_post.return_value = mock_devbox_running
1806+
1807+
result = await async_client.devboxes.create_and_await_running(name="test")
1808+
1809+
assert result.status == "running"
1810+
mock_await.assert_not_awaited()
1811+
17421812
@parametrize
17431813
async def test_method_create(self, async_client: AsyncRunloop) -> None:
17441814
devbox = await async_client.devboxes.create()

0 commit comments

Comments
 (0)