diff --git a/src/runloop_api_client/sdk/async_devbox.py b/src/runloop_api_client/sdk/async_devbox.py index 89080abef..d935ec269 100644 --- a/src/runloop_api_client/sdk/async_devbox.py +++ b/src/runloop_api_client/sdk/async_devbox.py @@ -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, diff --git a/src/runloop_api_client/sdk/devbox.py b/src/runloop_api_client/sdk/devbox.py index cad396815..77b6fdf16 100644 --- a/src/runloop_api_client/sdk/devbox.py +++ b/src/runloop_api_client/sdk/devbox.py @@ -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, diff --git a/tests/sdk/async_devbox/test_core.py b/tests/sdk/async_devbox/test_core.py index 5d3405c80..6fdd0eb10 100644 --- a/tests/sdk/async_devbox/test_core.py +++ b/tests/sdk/async_devbox/test_core.py @@ -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"}, diff --git a/tests/sdk/devbox/test_core.py b/tests/sdk/devbox/test_core.py index b482e030b..0bd5b211e 100644 --- a/tests/sdk/devbox/test_core.py +++ b/tests/sdk/devbox/test_core.py @@ -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() diff --git a/tests/smoketests/sdk/test_async_devbox.py b/tests/smoketests/sdk/test_async_devbox.py index fdaaa91ed..cbf26dd25 100644 --- a/tests/smoketests/sdk/test_async_devbox.py +++ b/tests/smoketests/sdk/test_async_devbox.py @@ -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 @@ -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.""" diff --git a/tests/smoketests/sdk/test_devbox.py b/tests/smoketests/sdk/test_devbox.py index 7238d27bb..801d725c0 100644 --- a/tests/smoketests/sdk/test_devbox.py +++ b/tests/smoketests/sdk/test_devbox.py @@ -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), ) @@ -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."""