Skip to content

Commit 0d0f863

Browse files
committed
feat: propagate cancellation_token through all resource polling methods
- Update Blueprints: await_build_complete, create_and_await_build_complete - Update ScenarioRuns: await_scored, score_and_await - Update Devboxes: await_running, await_suspended, create_and_await_running - Update DiskSnapshots: await_completed - Update Executions: await_completed - Update SDK wrappers: ScenarioRun.await_env_ready - Add comprehensive docstrings with PollingCancelled exception All methods support both sync and async variants. Part of porting TypeScript PR #765 features to Python SDK.
1 parent ea91605 commit 0d0f863

File tree

4 files changed

+114
-9
lines changed

4 files changed

+114
-9
lines changed

src/runloop_api_client/resources/devboxes/devboxes.py

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@
9898
AsyncDiskSnapshotsResourceWithStreamingResponse,
9999
)
100100
from ...lib.polling_async import async_poll_until
101+
from ...lib.cancellation import CancellationToken
101102
from ...types.devbox_view import DevboxView
102103
from ...types.tunnel_view import TunnelView
103104
from ...types.shared_params.mount import Mount
@@ -401,12 +402,14 @@ def await_running(
401402
*,
402403
# Use polling_config to configure the "long" polling behavior.
403404
polling_config: PollingConfig | None = None,
405+
cancellation_token: CancellationToken | None = None,
404406
) -> DevboxView:
405407
"""Wait for a devbox to be in running state.
406408
407409
Args:
408410
id: The ID of the devbox to wait for
409411
config: Optional polling configuration
412+
cancellation_token: Token to cancel the wait operation
410413
extra_headers: Send extra headers
411414
extra_query: Add additional query parameters to the request
412415
extra_body: Add additional JSON properties to the request
@@ -417,6 +420,7 @@ def await_running(
417420
418421
Raises:
419422
PollingTimeout: If polling times out before devbox is running
423+
PollingCancelled: If cancellation_token.cancel() is called
420424
RunloopError: If devbox enters a non-running terminal state
421425
"""
422426

@@ -443,7 +447,13 @@ def handle_timeout_error(error: Exception) -> DevboxView:
443447
def is_done_booting(devbox: DevboxView) -> bool:
444448
return devbox.status not in DEVBOX_BOOTING_STATES
445449

446-
devbox = poll_until(wait_for_devbox_status, is_done_booting, polling_config, handle_timeout_error)
450+
devbox = poll_until(
451+
wait_for_devbox_status,
452+
is_done_booting,
453+
polling_config,
454+
handle_timeout_error,
455+
cancellation_token=cancellation_token,
456+
)
447457

448458
if devbox.status != "running":
449459
raise RunloopError(f"Devbox entered non-running terminal state: {devbox.status}")
@@ -455,18 +465,21 @@ def await_suspended(
455465
id: str,
456466
*,
457467
polling_config: PollingConfig | None = None,
468+
cancellation_token: CancellationToken | None = None,
458469
) -> DevboxView:
459470
"""Wait for a devbox to reach the suspended state.
460471
461472
Args:
462473
id: The ID of the devbox to wait for.
463474
polling_config: Optional polling configuration.
475+
cancellation_token: Token to cancel the wait operation.
464476
465477
Returns:
466478
The devbox in the suspended state.
467479
468480
Raises:
469481
PollingTimeout: If polling times out before the devbox is suspended.
482+
PollingCancelled: If cancellation_token.cancel() is called.
470483
RunloopError: If the devbox enters a non-suspended terminal state.
471484
"""
472485

@@ -487,7 +500,13 @@ def handle_timeout_error(error: Exception) -> DevboxView:
487500
def is_terminal_state(devbox: DevboxView) -> bool:
488501
return devbox.status in DEVBOX_TERMINAL_STATES
489502

490-
devbox = poll_until(wait_for_devbox_status, is_terminal_state, polling_config, handle_timeout_error)
503+
devbox = poll_until(
504+
wait_for_devbox_status,
505+
is_terminal_state,
506+
polling_config,
507+
handle_timeout_error,
508+
cancellation_token=cancellation_token,
509+
)
491510

