diff --git a/api.md b/api.md index a36b2fa0b..47223f898 100644 --- a/api.md +++ b/api.md @@ -57,13 +57,13 @@ from runloop_api_client.types import ( Methods: - client.blueprints.create(\*\*params) -> BlueprintView +- client.blueprints.create_and_await_build_complete(\*\*params) -> BlueprintView - client.blueprints.retrieve(id) -> BlueprintView - client.blueprints.list(\*\*params) -> SyncBlueprintsCursorIDPage[BlueprintView] - client.blueprints.delete(id) -> object - client.blueprints.list_public(\*\*params) -> SyncBlueprintsCursorIDPage[BlueprintView] - client.blueprints.logs(id) -> BlueprintBuildLogsListView - client.blueprints.preview(\*\*params) -> BlueprintPreviewView -- client.blueprints.create_and_await_build_complete(create_args, request_args=None) -> BlueprintView # Devboxes @@ -86,6 +86,7 @@ from runloop_api_client.types import ( Methods: - client.devboxes.create(\*\*params) -> DevboxView +- client.devboxes.create_and_await_running(\*\*params) -> DevboxView - client.devboxes.retrieve(id) -> DevboxView - client.devboxes.update(id, \*\*params) -> DevboxView - client.devboxes.list(\*\*params) -> SyncDevboxesCursorIDPage[DevboxView] @@ -106,7 +107,7 @@ Methods: - client.devboxes.suspend(id) -> DevboxView - client.devboxes.upload_file(id, \*\*params) -> object - client.devboxes.write_file_contents(id, \*\*params) -> DevboxExecutionDetailView -- client.devboxes.create_and_await_running(create_args, request_args=None) -> DevboxView + ## DiskSnapshots diff --git a/src/runloop_api_client/lib/polling_async.py b/src/runloop_api_client/lib/polling_async.py index 1f52dae07..7ba192e86 100644 --- a/src/runloop_api_client/lib/polling_async.py +++ b/src/runloop_api_client/lib/polling_async.py @@ -4,7 +4,8 @@ from .polling import PollingConfig, PollingTimeout -T = TypeVar('T') +T = TypeVar("T") + async def async_poll_until( retriever: Callable[[], Awaitable[T]], @@ -14,27 +15,27 @@ async def async_poll_until( ) -> T: """ Poll until a condition is met or timeout/max attempts are reached. - + Args: retriever: Async or sync callable that returns the object to check is_terminal: Callable that returns True when polling should stop config: Optional polling configuration on_error: Optional error handler that can return a value to continue polling or re-raise the exception to stop polling - + Returns: The final state of the polled object - + Raises: PollingTimeout: When max attempts or timeout is reached """ if config is None: config = PollingConfig() - + attempts = 0 start_time = time.time() last_result: Union[T, None] = None - + while True: try: last_result = await retriever() @@ -43,23 +44,17 @@ async def async_poll_until( last_result = on_error(e) else: raise - + if is_terminal(last_result): return last_result - + attempts += 1 if attempts >= config.max_attempts: - raise PollingTimeout( - f"Exceeded maximum attempts ({config.max_attempts})", - last_result - ) - + raise PollingTimeout(f"Exceeded maximum attempts ({config.max_attempts})", last_result) + if config.timeout_seconds is not None: elapsed = time.time() - start_time if elapsed >= config.timeout_seconds: - raise PollingTimeout( - f"Exceeded timeout of {config.timeout_seconds} seconds", - last_result - ) - + raise PollingTimeout(f"Exceeded timeout of {config.timeout_seconds} seconds", last_result) + await asyncio.sleep(config.interval_seconds) diff --git a/src/runloop_api_client/resources/blueprints.py b/src/runloop_api_client/resources/blueprints.py index 039dbeca5..8d49769f2 100644 --- a/src/runloop_api_client/resources/blueprints.py +++ b/src/runloop_api_client/resources/blueprints.py @@ -230,16 +230,30 @@ def is_done_building(blueprint: BlueprintView) -> bool: def create_and_await_build_complete( self, *, - create_args: blueprint_create_params.BlueprintCreateParams, - request_args: BlueprintRequestArgs | None = None, + name: str, + base_blueprint_id: Optional[str] | NotGiven = NOT_GIVEN, + code_mounts: Optional[Iterable[CodeMountParameters]] | NotGiven = NOT_GIVEN, + dockerfile: Optional[str] | NotGiven = NOT_GIVEN, + file_mounts: Optional[Dict[str, str]] | NotGiven = NOT_GIVEN, + launch_parameters: Optional[LaunchParameters] | NotGiven = NOT_GIVEN, + polling_config: PollingConfig | None = None, + services: Optional[Iterable[blueprint_create_params.Service]] | NotGiven = NOT_GIVEN, + system_setup_commands: Optional[List[str]] | NotGiven = NOT_GIVEN, + # 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, + idempotency_key: str | None = None, ) -> BlueprintView: """Create a new Blueprint and wait for it to finish building. This is a wrapper around the `create` method that waits for the blueprint to finish building. Args: - create_args: Arguments to pass to the `create` method. See the `create` method for detailed documentation. - request_args: Optional request arguments including polling configuration and additional request options + See the `create` method for detailed documentation. + polling_config: Optional polling configuration Returns: The built blueprint @@ -249,18 +263,29 @@ def create_and_await_build_complete( RunloopError: If blueprint enters a non-built terminal state """ # Pass all create_args to the underlying create method - blueprint = self.create(**create_args) - - if request_args is None: - request_args = {} + blueprint = self.create( + name=name, + base_blueprint_id=base_blueprint_id, + code_mounts=code_mounts, + dockerfile=dockerfile, + file_mounts=file_mounts, + launch_parameters=launch_parameters, + services=services, + system_setup_commands=system_setup_commands, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ) return self.await_build_complete( blueprint.id, - polling_config=request_args.get("polling_config", None), - extra_headers=request_args.get("extra_headers", None), - extra_query=request_args.get("extra_query", None), - extra_body=request_args.get("extra_body", None), - timeout=request_args.get("timeout", None), + polling_config=polling_config, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, ) def list( @@ -700,16 +725,30 @@ def is_done_building(blueprint: BlueprintView) -> bool: async def create_and_await_build_complete( self, *, - create_args: blueprint_create_params.BlueprintCreateParams, - request_args: BlueprintRequestArgs | None = None, + name: str, + base_blueprint_id: Optional[str] | NotGiven = NOT_GIVEN, + code_mounts: Optional[Iterable[CodeMountParameters]] | NotGiven = NOT_GIVEN, + dockerfile: Optional[str] | NotGiven = NOT_GIVEN, + file_mounts: Optional[Dict[str, str]] | NotGiven = NOT_GIVEN, + launch_parameters: Optional[LaunchParameters] | NotGiven = NOT_GIVEN, + polling_config: PollingConfig | None = None, + services: Optional[Iterable[blueprint_create_params.Service]] | NotGiven = NOT_GIVEN, + system_setup_commands: Optional[List[str]] | NotGiven = NOT_GIVEN, + # 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, + idempotency_key: str | None = None, ) -> BlueprintView: """Create a new Blueprint and wait for it to finish building. This is a wrapper around the `create` method that waits for the blueprint to finish building. Args: - create_args: Arguments to pass to the `create` method. See the `create` method for detailed documentation. - request_args: Optional request arguments including polling configuration and additional request options + See the `create` method for detailed documentation. + polling_config: Optional polling configuration Returns: The built blueprint @@ -719,19 +758,29 @@ async def create_and_await_build_complete( RunloopError: If blueprint enters a non-built terminal state """ # Pass all create_args to the underlying create method - blueprint = await self.create(**create_args) - - # Extract polling config and other request args - if request_args is None: - request_args = {} + blueprint = await self.create( + name=name, + base_blueprint_id=base_blueprint_id, + code_mounts=code_mounts, + dockerfile=dockerfile, + file_mounts=file_mounts, + launch_parameters=launch_parameters, + services=services, + system_setup_commands=system_setup_commands, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ) return await self.await_build_complete( blueprint.id, - polling_config=request_args.get("polling_config", None), - extra_headers=request_args.get("extra_headers", None), - extra_query=request_args.get("extra_query", None), - extra_body=request_args.get("extra_body", None), - timeout=request_args.get("timeout", None), + polling_config=polling_config, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, ) def list( diff --git a/src/runloop_api_client/resources/devboxes/devboxes.py b/src/runloop_api_client/resources/devboxes/devboxes.py index c59157367..0d6b11d20 100644 --- a/src/runloop_api_client/resources/devboxes/devboxes.py +++ b/src/runloop_api_client/resources/devboxes/devboxes.py @@ -430,8 +430,26 @@ def is_done_booting(devbox: DevboxView) -> bool: def create_and_await_running( self, *, - create_args: devbox_create_params.DevboxCreateParams | None = None, - request_args: DevboxRequestArgs | None = None, + blueprint_id: Optional[str] | NotGiven = NOT_GIVEN, + blueprint_name: Optional[str] | NotGiven = NOT_GIVEN, + code_mounts: Optional[Iterable[CodeMountParameters]] | NotGiven = NOT_GIVEN, + entrypoint: Optional[str] | NotGiven = NOT_GIVEN, + environment_variables: Optional[Dict[str, str]] | NotGiven = NOT_GIVEN, + file_mounts: Optional[Dict[str, str]] | NotGiven = NOT_GIVEN, + launch_parameters: Optional[LaunchParameters] | NotGiven = NOT_GIVEN, + metadata: Optional[Dict[str, str]] | NotGiven = NOT_GIVEN, + name: Optional[str] | NotGiven = NOT_GIVEN, + polling_config: PollingConfig | None = None, + repo_connection_id: Optional[str] | NotGiven = NOT_GIVEN, + secrets: Optional[Dict[str, str]] | NotGiven = NOT_GIVEN, + snapshot_id: Optional[str] | NotGiven = NOT_GIVEN, + # 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, + idempotency_key: str | None = None, ) -> DevboxView: """Create a new devbox and wait for it to be in running state. @@ -448,22 +466,30 @@ def create_and_await_running( PollingTimeout: If polling times out before devbox is running RunloopError: If devbox enters a non-running terminal state """ - # Extract polling config and other request args - if request_args is None: - request_args = {} - # Pass all create_args to the underlying create method devbox = self.create( - **(create_args or {}), - extra_headers=request_args.get("extra_headers", None), - extra_query=request_args.get("extra_query", None), - extra_body=request_args.get("extra_body", None), - timeout=request_args.get("timeout", None), + blueprint_id=blueprint_id, + blueprint_name=blueprint_name, + code_mounts=code_mounts, + entrypoint=entrypoint, + environment_variables=environment_variables, + file_mounts=file_mounts, + launch_parameters=launch_parameters, + metadata=metadata, + name=name, + repo_connection_id=repo_connection_id, + secrets=secrets, + snapshot_id=snapshot_id, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, ) return self.await_running( devbox.id, - polling_config=request_args.get("polling_config", None), + polling_config=polling_config, ) def list( @@ -1558,16 +1584,34 @@ async def retrieve( async def create_and_await_running( self, *, - create_args: devbox_create_params.DevboxCreateParams | None = None, - request_args: DevboxRequestArgs | None = None, + blueprint_id: Optional[str] | NotGiven = NOT_GIVEN, + blueprint_name: Optional[str] | NotGiven = NOT_GIVEN, + code_mounts: Optional[Iterable[CodeMountParameters]] | NotGiven = NOT_GIVEN, + entrypoint: Optional[str] | NotGiven = NOT_GIVEN, + environment_variables: Optional[Dict[str, str]] | NotGiven = NOT_GIVEN, + file_mounts: Optional[Dict[str, str]] | NotGiven = NOT_GIVEN, + launch_parameters: Optional[LaunchParameters] | NotGiven = NOT_GIVEN, + metadata: Optional[Dict[str, str]] | NotGiven = NOT_GIVEN, + name: Optional[str] | NotGiven = NOT_GIVEN, + polling_config: PollingConfig | None = None, + repo_connection_id: Optional[str] | NotGiven = NOT_GIVEN, + secrets: Optional[Dict[str, str]] | NotGiven = NOT_GIVEN, + snapshot_id: Optional[str] | NotGiven = NOT_GIVEN, + # 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, + idempotency_key: str | None = None, ) -> DevboxView: """Create a devbox and wait for it to be in running state. This is a wrapper around the `create` method that waits for the devbox to reach running state. Args: - create_args: Arguments to pass to the `create` method. See the `create` method for detailed documentation. - request_args: Optional request arguments including polling configuration and additional request options + See the `create` method for detailed documentation. + polling_config: Optional polling configuration Returns: The devbox in running state @@ -1576,16 +1620,31 @@ async def create_and_await_running( PollingTimeout: If polling times out before devbox is running RunloopError: If devbox enters a non-running terminal state """ - # Pass all create_args to the underlying create method - devbox = await self.create(**(create_args or {})) - # Extract polling config and other request args - if request_args is None: - request_args = {} + # Pass all create_args, relevant request args to the underlying create method + devbox = await self.create( + blueprint_id=blueprint_id, + blueprint_name=blueprint_name, + code_mounts=code_mounts, + entrypoint=entrypoint, + environment_variables=environment_variables, + file_mounts=file_mounts, + launch_parameters=launch_parameters, + metadata=metadata, + name=name, + repo_connection_id=repo_connection_id, + secrets=secrets, + snapshot_id=snapshot_id, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ) return await self.await_running( devbox.id, - polling_config=request_args.get("polling_config", None), + polling_config=polling_config, ) async def await_running( diff --git a/src/runloop_api_client/resources/scenarios/runs.py b/src/runloop_api_client/resources/scenarios/runs.py index 029c16c1b..8f759a367 100644 --- a/src/runloop_api_client/resources/scenarios/runs.py +++ b/src/runloop_api_client/resources/scenarios/runs.py @@ -316,7 +316,7 @@ def await_scored( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> ScenarioRunView: """Wait for a scenario run to be scored. - + Args: id: The ID of the scenario run to wait for polling_config: Optional polling configuration @@ -332,13 +332,10 @@ def await_scored( PollingTimeout: If polling times out before scenario run is scored RunloopError: If scenario run enters a non-scored terminal state """ + def retrieve_run() -> ScenarioRunView: return self.retrieve( - id, - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout + id, extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ) def is_done_scoring(run: ScenarioRunView) -> bool: @@ -347,9 +344,7 @@ def is_done_scoring(run: ScenarioRunView) -> bool: run = poll_until(retrieve_run, is_done_scoring, polling_config) if run.state != "scored": - raise RunloopError( - f"Scenario run entered non-scored state unexpectedly: {run.state}" - ) + raise RunloopError(f"Scenario run entered non-scored state unexpectedly: {run.state}") return run @@ -366,7 +361,7 @@ def score_and_await( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> ScenarioRunView: """Score a scenario run and wait for it to be scored. - + Args: id: The ID of the scenario run to score and wait for polling_config: Optional polling configuration @@ -412,7 +407,7 @@ def score_and_complete( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> ScenarioRunView: """Score a scenario run, wait for it to be scored, then complete it. - + Args: id: The ID of the scenario run to score, wait for, and complete polling_config: Optional polling configuration @@ -445,6 +440,7 @@ def score_and_complete( timeout=timeout, ) + class AsyncRunsResource(AsyncAPIResource): @cached_property def with_raw_response(self) -> AsyncRunsResourceWithRawResponse: @@ -728,7 +724,7 @@ async def await_scored( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> ScenarioRunView: """Wait for a scenario run to be scored. - + Args: id: The ID of the scenario run to wait for polling_config: Optional polling configuration @@ -744,13 +740,10 @@ async def await_scored( PollingTimeout: If polling times out before scenario run is scored RunloopError: If scenario run enters a non-scored terminal state """ + async def retrieve_run() -> ScenarioRunView: return await self.retrieve( - id, - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout + id, extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ) def is_done_scoring(run: ScenarioRunView) -> bool: @@ -759,9 +752,7 @@ def is_done_scoring(run: ScenarioRunView) -> bool: run = await async_poll_until(retrieve_run, is_done_scoring, polling_config) if run.state != "scored": - raise RunloopError( - f"Scenario run entered non-scored state unexpectedly: {run.state}" - ) + raise RunloopError(f"Scenario run entered non-scored state unexpectedly: {run.state}") return run @@ -778,7 +769,7 @@ async def score_and_await( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> ScenarioRunView: """Score a scenario run and wait for it to be scored. - + Args: id: The ID of the scenario run to score and wait for polling_config: Optional polling configuration @@ -824,7 +815,7 @@ async def score_and_complete( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> ScenarioRunView: """Score a scenario run, wait for it to be scored, then complete it. - + Args: id: The ID of the scenario run to score, wait for, and complete polling_config: Optional polling configuration diff --git a/src/runloop_api_client/resources/scenarios/scenarios.py b/src/runloop_api_client/resources/scenarios/scenarios.py index 7e041d594..528b91bc5 100644 --- a/src/runloop_api_client/resources/scenarios/scenarios.py +++ b/src/runloop_api_client/resources/scenarios/scenarios.py @@ -926,6 +926,7 @@ async def start_run_and_await_env_ready( return run + class ScenariosResourceWithRawResponse: def __init__(self, scenarios: ScenariosResource) -> None: self._scenarios = scenarios diff --git a/tests/api_resources/test_devboxes.py b/tests/api_resources/test_devboxes.py index 337312112..dfe1f1cbb 100644 --- a/tests/api_resources/test_devboxes.py +++ b/tests/api_resources/test_devboxes.py @@ -1168,9 +1168,7 @@ def test_method_create_and_await_running_success(self, client: Runloop) -> None: mock_await.return_value = mock_devbox_running result = client.devboxes.create_and_await_running( - create_args={ - "name": "test", - } + name="test", ) assert result.id == "test_id" @@ -1210,12 +1208,8 @@ def test_method_create_and_await_running_with_config(self, client: Runloop) -> N mock_await.return_value = mock_devbox_running result = client.devboxes.create_and_await_running( - create_args={ - "name": "test", - }, - request_args={ - "polling_config": config, - }, + name="test", + polling_config=config, ) assert result.id == "test_id" @@ -1235,9 +1229,7 @@ def test_method_create_and_await_running_create_failure(self, client: Runloop) - with pytest.raises(APIStatusError, match="Bad request"): client.devboxes.create_and_await_running( - create_args={ - "name": "test", - } + name="test", ) @parametrize @@ -1261,9 +1253,7 @@ def test_method_create_and_await_running_await_failure(self, client: Runloop) -> with pytest.raises(RunloopError, match="Devbox entered non-running terminal state: failed"): client.devboxes.create_and_await_running( - create_args={ - "name": "test", - } + name="test", ) diff --git a/uv.lock b/uv.lock index 3e8708e13..6a4831cd2 100644 --- a/uv.lock +++ b/uv.lock @@ -1242,7 +1242,7 @@ wheels = [ [[package]] name = "runloop-api-client" -version = "0.53.0" +version = "0.55.1" source = { editable = "." } dependencies = [ { name = "anyio", version = "4.5.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },