Skip to content

Commit 6ec8442

Browse files
authored
Merge pull request #616 from runloopai/alb/long-poll-updates
Remove unnecessary arguments for await_running, await_completed, create_and_await_running
2 parents cb6712d + 5a60355 commit 6ec8442

5 files changed

Lines changed: 65 additions & 114 deletions

File tree

src/runloop_api_client/resources/devboxes/devboxes.py

Lines changed: 31 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@
8888
SyncDiskSnapshotsCursorIDPage,
8989
AsyncDiskSnapshotsCursorIDPage,
9090
)
91-
from ..._exceptions import RunloopError, APIStatusError
91+
from ..._exceptions import RunloopError, APIStatusError, APITimeoutError
9292
from ...lib.polling import PollingConfig, poll_until
9393
from ..._base_client import AsyncPaginator, make_request_options
9494
from .disk_snapshots import (
@@ -115,6 +115,18 @@
115115
DEVBOX_BOOTING_STATES = frozenset(("provisioning", "initializing"))
116116

117117

118+
def placeholder_devbox_view(id: str) -> DevboxView:
119+
return DevboxView(
120+
id=id,
121+
status="provisioning",
122+
capabilities=[],
123+
create_time_ms=0,
124+
launch_parameters=SharedLaunchParameters(),
125+
metadata={},
126+
state_transitions=[],
127+
)
128+
129+
118130
class DevboxesResource(SyncAPIResource):
119131
@cached_property
120132
def disk_snapshots(self) -> DiskSnapshotsResource:
@@ -360,13 +372,8 @@ def await_running(
360372
self,
361373
id: str,
362374
*,
375+
# Use polling_config to configure the "long" polling behavior.
363376
polling_config: PollingConfig | None = None,
364-
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
365-
# The extra values given here take precedence over values defined on the client or passed to this method.
366-
extra_headers: Headers | None = None,
367-
extra_query: Query | None = None,
368-
extra_body: Body | None = None,
369-
timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN,
370377
) -> DevboxView:
371378
"""Wait for a devbox to be in running state.
372379
@@ -392,28 +399,19 @@ def wait_for_devbox_status() -> DevboxView:
392399
return self._post(
393400
f"/v1/devboxes/{id}/wait_for_status",
394401
body={"statuses": ["running", "failure"]},
395-
options=make_request_options(
396-
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
397-
),
398402
cast_to=DevboxView,
399403
)
400404

401405
def handle_timeout_error(error: Exception) -> DevboxView:
402-
# Handle 408 timeout errors by returning current devbox state to continue polling
403-
if isinstance(error, APIStatusError) and error.response.status_code == 408:
406+
# Handle timeout errors by returning current devbox state to continue polling
407+
if isinstance(error, APITimeoutError) or (
408+
isinstance(error, APIStatusError) and error.response.status_code == 408
409+
):
404410
# Return a placeholder result to continue polling
405-
return DevboxView(
406-
id=id,
407-
status="provisioning",
408-
capabilities=[],
409-
create_time_ms=0,
410-
launch_parameters=SharedLaunchParameters(),
411-
metadata={},
412-
state_transitions=[],
413-
)
414-
else:
415-
# Re-raise other errors to stop polling
416-
raise error
411+
return placeholder_devbox_view(id)
412+
413+
# Re-raise other errors to stop polling
414+
raise error
417415

418416
def is_done_booting(devbox: DevboxView) -> bool:
419417
return devbox.status not in DEVBOX_BOOTING_STATES
@@ -495,10 +493,6 @@ def create_and_await_running(
495493
return self.await_running(
496494
devbox.id,
497495
polling_config=polling_config,
498-
extra_headers=extra_headers,
499-
extra_query=extra_query,
500-
extra_body=extra_body,
501-
timeout=timeout,
502496
)
503497

504498
def list(
@@ -1662,23 +1656,14 @@ async def create_and_await_running(
16621656
return await self.await_running(
16631657
devbox.id,
16641658
polling_config=polling_config,
1665-
extra_headers=extra_headers,
1666-
extra_query=extra_query,
1667-
extra_body=extra_body,
1668-
timeout=timeout,
16691659
)
16701660

16711661
async def await_running(
16721662
self,
16731663
id: str,
16741664
*,
1665+
# Use polling_config to configure the "long" polling behavior.
16751666
polling_config: PollingConfig | None = None,
1676-
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
1677-
# The extra values given here take precedence over values defined on the client or passed to this method.
1678-
extra_headers: Headers | None = None,
1679-
extra_query: Query | None = None,
1680-
extra_body: Body | None = None,
1681-
timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN,
16821667
) -> DevboxView:
16831668
"""Wait for a devbox to be in running state.
16841669
@@ -1705,26 +1690,16 @@ async def wait_for_devbox_status() -> DevboxView:
17051690
return await self._post(
17061691
f"/v1/devboxes/{id}/wait_for_status",
17071692
body={"statuses": ["running", "failure"]},
1708-
options=make_request_options(
1709-
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
1710-
),
17111693
cast_to=DevboxView,
17121694
)
1713-
except APIStatusError as error:
1714-
if error.response.status_code == 408:
1715-
# Handle 408 timeout errors by returning a placeholder result to continue polling
1716-
return DevboxView(
1717-
id=id,
1718-
status="provisioning",
1719-
capabilities=[],
1720-
create_time_ms=0,
1721-
launch_parameters=SharedLaunchParameters(),
1722-
metadata={},
1723-
state_transitions=[],
1724-
)
1725-
else:
1726-
# Re-raise other errors to stop polling
1727-
raise
1695+
except (APITimeoutError, APIStatusError) as error:
1696+
# Handle timeout errors by returning current devbox state to continue polling
1697+
if isinstance(error, APITimeoutError) or error.response.status_code == 408:
1698+
# Return a placeholder result to continue polling
1699+
return placeholder_devbox_view(id)
1700+
1701+
# Re-raise other errors to stop polling
1702+
raise
17281703

17291704
def is_done_booting(devbox: DevboxView) -> bool:
17301705
return devbox.status not in DEVBOX_BOOTING_STATES

src/runloop_api_client/resources/devboxes/executions.py

Lines changed: 27 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
async_to_streamed_response_wrapper,
1818
)
1919
from ..._constants import DEFAULT_TIMEOUT
20-
from ..._exceptions import APIStatusError
20+
from ..._exceptions import APIStatusError, APITimeoutError
2121
from ...lib.polling import PollingConfig, poll_until
2222
from ..._base_client import make_request_options
2323
from ...types.devboxes import execution_retrieve_params, execution_execute_sync_params, execution_execute_async_params
@@ -28,6 +28,16 @@
2828
__all__ = ["ExecutionsResource", "AsyncExecutionsResource"]
2929

