From 86b6ad9ddeb124c02eccabb44d95657871d43a0b Mon Sep 17 00:00:00 2001 From: Albert Li Date: Thu, 10 Jul 2025 18:01:40 -0700 Subject: [PATCH 1/3] Remove unnecessary arguments for await_running, await_completed, create_and_await_running --- .../resources/devboxes/devboxes.py | 28 ++----------------- .../resources/devboxes/executions.py | 24 +++------------- .../resources/scenarios/scenarios.py | 4 --- .../api_resources/devboxes/test_executions.py | 4 +-- 4 files changed, 8 insertions(+), 52 deletions(-) diff --git a/src/runloop_api_client/resources/devboxes/devboxes.py b/src/runloop_api_client/resources/devboxes/devboxes.py index 94df39be2..f310c427f 100644 --- a/src/runloop_api_client/resources/devboxes/devboxes.py +++ b/src/runloop_api_client/resources/devboxes/devboxes.py @@ -360,13 +360,8 @@ def await_running( self, id: str, *, + # Use polling_config to configure the "long" polling behavior. polling_config: PollingConfig | None = None, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> DevboxView: """Wait for a devbox to be in running state. @@ -392,9 +387,6 @@ def wait_for_devbox_status() -> DevboxView: return self._post( f"/v1/devboxes/{id}/wait_for_status", body={"statuses": ["running", "failure"]}, - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), cast_to=DevboxView, ) @@ -495,10 +487,6 @@ def create_and_await_running( return self.await_running( devbox.id, polling_config=polling_config, - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, ) def list( @@ -1662,23 +1650,14 @@ async def create_and_await_running( return await self.await_running( devbox.id, polling_config=polling_config, - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, ) async def await_running( self, id: str, *, + # Use polling_config to configure the "long" polling behavior. polling_config: PollingConfig | None = None, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> DevboxView: """Wait for a devbox to be in running state. @@ -1705,9 +1684,6 @@ async def wait_for_devbox_status() -> DevboxView: return await self._post( f"/v1/devboxes/{id}/wait_for_status", body={"statuses": ["running", "failure"]}, - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), cast_to=DevboxView, ) except APIStatusError as error: diff --git a/src/runloop_api_client/resources/devboxes/executions.py b/src/runloop_api_client/resources/devboxes/executions.py index 0481039ed..69a5ad3e0 100755 --- a/src/runloop_api_client/resources/devboxes/executions.py +++ b/src/runloop_api_client/resources/devboxes/executions.py @@ -97,13 +97,8 @@ def await_completed( execution_id: str, devbox_id: str, *, - config: PollingConfig | None = None, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + # Use polling_config to configure the "long" polling behavior. + polling_config: PollingConfig | None = None, ) -> DevboxAsyncExecutionDetailView: """Wait for an execution to complete. @@ -128,9 +123,6 @@ def wait_for_execution_status() -> DevboxAsyncExecutionDetailView: return self._post( f"/v1/devboxes/{devbox_id}/executions/{execution_id}/wait_for_status", body={"statuses": ["completed"]}, - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), cast_to=DevboxAsyncExecutionDetailView, ) @@ -152,7 +144,7 @@ def handle_timeout_error(error: Exception) -> DevboxAsyncExecutionDetailView: def is_done(execution: DevboxAsyncExecutionDetailView) -> bool: return execution.status == "completed" - return poll_until(wait_for_execution_status, is_done, config, handle_timeout_error) + return poll_until(wait_for_execution_status, is_done, polling_config, handle_timeout_error) def execute_async( self, @@ -390,13 +382,8 @@ async def await_completed( execution_id: str, *, devbox_id: str, + # Use polling_config to configure the "long" polling behavior. polling_config: PollingConfig | None = None, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> DevboxAsyncExecutionDetailView: """Wait for an execution to complete. @@ -421,9 +408,6 @@ async def wait_for_execution_status() -> DevboxAsyncExecutionDetailView: return await self._post( f"/v1/devboxes/{devbox_id}/executions/{execution_id}/wait_for_status", body={"statuses": ["completed"]}, - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), cast_to=DevboxAsyncExecutionDetailView, ) except APIStatusError as error: diff --git a/src/runloop_api_client/resources/scenarios/scenarios.py b/src/runloop_api_client/resources/scenarios/scenarios.py index 258b8d352..05f255a2f 100644 --- a/src/runloop_api_client/resources/scenarios/scenarios.py +++ b/src/runloop_api_client/resources/scenarios/scenarios.py @@ -471,10 +471,6 @@ def start_run_and_await_env_ready( self._client.devboxes.await_running( run.devbox_id, polling_config=polling_config, - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, ) return run diff --git a/tests/api_resources/devboxes/test_executions.py b/tests/api_resources/devboxes/test_executions.py index c712dc20b..c4d8897b7 100755 --- a/tests/api_resources/devboxes/test_executions.py +++ b/tests/api_resources/devboxes/test_executions.py @@ -338,7 +338,7 @@ def test_method_await_completed_with_config(self, client: Runloop) -> None: with patch.object(client.devboxes.executions, "_post") as mock_post: mock_post.return_value = mock_execution_completed - result = client.devboxes.executions.await_completed("execution_id", "devbox_id", config=config) + result = client.devboxes.executions.await_completed("execution_id", "devbox_id", polling_config=config) assert result.execution_id == "execution_id" assert result.status == "completed" @@ -361,7 +361,7 @@ def test_method_await_completed_polling_timeout(self, client: Runloop) -> None: mock_post.return_value = mock_execution_running with pytest.raises(PollingTimeout): - client.devboxes.executions.await_completed("execution_id", "devbox_id", config=config) + client.devboxes.executions.await_completed("execution_id", "devbox_id", polling_config=config) @parametrize def test_method_await_completed_various_statuses(self, client: Runloop) -> None: From 2a9eeb5cac3c5215a2d871b40280abd216397e81 Mon Sep 17 00:00:00 2001 From: Albert Li Date: Thu, 10 Jul 2025 18:12:36 -0700 Subject: [PATCH 2/3] Fix tests --- tests/api_resources/test_devboxes.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/tests/api_resources/test_devboxes.py b/tests/api_resources/test_devboxes.py index f27641463..f06af54f3 100644 --- a/tests/api_resources/test_devboxes.py +++ b/tests/api_resources/test_devboxes.py @@ -4,7 +4,7 @@ import os from typing import Any, cast -from unittest.mock import ANY, Mock, patch +from unittest.mock import Mock, patch import httpx import pytest @@ -1171,9 +1171,7 @@ 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() - mock_await.assert_called_once_with( - "test_id", polling_config=None, extra_headers=None, extra_query=None, extra_body=None, timeout=ANY - ) + mock_await.assert_called_once_with("test_id", polling_config=None) @parametrize def test_method_create_and_await_running_with_config(self, client: Runloop) -> None: @@ -1210,9 +1208,7 @@ def test_method_create_and_await_running_with_config(self, client: Runloop) -> N assert result.id == "test_id" assert result.status == "running" - mock_await.assert_called_once_with( - "test_id", polling_config=config, extra_headers=None, extra_query=None, extra_body=None, timeout=ANY - ) + mock_await.assert_called_once_with("test_id", polling_config=config) @parametrize def test_method_create_and_await_running_create_failure(self, client: Runloop) -> None: From 5a60355277ca485f04dd06a1a9b81140f0a50cc8 Mon Sep 17 00:00:00 2001 From: Albert Li Date: Fri, 11 Jul 2025 10:03:17 -0700 Subject: [PATCH 3/3] Clean up error handling in long polling --- .../resources/devboxes/devboxes.py | 59 +++++++++---------- .../resources/devboxes/executions.py | 46 +++++++-------- .../api_resources/devboxes/test_executions.py | 4 +- 3 files changed, 54 insertions(+), 55 deletions(-) diff --git a/src/runloop_api_client/resources/devboxes/devboxes.py b/src/runloop_api_client/resources/devboxes/devboxes.py index f310c427f..7ae6d2449 100644 --- a/src/runloop_api_client/resources/devboxes/devboxes.py +++ b/src/runloop_api_client/resources/devboxes/devboxes.py @@ -88,7 +88,7 @@ SyncDiskSnapshotsCursorIDPage, AsyncDiskSnapshotsCursorIDPage, ) -from ..._exceptions import RunloopError, APIStatusError +from ..._exceptions import RunloopError, APIStatusError, APITimeoutError from ...lib.polling import PollingConfig, poll_until from ..._base_client import AsyncPaginator, make_request_options from .disk_snapshots import ( @@ -115,6 +115,18 @@ DEVBOX_BOOTING_STATES = frozenset(("provisioning", "initializing")) +def placeholder_devbox_view(id: str) -> DevboxView: + return DevboxView( + id=id, + status="provisioning", + capabilities=[], + create_time_ms=0, + launch_parameters=SharedLaunchParameters(), + metadata={}, + state_transitions=[], + ) + + class DevboxesResource(SyncAPIResource): @cached_property def disk_snapshots(self) -> DiskSnapshotsResource: @@ -391,21 +403,15 @@ def wait_for_devbox_status() -> DevboxView: ) def handle_timeout_error(error: Exception) -> DevboxView: - # Handle 408 timeout errors by returning current devbox state to continue polling - if isinstance(error, APIStatusError) and error.response.status_code == 408: + # Handle timeout errors by returning current devbox state to continue polling + if isinstance(error, APITimeoutError) or ( + isinstance(error, APIStatusError) and error.response.status_code == 408 + ): # Return a placeholder result to continue polling - return DevboxView( - id=id, - status="provisioning", - capabilities=[], - create_time_ms=0, - launch_parameters=SharedLaunchParameters(), - metadata={}, - state_transitions=[], - ) - else: - # Re-raise other errors to stop polling - raise error + return placeholder_devbox_view(id) + + # Re-raise other errors to stop polling + raise error def is_done_booting(devbox: DevboxView) -> bool: return devbox.status not in DEVBOX_BOOTING_STATES @@ -1686,21 +1692,14 @@ async def wait_for_devbox_status() -> DevboxView: body={"statuses": ["running", "failure"]}, cast_to=DevboxView, ) - except APIStatusError as error: - if error.response.status_code == 408: - # Handle 408 timeout errors by returning a placeholder result to continue polling - return DevboxView( - id=id, - status="provisioning", - capabilities=[], - create_time_ms=0, - launch_parameters=SharedLaunchParameters(), - metadata={}, - state_transitions=[], - ) - else: - # Re-raise other errors to stop polling - raise + except (APITimeoutError, APIStatusError) as error: + # Handle timeout errors by returning current devbox state to continue polling + if isinstance(error, APITimeoutError) or error.response.status_code == 408: + # Return a placeholder result to continue polling + return placeholder_devbox_view(id) + + # Re-raise other errors to stop polling + raise def is_done_booting(devbox: DevboxView) -> bool: return devbox.status not in DEVBOX_BOOTING_STATES diff --git a/src/runloop_api_client/resources/devboxes/executions.py b/src/runloop_api_client/resources/devboxes/executions.py index 69a5ad3e0..99df1fd85 100755 --- a/src/runloop_api_client/resources/devboxes/executions.py +++ b/src/runloop_api_client/resources/devboxes/executions.py @@ -17,7 +17,7 @@ async_to_streamed_response_wrapper, ) from ..._constants import DEFAULT_TIMEOUT -from ..._exceptions import APIStatusError +from ..._exceptions import APIStatusError, APITimeoutError from ...lib.polling import PollingConfig, poll_until from ..._base_client import make_request_options from ...types.devboxes import execution_retrieve_params, execution_execute_sync_params, execution_execute_async_params @@ -28,6 +28,16 @@ __all__ = ["ExecutionsResource", "AsyncExecutionsResource"] +def placeholder_execution_detail_view(devbox_id: str, execution_id: str) -> DevboxAsyncExecutionDetailView: + return DevboxAsyncExecutionDetailView( + devbox_id=devbox_id, + execution_id=execution_id, + status="queued", + stdout="", + stderr="", + ) + + class ExecutionsResource(SyncAPIResource): @cached_property def with_raw_response(self) -> ExecutionsResourceWithRawResponse: @@ -127,16 +137,12 @@ def wait_for_execution_status() -> DevboxAsyncExecutionDetailView: ) def handle_timeout_error(error: Exception) -> DevboxAsyncExecutionDetailView: - # Handle 408 timeout errors by returning current execution state to continue polling - if isinstance(error, APIStatusError) and error.response.status_code == 408: + # Handle timeout errors by returning current execution state to continue polling + if isinstance(error, APITimeoutError) or ( + isinstance(error, APIStatusError) and error.response.status_code == 408 + ): # Return a placeholder result to continue polling - return DevboxAsyncExecutionDetailView( - devbox_id=devbox_id, - execution_id=execution_id, - status="queued", - stdout="", - stderr="", - ) + return placeholder_execution_detail_view(devbox_id, execution_id) else: # Re-raise other errors to stop polling raise error @@ -410,19 +416,13 @@ async def wait_for_execution_status() -> DevboxAsyncExecutionDetailView: body={"statuses": ["completed"]}, cast_to=DevboxAsyncExecutionDetailView, ) - except APIStatusError as error: - if error.response.status_code == 408: - # Handle 408 timeout errors by returning current execution state to continue polling - return DevboxAsyncExecutionDetailView( - devbox_id=devbox_id, - execution_id=execution_id, - status="queued", - stdout="", - stderr="", - ) - else: - # Re-raise other errors to stop polling - raise + except (APITimeoutError, APIStatusError) as error: + # Handle timeout errors by returning placeholder to continue polling + if isinstance(error, APITimeoutError) or error.response.status_code == 408: + return placeholder_execution_detail_view(devbox_id, execution_id) + + # Re-raise other errors to stop polling + raise def is_done(execution: DevboxAsyncExecutionDetailView) -> bool: return execution.status == "completed" diff --git a/tests/api_resources/devboxes/test_executions.py b/tests/api_resources/devboxes/test_executions.py index c4d8897b7..5818a94d7 100755 --- a/tests/api_resources/devboxes/test_executions.py +++ b/tests/api_resources/devboxes/test_executions.py @@ -11,7 +11,7 @@ from tests.utils import assert_matches_type from runloop_api_client import Runloop, AsyncRunloop from runloop_api_client.types import DevboxExecutionDetailView, DevboxAsyncExecutionDetailView -from runloop_api_client._exceptions import APIStatusError +from runloop_api_client._exceptions import APIStatusError, APITimeoutError from runloop_api_client.lib.polling import PollingConfig, PollingTimeout base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -665,7 +665,7 @@ async def test_method_await_completed_timeout_handling(self, async_client: Async # Create a mock 408 response mock_response = Mock() mock_response.status_code = 408 - mock_408_error = APIStatusError("Request timeout", response=mock_response, body=None) + mock_408_error = APITimeoutError(request=mock_response.request) mock_execution_completed = DevboxAsyncExecutionDetailView( devbox_id="devbox_id",