Skip to content

Commit 802172a

Browse files
release: 1.30.0 (#832)
Co-authored-by: Adam Lesinski <adam@runloop.ai> Co-authored-by: stainless-app[bot] <142633134+stainless-app[bot]@users.noreply.github.com>
1 parent ae5ffd5 commit 802172a

8 files changed

Lines changed: 181 additions & 63 deletions

File tree

.release-please-manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{
2-
".": "1.29.0"
2+
".": "1.30.0"
33
}

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,18 @@
11
# Changelog
22

3+
## 1.30.0 (2026-08-20)
4+
5+
Full Changelog: [v1.29.0...v1.30.0](https://github.com/runloopai/api-client-python/compare/v1.29.0...v1.30.0)
6+
7+
### Features
8+
9+
* **devboxes:** use optimistic create endpoint ([#829](https://github.com/runloopai/api-client-python/issues/829)) ([3f47b22](https://github.com/runloopai/api-client-python/commit/3f47b2256189b1aca4c4137134df2643dd3d7ca5))
10+
11+
12+
### Chores
13+
14+
* **internal:** allow the mock server port to be set with STAINLESS_MOCK_PORT ([b7a3d87](https://github.com/runloopai/api-client-python/commit/b7a3d87997538ff6d4658df77d68177f83811eb3))
15+
316
## 1.29.0 (2026-08-20)
417

518
Full Changelog: [v1.28.0...v1.29.0](https://github.com/runloopai/api-client-python/compare/v1.28.0...v1.29.0)

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "runloop_api_client"
3-
version = "1.29.0"
3+
version = "1.30.0"
44
description = "The official Python library for the runloop API"
55
dynamic = ["readme"]
66
license = "MIT"

scripts/mock

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ set -e
44

55
cd "$(dirname "$0")/.."
66

7+
if [ "$1" == "--install" ]; then
8+
npm exec --package=@stdy/cli@0.22.1 -- steady --version
9+
exit 0
10+
fi
11+
712
if [[ -n "$1" && "$1" != '--'* ]]; then
813
URL="$1"
914
shift
@@ -20,16 +25,18 @@ fi
2025
echo "==> Starting mock server with URL ${URL}"
2126

2227
# Run steady mock on the given spec
28+
MOCK_PORT="${STAINLESS_MOCK_PORT:-4010}"
29+
2330
if [ "$1" == "--daemon" ]; then
2431
# Pre-install the package so the download doesn't eat into the startup timeout
2532
npm exec --package=@stdy/cli@0.22.1 -- steady --version
2633

27-
npm exec --package=@stdy/cli@0.22.1 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-form-array-format=comma --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL" &> .stdy.log &
34+
npm exec --package=@stdy/cli@0.22.1 -- steady --host 127.0.0.1 -p "$MOCK_PORT" --validator-query-array-format=comma --validator-form-array-format=comma --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL" &> .stdy.log &
2835

2936
# Wait for server to come online via health endpoint (max 30s)
3037
echo -n "Waiting for server"
3138
attempts=0
32-
while ! curl --silent --fail "http://127.0.0.1:4010/_x-steady/health" >/dev/null 2>&1; do
39+
while ! curl --silent --fail "http://127.0.0.1:$MOCK_PORT/_x-steady/health" >/dev/null 2>&1; do
3340
if ! kill -0 $! 2>/dev/null; then
3441
echo
3542
cat .stdy.log
@@ -48,5 +55,5 @@ if [ "$1" == "--daemon" ]; then
4855

4956
echo
5057
else
51-
npm exec --package=@stdy/cli@0.22.1 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-form-array-format=comma --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL"
58+
npm exec --package=@stdy/cli@0.22.1 -- steady --host 127.0.0.1 -p "$MOCK_PORT" --validator-query-array-format=comma --validator-form-array-format=comma --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL"
5259
fi

scripts/test

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@ GREEN='\033[0;32m'
99
YELLOW='\033[0;33m'
1010
NC='\033[0m' # No Color
1111

12+
MOCK_PORT="${STAINLESS_MOCK_PORT:-4010}"
13+
1214
function steady_is_running() {
13-
curl --silent "http://127.0.0.1:4010/_x-steady/health" >/dev/null 2>&1
15+
curl --silent "http://127.0.0.1:$MOCK_PORT/_x-steady/health" >/dev/null 2>&1
1416
}
1517

1618
find_pids_on_port_via_proc() {
@@ -60,7 +62,7 @@ function is_overriding_api_base_url() {
6062

6163
if ! is_overriding_api_base_url && ! steady_is_running ; then
6264
# When we exit this script, make sure to kill the background mock server process
63-
trap 'kill_server_on_port 4010' EXIT
65+
trap 'kill_server_on_port "$MOCK_PORT"' EXIT
6466

6567
# Start the dev server
6668
./scripts/mock --daemon
@@ -76,7 +78,7 @@ elif ! steady_is_running ; then
7678
echo -e "To run the server, pass in the path or url of your OpenAPI"
7779
echo -e "spec to the steady command:"
7880
echo
79-
echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.22.1 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-form-array-format=comma --validator-query-object-format=brackets --validator-form-object-format=brackets${NC}"
81+
echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.22.1 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p $MOCK_PORT --validator-query-array-format=comma --validator-form-array-format=comma --validator-query-object-format=brackets --validator-form-object-format=brackets${NC}"
8082
echo
8183

8284
exit 1
@@ -85,6 +87,10 @@ else
8587
echo
8688
fi
8789

90+
if [ -n "${STAINLESS_MOCK_PORT:-}" ] && ! is_overriding_api_base_url ; then
91+
export TEST_API_BASE_URL="http://127.0.0.1:$MOCK_PORT"
92+
fi
93+
8894
export DEFER_PYDANTIC_BUILD=false
8995

9096
# Note that we need to specify the patch version here so that uv

src/runloop_api_client/_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
22

33
__title__ = "runloop_api_client"
4-
__version__ = "1.29.0" # x-release-please-version
4+
__version__ = "1.30.0" # x-release-please-version

src/runloop_api_client/resources/devboxes/devboxes.py

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

535+
if devbox.status == "running":
536+
return devbox
537+
527538
return self.await_running(
528539
devbox.id,
529540
polling_config=polling_config,
@@ -2071,30 +2082,41 @@ async def create_and_await_running(
20712082
RunloopError: If devbox enters a non-running terminal state
20722083
"""
20732084

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

2117+
if devbox.status == "running":
2118+
return devbox
2119+
20982120
return await self.await_running(
20992121
devbox.id,
21002122
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)