3030

31+
def placeholder_execution_detail_view(devbox_id: str, execution_id: str) -> DevboxAsyncExecutionDetailView:
32+
return DevboxAsyncExecutionDetailView(
33+
devbox_id=devbox_id,
34+
execution_id=execution_id,
35+
status="queued",
36+
stdout="",
37+
stderr="",
38+
)
39+
40+
3141
class ExecutionsResource(SyncAPIResource):
3242
@cached_property
3343
def with_raw_response(self) -> ExecutionsResourceWithRawResponse:
@@ -97,13 +107,8 @@ def await_completed(
97107
execution_id: str,
98108
devbox_id: str,
99109
*,
100-
config: PollingConfig | None = None,
101-
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
102-
# The extra values given here take precedence over values defined on the client or passed to this method.
103-
extra_headers: Headers | None = None,
104-
extra_query: Query | None = None,
105-
extra_body: Body | None = None,
106-
timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN,
110+
# Use polling_config to configure the "long" polling behavior.
111+
polling_config: PollingConfig | None = None,
107112
) -> DevboxAsyncExecutionDetailView:
108113
"""Wait for an execution to complete.
109114
@@ -128,31 +133,24 @@ def wait_for_execution_status() -> DevboxAsyncExecutionDetailView:
128133
return self._post(
129134
f"/v1/devboxes/{devbox_id}/executions/{execution_id}/wait_for_status",
130135
body={"statuses": ["completed"]},
131-
options=make_request_options(
132-
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
133-
),
134136
cast_to=DevboxAsyncExecutionDetailView,
135137
)
136138

137139
def handle_timeout_error(error: Exception) -> DevboxAsyncExecutionDetailView:
138-
# Handle 408 timeout errors by returning current execution state to continue polling
139-
if isinstance(error, APIStatusError) and error.response.status_code == 408:
140+
# Handle timeout errors by returning current execution state to continue polling
141+
if isinstance(error, APITimeoutError) or (
142+
isinstance(error, APIStatusError) and error.response.status_code == 408
143+
):
140144
# Return a placeholder result to continue polling
141-
return DevboxAsyncExecutionDetailView(
142-
devbox_id=devbox_id,
143-
execution_id=execution_id,
144-
status="queued",
145-
stdout="",
146-
stderr="",
147-
)
145+
return placeholder_execution_detail_view(devbox_id, execution_id)
148146
else:
149147
# Re-raise other errors to stop polling
150148
raise error
151149

152150
def is_done(execution: DevboxAsyncExecutionDetailView) -> bool:
153151
return execution.status == "completed"
154152

155-
return poll_until(wait_for_execution_status, is_done, config, handle_timeout_error)
153+
return poll_until(wait_for_execution_status, is_done, polling_config, handle_timeout_error)
156154

157155
def execute_async(
158156
self,
@@ -390,13 +388,8 @@ async def await_completed(
390388
execution_id: str,
391389
*,
392390
devbox_id: str,
391+
# Use polling_config to configure the "long" polling behavior.
393392
polling_config: PollingConfig | None = None,
394-
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
395-
# The extra values given here take precedence over values defined on the client or passed to this method.
396-
extra_headers: Headers | None = None,
397-
extra_query: Query | None = None,
398-
extra_body: Body | None = None,
399-
timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN,
400393
) -> DevboxAsyncExecutionDetailView:
401394
"""Wait for an execution to complete.
402395
@@ -421,24 +414,15 @@ async def wait_for_execution_status() -> DevboxAsyncExecutionDetailView:
421414
return await self._post(
422415
f"/v1/devboxes/{devbox_id}/executions/{execution_id}/wait_for_status",
423416
body={"statuses": ["completed"]},
424-
options=make_request_options(
425-
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
426-
),
427417
cast_to=DevboxAsyncExecutionDetailView,
428418
)
429-
except APIStatusError as error:
430-
if error.response.status_code == 408:
431-
# Handle 408 timeout errors by returning current execution state to continue polling
432-
return DevboxAsyncExecutionDetailView(
433-
devbox_id=devbox_id,
434-
execution_id=execution_id,
435-
status="queued",
436-
stdout="",
437-
stderr="",
438-
)
439-
else:
440-
# Re-raise other errors to stop polling
441-
raise
419+
except (APITimeoutError, APIStatusError) as error:
420+
# Handle timeout errors by returning placeholder to continue polling
421+
if isinstance(error, APITimeoutError) or error.response.status_code == 408:
422+
return placeholder_execution_detail_view(devbox_id, execution_id)
423+
424+
# Re-raise other errors to stop polling
425+
raise
442426