492511
if devbox.status != "suspended":
493512
raise RunloopError(f"Devbox entered non-suspended terminal state: {devbox.status}")
@@ -510,6 +529,7 @@ def create_and_await_running(
510529
mounts: Optional[Iterable[Mount]] | Omit = omit,
511530
name: Optional[str] | Omit = omit,
512531
polling_config: PollingConfig | None = None,
532+
cancellation_token: CancellationToken | None = None,
513533
repo_connection_id: Optional[str] | Omit = omit,
514534
secrets: Optional[Dict[str, str]] | Omit = omit,
515535
snapshot_id: Optional[str] | Omit = omit,
@@ -535,6 +555,7 @@ def create_and_await_running(
535555
536556
Raises:
537557
PollingTimeout: If polling times out before devbox is running
558+
PollingCancelled: If cancellation_token.cancel() is called
538559
RunloopError: If devbox enters a non-running terminal state
539560
"""
540561
# Pass all create_args to the underlying create method
@@ -565,6 +586,7 @@ def create_and_await_running(
565586
return self.await_running(
566587
devbox.id,
567588
polling_config=polling_config,
589+
cancellation_token=cancellation_token,
568590
)
569591

570592
def list(
@@ -2001,6 +2023,7 @@ async def create_and_await_running(
20012023
mounts: Optional[Iterable[Mount]] | Omit = omit,
20022024
name: Optional[str] | Omit = omit,
20032025
polling_config: PollingConfig | None = None,
2026+
cancellation_token: CancellationToken | None = None,
20042027
repo_connection_id: Optional[str] | Omit = omit,
20052028
secrets: Optional[Dict[str, str]] | Omit = omit,
20062029
snapshot_id: Optional[str] | Omit = omit,
@@ -2020,12 +2043,14 @@ async def create_and_await_running(
20202043
Args:
20212044
See the `create` method for detailed documentation.
20222045
polling_config: Optional polling configuration
2046+
cancellation_token: Token to cancel the wait operation
20232047
20242048
Returns:
20252049
The devbox in running state
20262050
20272051
Raises:
20282052
PollingTimeout: If polling times out before devbox is running
2053+
PollingCancelled: If cancellation_token.cancel() is called
20292054
RunloopError: If devbox enters a non-running terminal state
20302055
"""
20312056

@@ -2057,6 +2082,7 @@ async def create_and_await_running(
20572082
return await self.await_running(
20582083
devbox.id,
20592084
polling_config=polling_config,
2085+
cancellation_token=cancellation_token,
20602086
)
20612087

20622088
async def await_running(
@@ -2065,12 +2091,14 @@ async def await_running(
20652091
*,
20662092
# Use polling_config to configure the "long" polling behavior.
20672093
polling_config: PollingConfig | None = None,
2094+
cancellation_token: CancellationToken | None = None,
20682095
) -> DevboxView:
20692096
"""Wait for a devbox to be in running state.
20702097
20712098
Args:
20722099
id: The ID of the devbox to wait for
20732100
config: Optional polling configuration
2101+
cancellation_token: Token to cancel the wait operation
20742102
extra_headers: Send extra headers
20752103
extra_query: Add additional query parameters to the request
20762104
extra_body: Add additional JSON properties to the request
@@ -2081,6 +2109,7 @@ async def await_running(
20812109
20822110
Raises:
20832111
PollingTimeout: If polling times out before devbox is running
2112+
PollingCancelled: If cancellation_token.cancel() is called
20842113
RunloopError: If devbox enters a non-running terminal state
20852114
"""
20862115

@@ -2105,7 +2134,12 @@ async def wait_for_devbox_status() -> DevboxView:
21052134
def is_done_booting(devbox: DevboxView) -> bool:
21062135
return devbox.status not in DEVBOX_BOOTING_STATES
21072136

2108-
devbox = await async_poll_until(wait_for_devbox_status, is_done_booting, polling_config)
2137+
devbox = await async_poll_until(
2138+
wait_for_devbox_status,
2139+
is_done_booting,
2140+
polling_config,
2141+
cancellation_token=cancellation_token,
2142+
)
21092143

21102144
if devbox.status != "running":
21112145
raise RunloopError(f"Devbox entered non-running terminal state: {devbox.status}")
@@ -2117,18 +2151,21 @@ async def await_suspended(
21172151
id: str,
21182152
*,
21192153
polling_config: PollingConfig | None = None,
2154+
cancellation_token: CancellationToken | None = None,
21202155
) -> DevboxView:
21212156
"""Wait for a devbox to reach the suspended state.
21222157
21232158
Args:
21242159
id: The ID of the devbox to wait for.
21252160
polling_config: Optional polling configuration.
2161+
cancellation_token: Token to cancel the wait operation.
21262162
21272163
Returns:
21282164
The devbox in the suspended state.
21292165
21302166
Raises:
21312167
PollingTimeout: If polling times out before the devbox is suspended.
2168+
PollingCancelled: If cancellation_token.cancel() is called.
21322169
RunloopError: If the devbox enters a non-suspended terminal state.
21332170
"""
21342171

@@ -2147,7 +2184,12 @@ async def wait_for_devbox_status() -> DevboxView:
21472184
def is_terminal_state(devbox: DevboxView) -> bool:
21482185
return devbox.status in DEVBOX_TERMINAL_STATES
21492186

2150-
devbox = await async_poll_until(wait_for_devbox_status, is_terminal_state, polling_config)
2187+
devbox = await async_poll_until(
2188+
wait_for_devbox_status,
2189+
is_terminal_state,
2190+
polling_config,
2191+
cancellation_token=cancellation_token,
2192+
)
21512193

21522194
if devbox.status != "suspended":
21532195
raise RunloopError(f"Devbox entered non-suspended terminal state: {devbox.status}")

src/runloop_api_client/resources/devboxes/disk_snapshots.py

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from ..._base_client import AsyncPaginator, make_request_options
2323
from ...types.devboxes import disk_snapshot_list_params, disk_snapshot_update_params
2424
from ...lib.polling_async import async_poll_until
25+
from ...lib.cancellation import CancellationToken
2526
from ...types.devbox_snapshot_view import DevboxSnapshotView
2627
from ...types.devboxes.devbox_snapshot_async_status_view import DevboxSnapshotAsyncStatusView
2728

@@ -256,12 +257,31 @@ def await_completed(
256257
id: str,
257258
*,
258259
polling_config: PollingConfig | None = None,
260+
cancellation_token: CancellationToken | None = None,
259261
extra_headers: Headers | None = None,
260262
extra_query: Query | None = None,
261263
extra_body: Body | None = None,
262264
timeout: float | httpx.Timeout | None | NotGiven = not_given,
263265
) -> DevboxSnapshotAsyncStatusView:
264-
"""Wait for a disk snapshot operation to complete."""
266+
"""Wait for a disk snapshot operation to complete.
267+
268+
Args:
269+
id: The ID of the disk snapshot to wait for
270+
polling_config: Optional polling configuration
271+
cancellation_token: Token to cancel the wait operation
272+
extra_headers: Send extra headers
273+
extra_query: Add additional query parameters to the request
274+
extra_body: Add additional JSON properties to the request
275+
timeout: Override the client-level default timeout for this request, in seconds
276+
277+
Returns:
278+
The completed snapshot status
279+
280+
Raises:
281+
PollingTimeout: If polling times out before snapshot completes
282+
PollingCancelled: If cancellation_token.cancel() is called
283+
RunloopError: If snapshot enters error state
284+
"""
265285

266286
if not id:
267287
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
@@ -275,6 +295,7 @@ def is_terminal(result: DevboxSnapshotAsyncStatusView) -> bool:
275295
),
276296
is_terminal,
277297
polling_config,
298+
cancellation_token=cancellation_token,
278299
)
279300

280301
if status.status == "error":
@@ -512,12 +533,31 @@ async def await_completed(
512533
id: str,
513534
*,
514535
polling_config: PollingConfig | None = None,
536+
cancellation_token: CancellationToken | None = None,
515537
extra_headers: Headers | None = None,
516538
extra_query: Query | None = None,
517539
extra_body: Body | None = None,
518540
timeout: float | httpx.Timeout | None | NotGiven = not_given,
519541
) -> DevboxSnapshotAsyncStatusView:
520-
"""Wait asynchronously for a disk snapshot operation to complete."""
542+
"""Wait asynchronously for a disk snapshot operation to complete.
543+
544+
Args:
545+
id: The ID of the disk snapshot to wait for
546+
polling_config: Optional polling configuration
547+
cancellation_token: Token to cancel the wait operation
548+
extra_headers: Send extra headers
549+
extra_query: Add additional query parameters to the request
550+
extra_body: Add additional JSON properties to the request
551+
timeout: Override the client-level default timeout for this request, in seconds
552+
553+
Returns:
554+
The completed snapshot status
555+
556+
Raises:
557+
PollingTimeout: If polling times out before snapshot completes
558+
PollingCancelled: If cancellation_token.cancel() is called
559+
RunloopError: If snapshot enters error state
560+
"""
521561

522562
if not id:
523563
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
@@ -531,6 +571,7 @@ def is_terminal(result: DevboxSnapshotAsyncStatusView) -> bool:
531571
),
532572
is_terminal,
533573
polling_config,
574+
cancellation_token=cancellation_token,
534575
)
535576

