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
26 changes: 22 additions & 4 deletions src/runloop_api_client/sdk/async_devbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,18 +172,36 @@ async def suspend(

async def resume(
self,
*,
polling_config: PollingConfig | None = None,
**options: Unpack[LongRequestOptions],
) -> DevboxView:
"""Resume a suspended devbox.
"""Resume a suspended devbox, restoring it to running state.

Returns immediately after issuing the resume request. Call
:meth:`await_running` if you need to wait for the devbox to reach the
``running`` state (contrast with the synchronous SDK, which blocks).
Waits for the devbox to reach running state before returning.

:param polling_config: Optional polling behavior overrides, defaults to None
:type polling_config: PollingConfig | None, optional
:param options: Optional long-running request configuration
:return: Resumed devbox state info
:rtype: DevboxView
"""
await self.resume_async(**options)
return await self.await_running(polling_config=polling_config)

async def resume_async(
self,
**options: Unpack[LongRequestOptions],
) -> DevboxView:
"""Resume a suspended devbox without waiting for it to reach running state.

Initiates the resume operation and returns immediately. Use :meth:`await_running`
to wait for the devbox to reach running state if needed.

:param options: Optional long-running request configuration
:return: Devbox state info immediately after resume request
:rtype: DevboxView
"""
return await self._client.devboxes.resume(
self._id,
**options,
Expand Down
21 changes: 18 additions & 3 deletions src/runloop_api_client/sdk/devbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,11 +185,26 @@ def resume(
:return: Resumed devbox state info
:rtype: :class:`~runloop_api_client.types.devbox_view.DevboxView`
"""
self._client.devboxes.resume(
self.resume_async(**filter_params(options, LongRequestOptions))
return self._client.devboxes.await_running(self._id, polling_config=options.get("polling_config"))

def resume_async(
self,
**options: Unpack[LongRequestOptions],
) -> DevboxView:
"""Resume a suspended devbox without waiting for it to reach running state.

Initiates the resume operation and returns immediately. Use :meth:`await_running`
to wait for the devbox to reach running state if needed.

:param options: Optional long-running request configuration
:return: Devbox state info immediately after resume request
:rtype: :class:`~runloop_api_client.types.devbox_view.DevboxView`
"""
return self._client.devboxes.resume(
self._id,
**filter_params(options, LongRequestOptions),
**options,
)
return self._client.devboxes.await_running(self._id, polling_config=options.get("polling_config"))

def keep_alive(
self,
Expand Down
31 changes: 31 additions & 0 deletions tests/sdk/async_devbox/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,9 +161,40 @@ async def test_suspend(self, mock_async_client: AsyncMock, devbox_view: MockDevb
async def test_resume(self, mock_async_client: AsyncMock, devbox_view: MockDevboxView) -> None:
"""Test resume method."""
mock_async_client.devboxes.resume = AsyncMock(return_value=devbox_view)
mock_async_client.devboxes.await_running = AsyncMock(return_value=devbox_view)
polling_config = PollingConfig(timeout_seconds=60.0)

devbox = AsyncDevbox(mock_async_client, "dev_123")
result = await devbox.resume(
polling_config=polling_config,
extra_headers={"X-Custom": "value"},
extra_query={"param": "value"},
extra_body={"key": "value"},
timeout=30.0,
idempotency_key="key-123",
)

assert result == devbox_view
mock_async_client.devboxes.resume.assert_called_once_with(
"dev_123",
extra_headers={"X-Custom": "value"},
extra_query={"param": "value"},
extra_body={"key": "value"},
timeout=30.0,
idempotency_key="key-123",
)
mock_async_client.devboxes.await_running.assert_called_once_with(
"dev_123",
polling_config=polling_config,
)

@pytest.mark.asyncio
async def test_resume_async(self, mock_async_client: AsyncMock, devbox_view: MockDevboxView) -> None:
"""Test resume_async method."""
mock_async_client.devboxes.resume = AsyncMock(return_value=devbox_view)

devbox = AsyncDevbox(mock_async_client, "dev_123")
result = await devbox.resume_async(
extra_headers={"X-Custom": "value"},
extra_query={"param": "value"},
extra_body={"key": "value"},
Expand Down
26 changes: 26 additions & 0 deletions tests/sdk/devbox/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,32 @@ def test_resume(self, mock_client: Mock, devbox_view: MockDevboxView) -> None:
polling_config=polling_config,
)

def test_resume_async(self, mock_client: Mock, devbox_view: MockDevboxView) -> None:
"""Test resume_async method."""
mock_client.devboxes.resume.return_value = devbox_view
mock_client.devboxes.await_running = Mock()

devbox = Devbox(mock_client, "dev_123")
result = devbox.resume_async(
extra_headers={"X-Custom": "value"},
extra_query={"param": "value"},
extra_body={"key": "value"},
timeout=30.0,
idempotency_key="key-123",
)

assert result == devbox_view
mock_client.devboxes.resume.assert_called_once_with(
"dev_123",
extra_headers={"X-Custom": "value"},
extra_query={"param": "value"},
extra_body={"key": "value"},
timeout=30.0,
idempotency_key="key-123",
)
# Should not call await_running
mock_client.devboxes.await_running.assert_not_called()

def test_keep_alive(self, mock_client: Mock) -> None:
"""Test keep_alive method."""
mock_client.devboxes.keep_alive.return_value = object()
Expand Down
47 changes: 41 additions & 6 deletions tests/smoketests/sdk/test_async_devbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -346,12 +346,10 @@ async def test_suspend_and_resume(self, async_sdk_client: AsyncRunloopSDK) -> No
info = await devbox.get_info()
assert info.status == "suspended"

# Resume the devbox
resumed_info = await devbox.resume()
if resumed_info.status != "running":
resumed_info = await devbox.await_running(
polling_config=PollingConfig(timeout_seconds=120.0, interval_seconds=5.0)
)
# Resume the devbox - resume() automatically waits for running state
resumed_info = await devbox.resume(
polling_config=PollingConfig(timeout_seconds=120.0, interval_seconds=5.0)
)
assert resumed_info.status == "running"

# Verify running state
Expand All @@ -360,6 +358,43 @@ async def test_suspend_and_resume(self, async_sdk_client: AsyncRunloopSDK) -> No
finally:
await devbox.shutdown()

@pytest.mark.timeout(TWO_MINUTE_TIMEOUT)
async def test_resume_async(self, async_sdk_client: AsyncRunloopSDK) -> None:
"""Test resuming a devbox asynchronously without waiting."""
devbox = await async_sdk_client.devbox.create(
name=unique_name("sdk-async-devbox-resume-async"),
launch_parameters={"resource_size_request": "SMALL", "keep_alive_time_seconds": 60 * 5},
)

try:
# Suspend the devbox
suspended_info = await devbox.suspend()
if suspended_info.status != "suspended":
suspended_info = await devbox.await_suspended(
polling_config=PollingConfig(timeout_seconds=120.0, interval_seconds=5.0)
)
assert suspended_info.status == "suspended"

# Verify suspended state
info = await devbox.get_info()
assert info.status == "suspended"

# Resume the devbox asynchronously - doesn't wait automatically
resume_response = await devbox.resume_async()
assert resume_response is not None

# Status might still be suspended or transitioning
info_after_resume = await devbox.get_info()
assert info_after_resume.status in ["suspended", "running", "starting"]

# Now wait for running state explicitly
running_info = await devbox.await_running(
polling_config=PollingConfig(timeout_seconds=120.0, interval_seconds=5.0)
)
assert running_info.status == "running"
finally:
await devbox.shutdown()

@pytest.mark.timeout(TWO_MINUTE_TIMEOUT)
async def test_await_running(self, async_sdk_client: AsyncRunloopSDK) -> None:
"""Test await_running method."""
Expand Down
37 changes: 36 additions & 1 deletion tests/smoketests/sdk/test_devbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -345,7 +345,7 @@ def test_suspend_and_resume(self, sdk_client: RunloopSDK) -> None:
info = devbox.get_info()
assert info.status == "suspended"

# Resume the devbox
# Resume the devbox - resume() automatically waits for running state
resumed_info = devbox.resume(
polling_config=PollingConfig(timeout_seconds=120.0, interval_seconds=5.0),
)
Expand All @@ -357,6 +357,41 @@ def test_suspend_and_resume(self, sdk_client: RunloopSDK) -> None:
finally:
devbox.shutdown()

@pytest.mark.timeout(TWO_MINUTE_TIMEOUT)
def test_resume_async(self, sdk_client: RunloopSDK) -> None:
"""Test resuming a devbox asynchronously without waiting."""
devbox = sdk_client.devbox.create(
name=unique_name("sdk-devbox-resume-async"),
launch_parameters={"resource_size_request": "SMALL", "keep_alive_time_seconds": 60 * 5},
)

try:
# Suspend the devbox
suspended_info = devbox.suspend(
polling_config=PollingConfig(timeout_seconds=120.0, interval_seconds=5.0),
)
assert suspended_info.status == "suspended"

# Verify suspended state
info = devbox.get_info()
assert info.status == "suspended"

# Resume the devbox asynchronously - doesn't wait automatically
resume_response = devbox.resume_async()
assert resume_response is not None

# Status might still be suspended or transitioning
info_after_resume = devbox.get_info()
assert info_after_resume.status in ["suspended", "running", "starting"]

# Now wait for running state explicitly
running_info = devbox.await_running(
polling_config=PollingConfig(timeout_seconds=120.0, interval_seconds=5.0)
)
assert running_info.status == "running"
finally:
devbox.shutdown()

@pytest.mark.timeout(TWO_MINUTE_TIMEOUT)
def test_await_running(self, sdk_client: RunloopSDK) -> None:
"""Test await_running method."""
Expand Down