|
6 | 6 | from typing_extensions import Literal |
7 | 7 |
|
8 | 8 | import httpx |
| 9 | +from uuid_utils import uuid7 |
9 | 10 |
|
10 | 11 | from .lsp import ( |
11 | 12 | LspResource, |
@@ -787,6 +788,59 @@ def execute( |
787 | 788 | cast_to=DevboxAsyncExecutionDetailView, |
788 | 789 | ) |
789 | 790 |
|
| 791 | + def execute_and_await_completion( |
| 792 | + self, |
| 793 | + devbox_id: str, |
| 794 | + *, |
| 795 | + command: str, |
| 796 | + shell_name: Optional[str] | NotGiven = NOT_GIVEN, |
| 797 | + polling_config: PollingConfig | None = None, |
| 798 | + # The following are forwarded to the initial execute request |
| 799 | + extra_headers: Headers | None = None, |
| 800 | + extra_query: Query | None = None, |
| 801 | + extra_body: Body | None = None, |
| 802 | + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, |
| 803 | + idempotency_key: str | None = None, |
| 804 | + ) -> DevboxAsyncExecutionDetailView: |
| 805 | + """ |
| 806 | + Execute a command and wait for it to complete with optimal latency for long running commands. |
| 807 | +
|
| 808 | + This method launches an execution with a generated command_id and first attempts to |
| 809 | + return the result within the initial request's timeout. If the execution is not yet |
| 810 | + complete, it switches to using wait_for_command to minimize latency while waiting. |
| 811 | + """ |
| 812 | + command_id = str(uuid7()) |
| 813 | + execution = self.execute( |
| 814 | + devbox_id, |
| 815 | + command=command, |
| 816 | + command_id=command_id, |
| 817 | + shell_name=shell_name, |
| 818 | + extra_headers=extra_headers, |
| 819 | + extra_query=extra_query, |
| 820 | + extra_body=extra_body, |
| 821 | + timeout=timeout, |
| 822 | + idempotency_key=idempotency_key, |
| 823 | + ) |
| 824 | + if execution.status == "completed": |
| 825 | + return execution |
| 826 | + |
| 827 | + def handle_timeout_error(error: Exception) -> DevboxAsyncExecutionDetailView: |
| 828 | + if isinstance(error, APITimeoutError) or ( |
| 829 | + isinstance(error, APIStatusError) and error.response.status_code == 408 |
| 830 | + ): |
| 831 | + return execution |
| 832 | + raise error |
| 833 | + |
| 834 | + def is_done(result: DevboxAsyncExecutionDetailView) -> bool: |
| 835 | + return result.status == "completed" |
| 836 | + |
| 837 | + return poll_until( |
| 838 | + lambda: self.wait_for_command(execution.execution_id, devbox_id=devbox_id, statuses=["completed"]), |
| 839 | + is_done, |
| 840 | + polling_config, |
| 841 | + handle_timeout_error, |
| 842 | + ) |
| 843 | + |
790 | 844 | def execute_async( |
791 | 845 | self, |
792 | 846 | id: str, |
@@ -2175,6 +2229,60 @@ async def execute( |
2175 | 2229 | cast_to=DevboxAsyncExecutionDetailView, |
2176 | 2230 | ) |
2177 | 2231 |
|
| 2232 | + async def execute_and_await_completion( |
| 2233 | + self, |
| 2234 | + devbox_id: str, |
| 2235 | + *, |
| 2236 | + command: str, |
| 2237 | + shell_name: Optional[str] | NotGiven = NOT_GIVEN, |
| 2238 | + polling_config: PollingConfig | None = None, |
| 2239 | + # The following are forwarded to the initial execute request |
| 2240 | + extra_headers: Headers | None = None, |
| 2241 | + extra_query: Query | None = None, |
| 2242 | + extra_body: Body | None = None, |
| 2243 | + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, |
| 2244 | + idempotency_key: str | None = None, |
| 2245 | + ) -> DevboxAsyncExecutionDetailView: |
| 2246 | + """ |
| 2247 | + Execute a command and wait for it to complete with optimal latency for long running commands. |
| 2248 | +
|
| 2249 | + This method launches an execution with a generated command_id and first attempts to |
| 2250 | + return the result within the initial request's timeout. If the execution is not yet |
| 2251 | + complete, it switches to using wait_for_command to minimize latency while waiting. |
| 2252 | + """ |
| 2253 | + |
| 2254 | + command_id = str(uuid7()) |
| 2255 | + execution = await self.execute( |
| 2256 | + devbox_id, |
| 2257 | + command=command, |
| 2258 | + command_id=command_id, |
| 2259 | + shell_name=shell_name, |
| 2260 | + extra_headers=extra_headers, |
| 2261 | + extra_query=extra_query, |
| 2262 | + extra_body=extra_body, |
| 2263 | + timeout=timeout, |
| 2264 | + idempotency_key=idempotency_key, |
| 2265 | + ) |
| 2266 | + if execution.status == "completed": |
| 2267 | + return execution |
| 2268 | + |
| 2269 | + def handle_timeout_error(error: Exception) -> DevboxAsyncExecutionDetailView: |
| 2270 | + if isinstance(error, APITimeoutError) or ( |
| 2271 | + isinstance(error, APIStatusError) and error.response.status_code == 408 |
| 2272 | + ): |
| 2273 | + return execution |
| 2274 | + raise error |
| 2275 | + |
| 2276 | + def is_done(result: DevboxAsyncExecutionDetailView) -> bool: |
| 2277 | + return result.status == "completed" |
| 2278 | + |
| 2279 | + return await async_poll_until( |
| 2280 | + lambda: self.wait_for_command(execution.execution_id, devbox_id=devbox_id, statuses=["completed"]), |
| 2281 | + is_done, |
| 2282 | + polling_config, |
| 2283 | + handle_timeout_error, |
| 2284 | + ) |
| 2285 | + |
2178 | 2286 | async def execute_async( |
2179 | 2287 | self, |
2180 | 2288 | id: str, |
|
0 commit comments