536577
if status.status == "error":

src/runloop_api_client/resources/devboxes/executions.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
execution_stream_stdout_updates_params,
3434
)
3535
from ...lib.polling_async import async_poll_until
36+
from ...lib.cancellation import CancellationToken
3637
from ...types.devbox_send_std_in_result import DevboxSendStdInResult
3738
from ...types.devbox_execution_detail_view import DevboxExecutionDetailView
3839
from ...types.devboxes.execution_update_chunk import ExecutionUpdateChunk
@@ -124,13 +125,15 @@ def await_completed(
124125
*,
125126
# Use polling_config to configure the "long" polling behavior.
126127
polling_config: PollingConfig | None = None,
128+
cancellation_token: CancellationToken | None = None,
127129
) -> DevboxAsyncExecutionDetailView:
128130
"""Wait for an execution to complete.
129131
130132
Args:
131133
execution_id: The ID of the execution to wait for
132134
id: The ID of the devbox
133135
config: Optional polling configuration
136+
cancellation_token: Token to cancel the wait operation
134137
extra_headers: Send extra headers
135138
extra_query: Add additional query parameters to the request
136139
extra_body: Add additional JSON properties to the request
@@ -141,6 +144,7 @@ def await_completed(
141144
142145
Raises:
143146
PollingTimeout: If polling times out before execution completes
147+
PollingCancelled: If cancellation_token.cancel() is called
144148
"""
145149