443427
def is_done(execution: DevboxAsyncExecutionDetailView) -> bool:
444428
return execution.status == "completed"

src/runloop_api_client/resources/scenarios/scenarios.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -471,10 +471,6 @@ def start_run_and_await_env_ready(
471471
self._client.devboxes.await_running(
472472
run.devbox_id,
473473
polling_config=polling_config,
474-
extra_headers=extra_headers,
475-
extra_query=extra_query,
476-
extra_body=extra_body,
477-
timeout=timeout,
478474
)
479475

480476
return run

tests/api_resources/devboxes/test_executions.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from tests.utils import assert_matches_type
1212
from runloop_api_client import Runloop, AsyncRunloop
1313
from runloop_api_client.types import DevboxExecutionDetailView, DevboxAsyncExecutionDetailView
14-
from runloop_api_client._exceptions import APIStatusError
14+
from runloop_api_client._exceptions import APIStatusError, APITimeoutError
1515
from runloop_api_client.lib.polling import PollingConfig, PollingTimeout
1616

1717
base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
@@ -338,7 +338,7 @@ def test_method_await_completed_with_config(self, client: Runloop) -> None:
338338
with patch.object(client.devboxes.executions, "_post") as mock_post:
339339
mock_post.return_value = mock_execution_completed
340340

341-
result = client.devboxes.executions.await_completed("execution_id", "devbox_id", config=config)
341+
result = client.devboxes.executions.await_completed("execution_id", "devbox_id", polling_config=config)
342342

343343
assert result.execution_id == "execution_id"
344344
assert result.status == "completed"
@@ -361,7 +361,7 @@ def test_method_await_completed_polling_timeout(self, client: Runloop) -> None:
361361
mock_post.return_value = mock_execution_running
362362

363363
with pytest.raises(PollingTimeout):
364-
client.devboxes.executions.await_completed("execution_id", "devbox_id", config=config)
364+
client.devboxes.executions.await_completed("execution_id", "devbox_id", polling_config=config)
365365

366366
@parametrize
367367
def test_method_await_completed_various_statuses(self, client: Runloop) -> None:
@@ -665,7 +665,7 @@ async def test_method_await_completed_timeout_handling(self, async_client: Async
665665
# Create a mock 408 response
666666
mock_response = Mock()
667667
mock_response.status_code = 408
668-
mock_408_error = APIStatusError("Request timeout", response=mock_response, body=None)
668+
mock_408_error = APITimeoutError(request=mock_response.request)
669669

670670
mock_execution_completed = DevboxAsyncExecutionDetailView(
671671
devbox_id="devbox_id",

tests/api_resources/test_devboxes.py

Lines changed: 3 additions & 7 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 ANY, Mock, patch
7+
from unittest.mock import Mock, patch
88

99
import httpx
1010
import pytest
@@ -1171,9 +1171,7 @@ def test_method_create_and_await_running_success(self, client: Runloop) -> None:
11711171
assert result.id == "test_id"
11721172
assert result.status == "running"
11731173
mock_create.assert_called_once()
1174-
mock_await.assert_called_once_with(
1175-
"test_id", polling_config=None, extra_headers=None, extra_query=None, extra_body=None, timeout=ANY
1176-
)
1174+
mock_await.assert_called_once_with("test_id", polling_config=None)
11771175

11781176
@parametrize
11791177
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
12101208

12111209
assert result.id == "test_id"
12121210
assert result.status == "running"
1213-
mock_await.assert_called_once_with(
1214-
"test_id", polling_config=config, extra_headers=None, extra_query=None, extra_body=None, timeout=ANY
1215-
)
1211+
mock_await.assert_called_once_with("test_id", polling_config=config)
12161212

12171213
@parametrize
12181214
def test_method_create_and_await_running_create_failure(self, client: Runloop) -> None:

0 commit comments

Comments
 (0)