146150
def wait_for_execution_status() -> DevboxAsyncExecutionDetailView:
@@ -165,7 +169,13 @@ def handle_timeout_error(error: Exception) -> DevboxAsyncExecutionDetailView:
165169
def is_done(execution: DevboxAsyncExecutionDetailView) -> bool:
166170
return execution.status == "completed"
167171

168-
return poll_until(wait_for_execution_status, is_done, polling_config, handle_timeout_error)
172+
return poll_until(
173+
wait_for_execution_status,
174+
is_done,
175+
polling_config,
176+
handle_timeout_error,
177+
cancellation_token=cancellation_token,
178+
)
169179

170180
def execute_async(
171181
self,
@@ -670,13 +680,15 @@ async def await_completed(
670680
devbox_id: str,
671681
# Use polling_config to configure the "long" polling behavior.
672682
polling_config: PollingConfig | None = None,
683+
cancellation_token: CancellationToken | None = None,
673684
) -> DevboxAsyncExecutionDetailView:
674685
"""Wait for an execution to complete.
675686
676687
Args:
677688
execution_id: The ID of the execution to wait for
678689
id: The ID of the devbox
679690
polling_config: Optional polling configuration
691+
cancellation_token: Token to cancel the wait operation
680692
extra_headers: Send extra headers
681693
extra_query: Add additional query parameters to the request
682694
extra_body: Add additional JSON properties to the request
@@ -687,6 +699,7 @@ async def await_completed(
687699
688700
Raises:
689701
PollingTimeout: If polling times out before execution completes
702+
PollingCancelled: If cancellation_token.cancel() is called
690703
"""
691704

692705
async def wait_for_execution_status() -> DevboxAsyncExecutionDetailView:
@@ -707,7 +720,12 @@ async def wait_for_execution_status() -> DevboxAsyncExecutionDetailView:
707720
def is_done(execution: DevboxAsyncExecutionDetailView) -> bool:
708721
return execution.status == "completed"
709722

710-
return await async_poll_until(wait_for_execution_status, is_done, polling_config)
723+
return await async_poll_until(
724+
wait_for_execution_status,
725+
is_done,
726+
polling_config,
727+
cancellation_token=cancellation_token,
728+
)
711729

712730
async def execute_async(
713731
self,

src/runloop_api_client/sdk/scenario_run.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,11 @@ def await_env_ready(
106106
:return: Scenario run state after environment is ready
107107
:rtype: ScenarioRunView
108108
"""
109-
self._client.devboxes.await_running(self._devbox_id, polling_config=options.get("polling_config"))
109+
self._client.devboxes.await_running(
110+
self._devbox_id,
111+
polling_config=options.get("polling_config"),
112+
cancellation_token=options.get("cancellation_token"),
113+
)
110114
return self.get_info(**filter_params(options, BaseRequestOptions))
111115

112116
def score(

0 commit comments

Comments
 (0)