From 159a38f0980c00430a1b949541076b0d63df2df2 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 22 Nov 2025 01:27:42 +0000 Subject: [PATCH 1/9] chore: hide build context APIs --- .stats.yml | 6 +- api.md | 3 - .../resources/blueprints.py | 44 --- .../resources/devboxes/devboxes.py | 288 ------------------ .../types/blueprint_build_parameters.py | 28 +- .../types/blueprint_create_params.py | 29 +- .../types/blueprint_preview_params.py | 29 +- tests/api_resources/test_blueprints.py | 40 --- tests/api_resources/test_devboxes.py | 228 -------------- 9 files changed, 8 insertions(+), 687 deletions(-) diff --git a/.stats.yml b/.stats.yml index bc705b998..1235f571e 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 97 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/runloop-ai%2Frunloop-0dd27c6877ed117c50fe0af95cee4d54c646d2484368e131b8e3315eba3fffcc.yml -openapi_spec_hash: 68f663172747aef8e66f2b23289efc7b +configured_endpoints: 94 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/runloop-ai%2Frunloop-907baea7d51fd2660895c74603cf1ff765eb9f62eb10ce6e0c1d17ac1c16abf2.yml +openapi_spec_hash: f1280edb22cdd91238efc2b18e3d324c config_hash: 2363f563f42501d2b1587a4f64bdccaf diff --git a/api.md b/api.md index a3c529f83..2c8632112 100644 --- a/api.md +++ b/api.md @@ -126,15 +126,12 @@ Methods: - client.devboxes.execute(id, \*\*params) -> DevboxAsyncExecutionDetailView - client.devboxes.execute_async(id, \*\*params) -> DevboxAsyncExecutionDetailView - client.devboxes.execute_sync(id, \*\*params) -> DevboxExecutionDetailView -- client.devboxes.keep_alive(id) -> object - client.devboxes.list_disk_snapshots(\*\*params) -> SyncDiskSnapshotsCursorIDPage[DevboxSnapshotView] - client.devboxes.read_file_contents(id, \*\*params) -> str - client.devboxes.remove_tunnel(id, \*\*params) -> object -- client.devboxes.resume(id) -> DevboxView - client.devboxes.shutdown(id) -> DevboxView - client.devboxes.snapshot_disk(id, \*\*params) -> DevboxSnapshotView - client.devboxes.snapshot_disk_async(id, \*\*params) -> DevboxSnapshotView -- client.devboxes.suspend(id) -> DevboxView - client.devboxes.upload_file(id, \*\*params) -> object - client.devboxes.wait_for_command(execution_id, \*, devbox_id, \*\*params) -> DevboxAsyncExecutionDetailView - client.devboxes.write_file_contents(id, \*\*params) -> DevboxExecutionDetailView diff --git a/src/runloop_api_client/resources/blueprints.py b/src/runloop_api_client/resources/blueprints.py index c627a8435..7456f762e 100644 --- a/src/runloop_api_client/resources/blueprints.py +++ b/src/runloop_api_client/resources/blueprints.py @@ -130,13 +130,11 @@ def create( base_blueprint_id: Optional[str] | Omit = omit, base_blueprint_name: Optional[str] | Omit = omit, build_args: Optional[Dict[str, str]] | Omit = omit, - build_context: Optional[blueprint_create_params.BuildContext] | Omit = omit, code_mounts: Optional[Iterable[CodeMountParameters]] | Omit = omit, dockerfile: Optional[str] | Omit = omit, file_mounts: Optional[Dict[str, str]] | Omit = omit, launch_parameters: Optional[LaunchParameters] | Omit = omit, metadata: Optional[Dict[str, str]] | Omit = omit, - named_build_contexts: Optional[Dict[str, blueprint_create_params.NamedBuildContexts]] | Omit = omit, secrets: Optional[Dict[str, str]] | Omit = omit, services: Optional[Iterable[blueprint_create_params.Service]] | Omit = omit, system_setup_commands: Optional[SequenceNotStr[str]] | Omit = omit, @@ -168,8 +166,6 @@ def create( build_args: (Optional) Arbitrary Docker build args to pass during build. - build_context: A build context backed by an Object. - code_mounts: A list of code mounts to be included in the Blueprint. dockerfile: Dockerfile contents to be used to build the Blueprint. @@ -180,11 +176,6 @@ def create( metadata: (Optional) User defined metadata for the Blueprint. - named_build_contexts: (Optional) Map of named build contexts to attach to the Blueprint build, where - the keys are the name used when referencing the contexts in a Dockerfile. See - Docker buildx additional contexts for details: - https://docs.docker.com/reference/cli/docker/buildx/build/#build-context - secrets: (Optional) Map of mount IDs/environment variable names to secret names. Secrets will be available to commands during the build. Secrets are NOT stored in the blueprint image. Example: {"DB_PASS": "DATABASE_PASSWORD"} makes the secret @@ -218,13 +209,11 @@ def create( "base_blueprint_id": base_blueprint_id, "base_blueprint_name": base_blueprint_name, "build_args": build_args, - "build_context": build_context, "code_mounts": code_mounts, "dockerfile": dockerfile, "file_mounts": file_mounts, "launch_parameters": launch_parameters, "metadata": metadata, - "named_build_contexts": named_build_contexts, "secrets": secrets, "services": services, "system_setup_commands": system_setup_commands, @@ -652,13 +641,11 @@ def preview( base_blueprint_id: Optional[str] | Omit = omit, base_blueprint_name: Optional[str] | Omit = omit, build_args: Optional[Dict[str, str]] | Omit = omit, - build_context: Optional[blueprint_preview_params.BuildContext] | Omit = omit, code_mounts: Optional[Iterable[CodeMountParameters]] | Omit = omit, dockerfile: Optional[str] | Omit = omit, file_mounts: Optional[Dict[str, str]] | Omit = omit, launch_parameters: Optional[LaunchParameters] | Omit = omit, metadata: Optional[Dict[str, str]] | Omit = omit, - named_build_contexts: Optional[Dict[str, blueprint_preview_params.NamedBuildContexts]] | Omit = omit, secrets: Optional[Dict[str, str]] | Omit = omit, services: Optional[Iterable[blueprint_preview_params.Service]] | Omit = omit, system_setup_commands: Optional[SequenceNotStr[str]] | Omit = omit, @@ -688,8 +675,6 @@ def preview( build_args: (Optional) Arbitrary Docker build args to pass during build. - build_context: A build context backed by an Object. - code_mounts: A list of code mounts to be included in the Blueprint. dockerfile: Dockerfile contents to be used to build the Blueprint. @@ -700,11 +685,6 @@ def preview( metadata: (Optional) User defined metadata for the Blueprint. - named_build_contexts: (Optional) Map of named build contexts to attach to the Blueprint build, where - the keys are the name used when referencing the contexts in a Dockerfile. See - Docker buildx additional contexts for details: - https://docs.docker.com/reference/cli/docker/buildx/build/#build-context - secrets: (Optional) Map of mount IDs/environment variable names to secret names. Secrets will be available to commands during the build. Secrets are NOT stored in the blueprint image. Example: {"DB_PASS": "DATABASE_PASSWORD"} makes the secret @@ -734,13 +714,11 @@ def preview( "base_blueprint_id": base_blueprint_id, "base_blueprint_name": base_blueprint_name, "build_args": build_args, - "build_context": build_context, "code_mounts": code_mounts, "dockerfile": dockerfile, "file_mounts": file_mounts, "launch_parameters": launch_parameters, "metadata": metadata, - "named_build_contexts": named_build_contexts, "secrets": secrets, "services": services, "system_setup_commands": system_setup_commands, @@ -785,13 +763,11 @@ async def create( base_blueprint_id: Optional[str] | Omit = omit, base_blueprint_name: Optional[str] | Omit = omit, build_args: Optional[Dict[str, str]] | Omit = omit, - build_context: Optional[blueprint_create_params.BuildContext] | Omit = omit, code_mounts: Optional[Iterable[CodeMountParameters]] | Omit = omit, dockerfile: Optional[str] | Omit = omit, file_mounts: Optional[Dict[str, str]] | Omit = omit, launch_parameters: Optional[LaunchParameters] | Omit = omit, metadata: Optional[Dict[str, str]] | Omit = omit, - named_build_contexts: Optional[Dict[str, blueprint_create_params.NamedBuildContexts]] | Omit = omit, secrets: Optional[Dict[str, str]] | Omit = omit, services: Optional[Iterable[blueprint_create_params.Service]] | Omit = omit, system_setup_commands: Optional[SequenceNotStr[str]] | Omit = omit, @@ -823,8 +799,6 @@ async def create( build_args: (Optional) Arbitrary Docker build args to pass during build. - build_context: A build context backed by an Object. - code_mounts: A list of code mounts to be included in the Blueprint. dockerfile: Dockerfile contents to be used to build the Blueprint. @@ -835,11 +809,6 @@ async def create( metadata: (Optional) User defined metadata for the Blueprint. - named_build_contexts: (Optional) Map of named build contexts to attach to the Blueprint build, where - the keys are the name used when referencing the contexts in a Dockerfile. See - Docker buildx additional contexts for details: - https://docs.docker.com/reference/cli/docker/buildx/build/#build-context - secrets: (Optional) Map of mount IDs/environment variable names to secret names. Secrets will be available to commands during the build. Secrets are NOT stored in the blueprint image. Example: {"DB_PASS": "DATABASE_PASSWORD"} makes the secret @@ -873,13 +842,11 @@ async def create( "base_blueprint_id": base_blueprint_id, "base_blueprint_name": base_blueprint_name, "build_args": build_args, - "build_context": build_context, "code_mounts": code_mounts, "dockerfile": dockerfile, "file_mounts": file_mounts, "launch_parameters": launch_parameters, "metadata": metadata, - "named_build_contexts": named_build_contexts, "secrets": secrets, "services": services, "system_setup_commands": system_setup_commands, @@ -1307,13 +1274,11 @@ async def preview( base_blueprint_id: Optional[str] | Omit = omit, base_blueprint_name: Optional[str] | Omit = omit, build_args: Optional[Dict[str, str]] | Omit = omit, - build_context: Optional[blueprint_preview_params.BuildContext] | Omit = omit, code_mounts: Optional[Iterable[CodeMountParameters]] | Omit = omit, dockerfile: Optional[str] | Omit = omit, file_mounts: Optional[Dict[str, str]] | Omit = omit, launch_parameters: Optional[LaunchParameters] | Omit = omit, metadata: Optional[Dict[str, str]] | Omit = omit, - named_build_contexts: Optional[Dict[str, blueprint_preview_params.NamedBuildContexts]] | Omit = omit, secrets: Optional[Dict[str, str]] | Omit = omit, services: Optional[Iterable[blueprint_preview_params.Service]] | Omit = omit, system_setup_commands: Optional[SequenceNotStr[str]] | Omit = omit, @@ -1343,8 +1308,6 @@ async def preview( build_args: (Optional) Arbitrary Docker build args to pass during build. - build_context: A build context backed by an Object. - code_mounts: A list of code mounts to be included in the Blueprint. dockerfile: Dockerfile contents to be used to build the Blueprint. @@ -1355,11 +1318,6 @@ async def preview( metadata: (Optional) User defined metadata for the Blueprint. - named_build_contexts: (Optional) Map of named build contexts to attach to the Blueprint build, where - the keys are the name used when referencing the contexts in a Dockerfile. See - Docker buildx additional contexts for details: - https://docs.docker.com/reference/cli/docker/buildx/build/#build-context - secrets: (Optional) Map of mount IDs/environment variable names to secret names. Secrets will be available to commands during the build. Secrets are NOT stored in the blueprint image. Example: {"DB_PASS": "DATABASE_PASSWORD"} makes the secret @@ -1389,13 +1347,11 @@ async def preview( "base_blueprint_id": base_blueprint_id, "base_blueprint_name": base_blueprint_name, "build_args": build_args, - "build_context": build_context, "code_mounts": code_mounts, "dockerfile": dockerfile, "file_mounts": file_mounts, "launch_parameters": launch_parameters, "metadata": metadata, - "named_build_contexts": named_build_contexts, "secrets": secrets, "services": services, "system_setup_commands": system_setup_commands, diff --git a/src/runloop_api_client/resources/devboxes/devboxes.py b/src/runloop_api_client/resources/devboxes/devboxes.py index dbddfde64..febcac806 100644 --- a/src/runloop_api_client/resources/devboxes/devboxes.py +++ b/src/runloop_api_client/resources/devboxes/devboxes.py @@ -1038,47 +1038,6 @@ def execute_sync( cast_to=DevboxExecutionDetailView, ) - def keep_alive( - self, - id: str, - *, - # 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, - ) -> object: - """ - Send a 'Keep Alive' signal to a running Devbox that is configured to shutdown on - idle so the idle time resets. - - Args: - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - - idempotency_key: Specify a custom idempotency key for this request - """ - if not id: - raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") - return self._post( - f"/v1/devboxes/{id}/keep_alive", - options=make_request_options( - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - idempotency_key=idempotency_key, - ), - cast_to=object, - ) - def list_disk_snapshots( self, *, @@ -1241,49 +1200,6 @@ def remove_tunnel( cast_to=object, ) - def resume( - self, - id: str, - *, - # 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: - """Resume a suspended Devbox with the disk state captured as suspend time. - - Note - that any previously running processes or daemons will need to be restarted using - the Devbox shell tools. - - Args: - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - - idempotency_key: Specify a custom idempotency key for this request - """ - if not id: - raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") - return self._post( - f"/v1/devboxes/{id}/resume", - options=make_request_options( - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - idempotency_key=idempotency_key, - ), - cast_to=DevboxView, - ) - def shutdown( self, id: str, @@ -1446,48 +1362,6 @@ def snapshot_disk_async( cast_to=DevboxSnapshotView, ) - def suspend( - self, - id: str, - *, - # 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: - """ - Suspend a running Devbox and create a disk snapshot to enable resuming the - Devbox later with the same disk. Note this will not snapshot memory state such - as running processes. - - Args: - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - - idempotency_key: Specify a custom idempotency key for this request - """ - if not id: - raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") - return self._post( - f"/v1/devboxes/{id}/suspend", - options=make_request_options( - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - idempotency_key=idempotency_key, - ), - cast_to=DevboxView, - ) - def upload_file( self, id: str, @@ -2574,47 +2448,6 @@ async def execute_sync( cast_to=DevboxExecutionDetailView, ) - async def keep_alive( - self, - id: str, - *, - # 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, - ) -> object: - """ - Send a 'Keep Alive' signal to a running Devbox that is configured to shutdown on - idle so the idle time resets. - - Args: - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - - idempotency_key: Specify a custom idempotency key for this request - """ - if not id: - raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") - return await self._post( - f"/v1/devboxes/{id}/keep_alive", - options=make_request_options( - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - idempotency_key=idempotency_key, - ), - cast_to=object, - ) - def list_disk_snapshots( self, *, @@ -2777,49 +2610,6 @@ async def remove_tunnel( cast_to=object, ) - async def resume( - self, - id: str, - *, - # 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: - """Resume a suspended Devbox with the disk state captured as suspend time. - - Note - that any previously running processes or daemons will need to be restarted using - the Devbox shell tools. - - Args: - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - - idempotency_key: Specify a custom idempotency key for this request - """ - if not id: - raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") - return await self._post( - f"/v1/devboxes/{id}/resume", - options=make_request_options( - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - idempotency_key=idempotency_key, - ), - cast_to=DevboxView, - ) - async def shutdown( self, id: str, @@ -2982,48 +2772,6 @@ async def snapshot_disk_async( cast_to=DevboxSnapshotView, ) - async def suspend( - self, - id: str, - *, - # 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: - """ - Suspend a running Devbox and create a disk snapshot to enable resuming the - Devbox later with the same disk. Note this will not snapshot memory state such - as running processes. - - Args: - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - - idempotency_key: Specify a custom idempotency key for this request - """ - if not id: - raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") - return await self._post( - f"/v1/devboxes/{id}/suspend", - options=make_request_options( - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - idempotency_key=idempotency_key, - ), - cast_to=DevboxView, - ) - async def upload_file( self, id: str, @@ -3252,9 +3000,6 @@ def __init__(self, devboxes: DevboxesResource) -> None: devboxes.execute_sync, # pyright: ignore[reportDeprecated], ) ) - self.keep_alive = to_raw_response_wrapper( - devboxes.keep_alive, - ) self.list_disk_snapshots = to_raw_response_wrapper( devboxes.list_disk_snapshots, ) @@ -3264,9 +3009,6 @@ def __init__(self, devboxes: DevboxesResource) -> None: self.remove_tunnel = to_raw_response_wrapper( devboxes.remove_tunnel, ) - self.resume = to_raw_response_wrapper( - devboxes.resume, - ) self.shutdown = to_raw_response_wrapper( devboxes.shutdown, ) @@ -3276,9 +3018,6 @@ def __init__(self, devboxes: DevboxesResource) -> None: self.snapshot_disk_async = to_raw_response_wrapper( devboxes.snapshot_disk_async, ) - self.suspend = to_raw_response_wrapper( - devboxes.suspend, - ) self.upload_file = to_raw_response_wrapper( devboxes.upload_file, ) @@ -3350,9 +3089,6 @@ def __init__(self, devboxes: AsyncDevboxesResource) -> None: devboxes.execute_sync, # pyright: ignore[reportDeprecated], ) ) - self.keep_alive = async_to_raw_response_wrapper( - devboxes.keep_alive, - ) self.list_disk_snapshots = async_to_raw_response_wrapper( devboxes.list_disk_snapshots, ) @@ -3362,9 +3098,6 @@ def __init__(self, devboxes: AsyncDevboxesResource) -> None: self.remove_tunnel = async_to_raw_response_wrapper( devboxes.remove_tunnel, ) - self.resume = async_to_raw_response_wrapper( - devboxes.resume, - ) self.shutdown = async_to_raw_response_wrapper( devboxes.shutdown, ) @@ -3374,9 +3107,6 @@ def __init__(self, devboxes: AsyncDevboxesResource) -> None: self.snapshot_disk_async = async_to_raw_response_wrapper( devboxes.snapshot_disk_async, ) - self.suspend = async_to_raw_response_wrapper( - devboxes.suspend, - ) self.upload_file = async_to_raw_response_wrapper( devboxes.upload_file, ) @@ -3448,9 +3178,6 @@ def __init__(self, devboxes: DevboxesResource) -> None: devboxes.execute_sync, # pyright: ignore[reportDeprecated], ) ) - self.keep_alive = to_streamed_response_wrapper( - devboxes.keep_alive, - ) self.list_disk_snapshots = to_streamed_response_wrapper( devboxes.list_disk_snapshots, ) @@ -3460,9 +3187,6 @@ def __init__(self, devboxes: DevboxesResource) -> None: self.remove_tunnel = to_streamed_response_wrapper( devboxes.remove_tunnel, ) - self.resume = to_streamed_response_wrapper( - devboxes.resume, - ) self.shutdown = to_streamed_response_wrapper( devboxes.shutdown, ) @@ -3472,9 +3196,6 @@ def __init__(self, devboxes: DevboxesResource) -> None: self.snapshot_disk_async = to_streamed_response_wrapper( devboxes.snapshot_disk_async, ) - self.suspend = to_streamed_response_wrapper( - devboxes.suspend, - ) self.upload_file = to_streamed_response_wrapper( devboxes.upload_file, ) @@ -3546,9 +3267,6 @@ def __init__(self, devboxes: AsyncDevboxesResource) -> None: devboxes.execute_sync, # pyright: ignore[reportDeprecated], ) ) - self.keep_alive = async_to_streamed_response_wrapper( - devboxes.keep_alive, - ) self.list_disk_snapshots = async_to_streamed_response_wrapper( devboxes.list_disk_snapshots, ) @@ -3558,9 +3276,6 @@ def __init__(self, devboxes: AsyncDevboxesResource) -> None: self.remove_tunnel = async_to_streamed_response_wrapper( devboxes.remove_tunnel, ) - self.resume = async_to_streamed_response_wrapper( - devboxes.resume, - ) self.shutdown = async_to_streamed_response_wrapper( devboxes.shutdown, ) @@ -3570,9 +3285,6 @@ def __init__(self, devboxes: AsyncDevboxesResource) -> None: self.snapshot_disk_async = async_to_streamed_response_wrapper( devboxes.snapshot_disk_async, ) - self.suspend = async_to_streamed_response_wrapper( - devboxes.suspend, - ) self.upload_file = async_to_streamed_response_wrapper( devboxes.upload_file, ) diff --git a/src/runloop_api_client/types/blueprint_build_parameters.py b/src/runloop_api_client/types/blueprint_build_parameters.py index cc86468df..63a92f146 100644 --- a/src/runloop_api_client/types/blueprint_build_parameters.py +++ b/src/runloop_api_client/types/blueprint_build_parameters.py @@ -1,27 +1,12 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. from typing import Dict, List, Optional -from typing_extensions import Literal from .._models import BaseModel from .shared.launch_parameters import LaunchParameters from .shared.code_mount_parameters import CodeMountParameters -__all__ = ["BlueprintBuildParameters", "BuildContext", "NamedBuildContexts", "Service", "ServiceCredentials"] - - -class BuildContext(BaseModel): - object_id: str - """The ID of an object, whose contents are to be used as a build context.""" - - type: Literal["object"] - - -class NamedBuildContexts(BaseModel): - object_id: str - """The ID of an object, whose contents are to be used as a build context.""" - - type: Literal["object"] +__all__ = ["BlueprintBuildParameters", "Service", "ServiceCredentials"] class ServiceCredentials(BaseModel): @@ -76,9 +61,6 @@ class BlueprintBuildParameters(BaseModel): build_args: Optional[Dict[str, str]] = None """(Optional) Arbitrary Docker build args to pass during build.""" - build_context: Optional[BuildContext] = None - """A build context backed by an Object.""" - code_mounts: Optional[List[CodeMountParameters]] = None """A list of code mounts to be included in the Blueprint.""" @@ -94,14 +76,6 @@ class BlueprintBuildParameters(BaseModel): metadata: Optional[Dict[str, str]] = None """(Optional) User defined metadata for the Blueprint.""" - named_build_contexts: Optional[Dict[str, NamedBuildContexts]] = None - """ - (Optional) Map of named build contexts to attach to the Blueprint build, where - the keys are the name used when referencing the contexts in a Dockerfile. See - Docker buildx additional contexts for details: - https://docs.docker.com/reference/cli/docker/buildx/build/#build-context - """ - secrets: Optional[Dict[str, str]] = None """(Optional) Map of mount IDs/environment variable names to secret names. diff --git a/src/runloop_api_client/types/blueprint_create_params.py b/src/runloop_api_client/types/blueprint_create_params.py index a15e6f470..9d0a15848 100644 --- a/src/runloop_api_client/types/blueprint_create_params.py +++ b/src/runloop_api_client/types/blueprint_create_params.py @@ -3,13 +3,13 @@ from __future__ import annotations from typing import Dict, Iterable, Optional -from typing_extensions import Literal, Required, TypedDict +from typing_extensions import Required, TypedDict from .._types import SequenceNotStr from .shared_params.launch_parameters import LaunchParameters from .shared_params.code_mount_parameters import CodeMountParameters -__all__ = ["BlueprintCreateParams", "BuildContext", "NamedBuildContexts", "Service", "ServiceCredentials"] +__all__ = ["BlueprintCreateParams", "Service", "ServiceCredentials"] class BlueprintCreateParams(TypedDict, total=False): @@ -33,9 +33,6 @@ class BlueprintCreateParams(TypedDict, total=False): build_args: Optional[Dict[str, str]] """(Optional) Arbitrary Docker build args to pass during build.""" - build_context: Optional[BuildContext] - """A build context backed by an Object.""" - code_mounts: Optional[Iterable[CodeMountParameters]] """A list of code mounts to be included in the Blueprint.""" @@ -51,14 +48,6 @@ class BlueprintCreateParams(TypedDict, total=False): metadata: Optional[Dict[str, str]] """(Optional) User defined metadata for the Blueprint.""" - named_build_contexts: Optional[Dict[str, NamedBuildContexts]] - """ - (Optional) Map of named build contexts to attach to the Blueprint build, where - the keys are the name used when referencing the contexts in a Dockerfile. See - Docker buildx additional contexts for details: - https://docs.docker.com/reference/cli/docker/buildx/build/#build-context - """ - secrets: Optional[Dict[str, str]] """(Optional) Map of mount IDs/environment variable names to secret names. @@ -78,20 +67,6 @@ class BlueprintCreateParams(TypedDict, total=False): """A list of commands to run to set up your system.""" -class BuildContext(TypedDict, total=False): - object_id: Required[str] - """The ID of an object, whose contents are to be used as a build context.""" - - type: Required[Literal["object"]] - - -class NamedBuildContexts(TypedDict, total=False): - object_id: Required[str] - """The ID of an object, whose contents are to be used as a build context.""" - - type: Required[Literal["object"]] - - class ServiceCredentials(TypedDict, total=False): password: Required[str] """The password of the container service.""" diff --git a/src/runloop_api_client/types/blueprint_preview_params.py b/src/runloop_api_client/types/blueprint_preview_params.py index 81244126d..5c1e257f2 100644 --- a/src/runloop_api_client/types/blueprint_preview_params.py +++ b/src/runloop_api_client/types/blueprint_preview_params.py @@ -3,13 +3,13 @@ from __future__ import annotations from typing import Dict, Iterable, Optional -from typing_extensions import Literal, Required, TypedDict +from typing_extensions import Required, TypedDict from .._types import SequenceNotStr from .shared_params.launch_parameters import LaunchParameters from .shared_params.code_mount_parameters import CodeMountParameters -__all__ = ["BlueprintPreviewParams", "BuildContext", "NamedBuildContexts", "Service", "ServiceCredentials"] +__all__ = ["BlueprintPreviewParams", "Service", "ServiceCredentials"] class BlueprintPreviewParams(TypedDict, total=False): @@ -33,9 +33,6 @@ class BlueprintPreviewParams(TypedDict, total=False): build_args: Optional[Dict[str, str]] """(Optional) Arbitrary Docker build args to pass during build.""" - build_context: Optional[BuildContext] - """A build context backed by an Object.""" - code_mounts: Optional[Iterable[CodeMountParameters]] """A list of code mounts to be included in the Blueprint.""" @@ -51,14 +48,6 @@ class BlueprintPreviewParams(TypedDict, total=False): metadata: Optional[Dict[str, str]] """(Optional) User defined metadata for the Blueprint.""" - named_build_contexts: Optional[Dict[str, NamedBuildContexts]] - """ - (Optional) Map of named build contexts to attach to the Blueprint build, where - the keys are the name used when referencing the contexts in a Dockerfile. See - Docker buildx additional contexts for details: - https://docs.docker.com/reference/cli/docker/buildx/build/#build-context - """ - secrets: Optional[Dict[str, str]] """(Optional) Map of mount IDs/environment variable names to secret names. @@ -78,20 +67,6 @@ class BlueprintPreviewParams(TypedDict, total=False): """A list of commands to run to set up your system.""" -class BuildContext(TypedDict, total=False): - object_id: Required[str] - """The ID of an object, whose contents are to be used as a build context.""" - - type: Required[Literal["object"]] - - -class NamedBuildContexts(TypedDict, total=False): - object_id: Required[str] - """The ID of an object, whose contents are to be used as a build context.""" - - type: Required[Literal["object"]] - - class ServiceCredentials(TypedDict, total=False): password: Required[str] """The password of the container service.""" diff --git a/tests/api_resources/test_blueprints.py b/tests/api_resources/test_blueprints.py index ab53bf0db..729747a74 100644 --- a/tests/api_resources/test_blueprints.py +++ b/tests/api_resources/test_blueprints.py @@ -36,10 +36,6 @@ def test_method_create_with_all_params(self, client: Runloop) -> None: base_blueprint_id="base_blueprint_id", base_blueprint_name="base_blueprint_name", build_args={"foo": "string"}, - build_context={ - "object_id": "object_id", - "type": "object", - }, code_mounts=[ { "repo_name": "repo_name", @@ -71,12 +67,6 @@ def test_method_create_with_all_params(self, client: Runloop) -> None: }, }, metadata={"foo": "string"}, - named_build_contexts={ - "foo": { - "object_id": "object_id", - "type": "object", - } - }, secrets={"foo": "string"}, services=[ { @@ -405,10 +395,6 @@ def test_method_preview_with_all_params(self, client: Runloop) -> None: base_blueprint_id="base_blueprint_id", base_blueprint_name="base_blueprint_name", build_args={"foo": "string"}, - build_context={ - "object_id": "object_id", - "type": "object", - }, code_mounts=[ { "repo_name": "repo_name", @@ -440,12 +426,6 @@ def test_method_preview_with_all_params(self, client: Runloop) -> None: }, }, metadata={"foo": "string"}, - named_build_contexts={ - "foo": { - "object_id": "object_id", - "type": "object", - } - }, secrets={"foo": "string"}, services=[ { @@ -508,10 +488,6 @@ async def test_method_create_with_all_params(self, async_client: AsyncRunloop) - base_blueprint_id="base_blueprint_id", base_blueprint_name="base_blueprint_name", build_args={"foo": "string"}, - build_context={ - "object_id": "object_id", - "type": "object", - }, code_mounts=[ { "repo_name": "repo_name", @@ -543,12 +519,6 @@ async def test_method_create_with_all_params(self, async_client: AsyncRunloop) - }, }, metadata={"foo": "string"}, - named_build_contexts={ - "foo": { - "object_id": "object_id", - "type": "object", - } - }, secrets={"foo": "string"}, services=[ { @@ -877,10 +847,6 @@ async def test_method_preview_with_all_params(self, async_client: AsyncRunloop) base_blueprint_id="base_blueprint_id", base_blueprint_name="base_blueprint_name", build_args={"foo": "string"}, - build_context={ - "object_id": "object_id", - "type": "object", - }, code_mounts=[ { "repo_name": "repo_name", @@ -912,12 +878,6 @@ async def test_method_preview_with_all_params(self, async_client: AsyncRunloop) }, }, metadata={"foo": "string"}, - named_build_contexts={ - "foo": { - "object_id": "object_id", - "type": "object", - } - }, secrets={"foo": "string"}, services=[ { diff --git a/tests/api_resources/test_devboxes.py b/tests/api_resources/test_devboxes.py index 358c675f0..aef3aab61 100644 --- a/tests/api_resources/test_devboxes.py +++ b/tests/api_resources/test_devboxes.py @@ -584,44 +584,6 @@ def test_path_params_execute_sync(self, client: Runloop) -> None: command="command", ) - @parametrize - def test_method_keep_alive(self, client: Runloop) -> None: - devbox = client.devboxes.keep_alive( - "id", - ) - assert_matches_type(object, devbox, path=["response"]) - - @parametrize - def test_raw_response_keep_alive(self, client: Runloop) -> None: - response = client.devboxes.with_raw_response.keep_alive( - "id", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - devbox = response.parse() - assert_matches_type(object, devbox, path=["response"]) - - @parametrize - def test_streaming_response_keep_alive(self, client: Runloop) -> None: - with client.devboxes.with_streaming_response.keep_alive( - "id", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - devbox = response.parse() - assert_matches_type(object, devbox, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @parametrize - def test_path_params_keep_alive(self, client: Runloop) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): - client.devboxes.with_raw_response.keep_alive( - "", - ) - @parametrize def test_method_list_disk_snapshots(self, client: Runloop) -> None: devbox = client.devboxes.list_disk_snapshots() @@ -743,44 +705,6 @@ def test_path_params_remove_tunnel(self, client: Runloop) -> None: port=0, ) - @parametrize - def test_method_resume(self, client: Runloop) -> None: - devbox = client.devboxes.resume( - "id", - ) - assert_matches_type(DevboxView, devbox, path=["response"]) - - @parametrize - def test_raw_response_resume(self, client: Runloop) -> None: - response = client.devboxes.with_raw_response.resume( - "id", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - devbox = response.parse() - assert_matches_type(DevboxView, devbox, path=["response"]) - - @parametrize - def test_streaming_response_resume(self, client: Runloop) -> None: - with client.devboxes.with_streaming_response.resume( - "id", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - devbox = response.parse() - assert_matches_type(DevboxView, devbox, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @parametrize - def test_path_params_resume(self, client: Runloop) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): - client.devboxes.with_raw_response.resume( - "", - ) - @parametrize def test_method_shutdown(self, client: Runloop) -> None: devbox = client.devboxes.shutdown( @@ -915,44 +839,6 @@ def test_path_params_snapshot_disk_async(self, client: Runloop) -> None: id="", ) - @parametrize - def test_method_suspend(self, client: Runloop) -> None: - devbox = client.devboxes.suspend( - "id", - ) - assert_matches_type(DevboxView, devbox, path=["response"]) - - @parametrize - def test_raw_response_suspend(self, client: Runloop) -> None: - response = client.devboxes.with_raw_response.suspend( - "id", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - devbox = response.parse() - assert_matches_type(DevboxView, devbox, path=["response"]) - - @parametrize - def test_streaming_response_suspend(self, client: Runloop) -> None: - with client.devboxes.with_streaming_response.suspend( - "id", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - devbox = response.parse() - assert_matches_type(DevboxView, devbox, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @parametrize - def test_path_params_suspend(self, client: Runloop) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): - client.devboxes.with_raw_response.suspend( - "", - ) - @parametrize def test_method_upload_file(self, client: Runloop) -> None: devbox = client.devboxes.upload_file( @@ -2133,44 +2019,6 @@ async def test_path_params_execute_sync(self, async_client: AsyncRunloop) -> Non command="command", ) - @parametrize - async def test_method_keep_alive(self, async_client: AsyncRunloop) -> None: - devbox = await async_client.devboxes.keep_alive( - "id", - ) - assert_matches_type(object, devbox, path=["response"]) - - @parametrize - async def test_raw_response_keep_alive(self, async_client: AsyncRunloop) -> None: - response = await async_client.devboxes.with_raw_response.keep_alive( - "id", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - devbox = await response.parse() - assert_matches_type(object, devbox, path=["response"]) - - @parametrize - async def test_streaming_response_keep_alive(self, async_client: AsyncRunloop) -> None: - async with async_client.devboxes.with_streaming_response.keep_alive( - "id", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - devbox = await response.parse() - assert_matches_type(object, devbox, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @parametrize - async def test_path_params_keep_alive(self, async_client: AsyncRunloop) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): - await async_client.devboxes.with_raw_response.keep_alive( - "", - ) - @parametrize async def test_method_list_disk_snapshots(self, async_client: AsyncRunloop) -> None: devbox = await async_client.devboxes.list_disk_snapshots() @@ -2292,44 +2140,6 @@ async def test_path_params_remove_tunnel(self, async_client: AsyncRunloop) -> No port=0, ) - @parametrize - async def test_method_resume(self, async_client: AsyncRunloop) -> None: - devbox = await async_client.devboxes.resume( - "id", - ) - assert_matches_type(DevboxView, devbox, path=["response"]) - - @parametrize - async def test_raw_response_resume(self, async_client: AsyncRunloop) -> None: - response = await async_client.devboxes.with_raw_response.resume( - "id", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - devbox = await response.parse() - assert_matches_type(DevboxView, devbox, path=["response"]) - - @parametrize - async def test_streaming_response_resume(self, async_client: AsyncRunloop) -> None: - async with async_client.devboxes.with_streaming_response.resume( - "id", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - devbox = await response.parse() - assert_matches_type(DevboxView, devbox, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @parametrize - async def test_path_params_resume(self, async_client: AsyncRunloop) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): - await async_client.devboxes.with_raw_response.resume( - "", - ) - @parametrize async def test_method_shutdown(self, async_client: AsyncRunloop) -> None: devbox = await async_client.devboxes.shutdown( @@ -2464,44 +2274,6 @@ async def test_path_params_snapshot_disk_async(self, async_client: AsyncRunloop) id="", ) - @parametrize - async def test_method_suspend(self, async_client: AsyncRunloop) -> None: - devbox = await async_client.devboxes.suspend( - "id", - ) - assert_matches_type(DevboxView, devbox, path=["response"]) - - @parametrize - async def test_raw_response_suspend(self, async_client: AsyncRunloop) -> None: - response = await async_client.devboxes.with_raw_response.suspend( - "id", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - devbox = await response.parse() - assert_matches_type(DevboxView, devbox, path=["response"]) - - @parametrize - async def test_streaming_response_suspend(self, async_client: AsyncRunloop) -> None: - async with async_client.devboxes.with_streaming_response.suspend( - "id", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - devbox = await response.parse() - assert_matches_type(DevboxView, devbox, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @parametrize - async def test_path_params_suspend(self, async_client: AsyncRunloop) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): - await async_client.devboxes.with_raw_response.suspend( - "", - ) - @parametrize async def test_method_upload_file(self, async_client: AsyncRunloop) -> None: devbox = await async_client.devboxes.upload_file( From 1c9c346e475b64fc389928fee0f7140e532c4f9c Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 24 Nov 2025 23:40:54 +0000 Subject: [PATCH 2/9] fix(devbox): launch parameter typo --- .stats.yml | 4 ++-- src/runloop_api_client/types/shared/launch_parameters.py | 2 +- .../types/shared_params/launch_parameters.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.stats.yml b/.stats.yml index 1235f571e..d57d5f1ec 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 94 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/runloop-ai%2Frunloop-907baea7d51fd2660895c74603cf1ff765eb9f62eb10ce6e0c1d17ac1c16abf2.yml -openapi_spec_hash: f1280edb22cdd91238efc2b18e3d324c +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/runloop-ai%2Frunloop-ae41f8669a7ba1eec677634573b55bace4dedcf7027e0c1fee503d7c44472e3e.yml +openapi_spec_hash: 9a3c12559ec74a00adffe7b254757903 config_hash: 2363f563f42501d2b1587a4f64bdccaf diff --git a/src/runloop_api_client/types/shared/launch_parameters.py b/src/runloop_api_client/types/shared/launch_parameters.py index 04901ebfc..b45cced7c 100644 --- a/src/runloop_api_client/types/shared/launch_parameters.py +++ b/src/runloop_api_client/types/shared/launch_parameters.py @@ -11,7 +11,7 @@ class UserParameters(BaseModel): uid: int - """User ID (UID) for the Linux user. Must be a positive integer.""" + """User ID (UID) for the Linux user. Must be a non-negative integer.""" username: str """Username for the Linux user.""" diff --git a/src/runloop_api_client/types/shared_params/launch_parameters.py b/src/runloop_api_client/types/shared_params/launch_parameters.py index 303835be3..5016d2acb 100644 --- a/src/runloop_api_client/types/shared_params/launch_parameters.py +++ b/src/runloop_api_client/types/shared_params/launch_parameters.py @@ -13,7 +13,7 @@ class UserParameters(TypedDict, total=False): uid: Required[int] - """User ID (UID) for the Linux user. Must be a positive integer.""" + """User ID (UID) for the Linux user. Must be a non-negative integer.""" username: Required[str] """Username for the Linux user.""" From df43a42a45b9ce67aba27835a41c9a0ebfc6a407 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 25 Nov 2025 02:37:37 +0000 Subject: [PATCH 3/9] fix(scorer): fixed RL_TEST_CONTEXT to RL_SCORER_CONTEXT --- .stats.yml | 4 ++-- .../types/scenarios/scorer_create_response.py | 2 +- .../types/scenarios/scorer_list_response.py | 2 +- .../types/scenarios/scorer_retrieve_response.py | 2 +- .../types/scenarios/scorer_update_response.py | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.stats.yml b/.stats.yml index d57d5f1ec..f02816b21 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 94 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/runloop-ai%2Frunloop-ae41f8669a7ba1eec677634573b55bace4dedcf7027e0c1fee503d7c44472e3e.yml -openapi_spec_hash: 9a3c12559ec74a00adffe7b254757903 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/runloop-ai%2Frunloop-c15740e10009ccfaf6cb6ae4463496618c05d033bdd779bf501f5afb0c05ffc9.yml +openapi_spec_hash: a08d1a45d83ed2956ab643d1c6c07a40 config_hash: 2363f563f42501d2b1587a4f64bdccaf diff --git a/src/runloop_api_client/types/scenarios/scorer_create_response.py b/src/runloop_api_client/types/scenarios/scorer_create_response.py index 345779a1d..376c50f70 100644 --- a/src/runloop_api_client/types/scenarios/scorer_create_response.py +++ b/src/runloop_api_client/types/scenarios/scorer_create_response.py @@ -10,7 +10,7 @@ class ScorerCreateResponse(BaseModel): """ID for the scenario scorer.""" bash_script: str - """Bash script that takes in $RL_TEST_CONTEXT as env variable and runs scoring.""" + """Bash script that takes in $RL_SCORER_CONTEXT as env variable and runs scoring.""" type: str """Name of the type of scenario scorer.""" diff --git a/src/runloop_api_client/types/scenarios/scorer_list_response.py b/src/runloop_api_client/types/scenarios/scorer_list_response.py index 8f8b12e15..bdbc9b9de 100644 --- a/src/runloop_api_client/types/scenarios/scorer_list_response.py +++ b/src/runloop_api_client/types/scenarios/scorer_list_response.py @@ -10,7 +10,7 @@ class ScorerListResponse(BaseModel): """ID for the scenario scorer.""" bash_script: str - """Bash script that takes in $RL_TEST_CONTEXT as env variable and runs scoring.""" + """Bash script that takes in $RL_SCORER_CONTEXT as env variable and runs scoring.""" type: str """Name of the type of scenario scorer.""" diff --git a/src/runloop_api_client/types/scenarios/scorer_retrieve_response.py b/src/runloop_api_client/types/scenarios/scorer_retrieve_response.py index f2dd7f0b1..ab0f85231 100644 --- a/src/runloop_api_client/types/scenarios/scorer_retrieve_response.py +++ b/src/runloop_api_client/types/scenarios/scorer_retrieve_response.py @@ -10,7 +10,7 @@ class ScorerRetrieveResponse(BaseModel): """ID for the scenario scorer.""" bash_script: str - """Bash script that takes in $RL_TEST_CONTEXT as env variable and runs scoring.""" + """Bash script that takes in $RL_SCORER_CONTEXT as env variable and runs scoring.""" type: str """Name of the type of scenario scorer.""" diff --git a/src/runloop_api_client/types/scenarios/scorer_update_response.py b/src/runloop_api_client/types/scenarios/scorer_update_response.py index 540107613..60a1b5e4b 100644 --- a/src/runloop_api_client/types/scenarios/scorer_update_response.py +++ b/src/runloop_api_client/types/scenarios/scorer_update_response.py @@ -10,7 +10,7 @@ class ScorerUpdateResponse(BaseModel): """ID for the scenario scorer.""" bash_script: str - """Bash script that takes in $RL_TEST_CONTEXT as env variable and runs scoring.""" + """Bash script that takes in $RL_SCORER_CONTEXT as env variable and runs scoring.""" type: str """Name of the type of scenario scorer.""" From fe3589f5fbb36a5b79f1d4a25e86f88676556fdb Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 25 Nov 2025 04:08:08 +0000 Subject: [PATCH 4/9] fix(api): don't ignore devbox keep_alive, suspend and resume in api --- .stats.yml | 6 +- api.md | 3 + .../resources/devboxes/devboxes.py | 288 ++++++++++++++++++ tests/api_resources/test_devboxes.py | 228 ++++++++++++++ 4 files changed, 522 insertions(+), 3 deletions(-) diff --git a/.stats.yml b/.stats.yml index f02816b21..21ba6b325 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 94 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/runloop-ai%2Frunloop-c15740e10009ccfaf6cb6ae4463496618c05d033bdd779bf501f5afb0c05ffc9.yml -openapi_spec_hash: a08d1a45d83ed2956ab643d1c6c07a40 +configured_endpoints: 97 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/runloop-ai%2Frunloop-d2463b4c27719ea7275c8f587fa2c90e333471fdede11e5f944faa92dfb417d1.yml +openapi_spec_hash: 132c1d9f04583e997df5d7698e155fff config_hash: 2363f563f42501d2b1587a4f64bdccaf diff --git a/api.md b/api.md index 2c8632112..a3c529f83 100644 --- a/api.md +++ b/api.md @@ -126,12 +126,15 @@ Methods: - client.devboxes.execute(id, \*\*params) -> DevboxAsyncExecutionDetailView - client.devboxes.execute_async(id, \*\*params) -> DevboxAsyncExecutionDetailView - client.devboxes.execute_sync(id, \*\*params) -> DevboxExecutionDetailView +- client.devboxes.keep_alive(id) -> object - client.devboxes.list_disk_snapshots(\*\*params) -> SyncDiskSnapshotsCursorIDPage[DevboxSnapshotView] - client.devboxes.read_file_contents(id, \*\*params) -> str - client.devboxes.remove_tunnel(id, \*\*params) -> object +- client.devboxes.resume(id) -> DevboxView - client.devboxes.shutdown(id) -> DevboxView - client.devboxes.snapshot_disk(id, \*\*params) -> DevboxSnapshotView - client.devboxes.snapshot_disk_async(id, \*\*params) -> DevboxSnapshotView +- client.devboxes.suspend(id) -> DevboxView - client.devboxes.upload_file(id, \*\*params) -> object - client.devboxes.wait_for_command(execution_id, \*, devbox_id, \*\*params) -> DevboxAsyncExecutionDetailView - client.devboxes.write_file_contents(id, \*\*params) -> DevboxExecutionDetailView diff --git a/src/runloop_api_client/resources/devboxes/devboxes.py b/src/runloop_api_client/resources/devboxes/devboxes.py index febcac806..dbddfde64 100644 --- a/src/runloop_api_client/resources/devboxes/devboxes.py +++ b/src/runloop_api_client/resources/devboxes/devboxes.py @@ -1038,6 +1038,47 @@ def execute_sync( cast_to=DevboxExecutionDetailView, ) + def keep_alive( + self, + id: str, + *, + # 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, + ) -> object: + """ + Send a 'Keep Alive' signal to a running Devbox that is configured to shutdown on + idle so the idle time resets. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._post( + f"/v1/devboxes/{id}/keep_alive", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=object, + ) + def list_disk_snapshots( self, *, @@ -1200,6 +1241,49 @@ def remove_tunnel( cast_to=object, ) + def resume( + self, + id: str, + *, + # 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: + """Resume a suspended Devbox with the disk state captured as suspend time. + + Note + that any previously running processes or daemons will need to be restarted using + the Devbox shell tools. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._post( + f"/v1/devboxes/{id}/resume", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=DevboxView, + ) + def shutdown( self, id: str, @@ -1362,6 +1446,48 @@ def snapshot_disk_async( cast_to=DevboxSnapshotView, ) + def suspend( + self, + id: str, + *, + # 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: + """ + Suspend a running Devbox and create a disk snapshot to enable resuming the + Devbox later with the same disk. Note this will not snapshot memory state such + as running processes. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._post( + f"/v1/devboxes/{id}/suspend", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=DevboxView, + ) + def upload_file( self, id: str, @@ -2448,6 +2574,47 @@ async def execute_sync( cast_to=DevboxExecutionDetailView, ) + async def keep_alive( + self, + id: str, + *, + # 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, + ) -> object: + """ + Send a 'Keep Alive' signal to a running Devbox that is configured to shutdown on + idle so the idle time resets. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._post( + f"/v1/devboxes/{id}/keep_alive", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=object, + ) + def list_disk_snapshots( self, *, @@ -2610,6 +2777,49 @@ async def remove_tunnel( cast_to=object, ) + async def resume( + self, + id: str, + *, + # 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: + """Resume a suspended Devbox with the disk state captured as suspend time. + + Note + that any previously running processes or daemons will need to be restarted using + the Devbox shell tools. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._post( + f"/v1/devboxes/{id}/resume", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=DevboxView, + ) + async def shutdown( self, id: str, @@ -2772,6 +2982,48 @@ async def snapshot_disk_async( cast_to=DevboxSnapshotView, ) + async def suspend( + self, + id: str, + *, + # 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: + """ + Suspend a running Devbox and create a disk snapshot to enable resuming the + Devbox later with the same disk. Note this will not snapshot memory state such + as running processes. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._post( + f"/v1/devboxes/{id}/suspend", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, + ), + cast_to=DevboxView, + ) + async def upload_file( self, id: str, @@ -3000,6 +3252,9 @@ def __init__(self, devboxes: DevboxesResource) -> None: devboxes.execute_sync, # pyright: ignore[reportDeprecated], ) ) + self.keep_alive = to_raw_response_wrapper( + devboxes.keep_alive, + ) self.list_disk_snapshots = to_raw_response_wrapper( devboxes.list_disk_snapshots, ) @@ -3009,6 +3264,9 @@ def __init__(self, devboxes: DevboxesResource) -> None: self.remove_tunnel = to_raw_response_wrapper( devboxes.remove_tunnel, ) + self.resume = to_raw_response_wrapper( + devboxes.resume, + ) self.shutdown = to_raw_response_wrapper( devboxes.shutdown, ) @@ -3018,6 +3276,9 @@ def __init__(self, devboxes: DevboxesResource) -> None: self.snapshot_disk_async = to_raw_response_wrapper( devboxes.snapshot_disk_async, ) + self.suspend = to_raw_response_wrapper( + devboxes.suspend, + ) self.upload_file = to_raw_response_wrapper( devboxes.upload_file, ) @@ -3089,6 +3350,9 @@ def __init__(self, devboxes: AsyncDevboxesResource) -> None: devboxes.execute_sync, # pyright: ignore[reportDeprecated], ) ) + self.keep_alive = async_to_raw_response_wrapper( + devboxes.keep_alive, + ) self.list_disk_snapshots = async_to_raw_response_wrapper( devboxes.list_disk_snapshots, ) @@ -3098,6 +3362,9 @@ def __init__(self, devboxes: AsyncDevboxesResource) -> None: self.remove_tunnel = async_to_raw_response_wrapper( devboxes.remove_tunnel, ) + self.resume = async_to_raw_response_wrapper( + devboxes.resume, + ) self.shutdown = async_to_raw_response_wrapper( devboxes.shutdown, ) @@ -3107,6 +3374,9 @@ def __init__(self, devboxes: AsyncDevboxesResource) -> None: self.snapshot_disk_async = async_to_raw_response_wrapper( devboxes.snapshot_disk_async, ) + self.suspend = async_to_raw_response_wrapper( + devboxes.suspend, + ) self.upload_file = async_to_raw_response_wrapper( devboxes.upload_file, ) @@ -3178,6 +3448,9 @@ def __init__(self, devboxes: DevboxesResource) -> None: devboxes.execute_sync, # pyright: ignore[reportDeprecated], ) ) + self.keep_alive = to_streamed_response_wrapper( + devboxes.keep_alive, + ) self.list_disk_snapshots = to_streamed_response_wrapper( devboxes.list_disk_snapshots, ) @@ -3187,6 +3460,9 @@ def __init__(self, devboxes: DevboxesResource) -> None: self.remove_tunnel = to_streamed_response_wrapper( devboxes.remove_tunnel, ) + self.resume = to_streamed_response_wrapper( + devboxes.resume, + ) self.shutdown = to_streamed_response_wrapper( devboxes.shutdown, ) @@ -3196,6 +3472,9 @@ def __init__(self, devboxes: DevboxesResource) -> None: self.snapshot_disk_async = to_streamed_response_wrapper( devboxes.snapshot_disk_async, ) + self.suspend = to_streamed_response_wrapper( + devboxes.suspend, + ) self.upload_file = to_streamed_response_wrapper( devboxes.upload_file, ) @@ -3267,6 +3546,9 @@ def __init__(self, devboxes: AsyncDevboxesResource) -> None: devboxes.execute_sync, # pyright: ignore[reportDeprecated], ) ) + self.keep_alive = async_to_streamed_response_wrapper( + devboxes.keep_alive, + ) self.list_disk_snapshots = async_to_streamed_response_wrapper( devboxes.list_disk_snapshots, ) @@ -3276,6 +3558,9 @@ def __init__(self, devboxes: AsyncDevboxesResource) -> None: self.remove_tunnel = async_to_streamed_response_wrapper( devboxes.remove_tunnel, ) + self.resume = async_to_streamed_response_wrapper( + devboxes.resume, + ) self.shutdown = async_to_streamed_response_wrapper( devboxes.shutdown, ) @@ -3285,6 +3570,9 @@ def __init__(self, devboxes: AsyncDevboxesResource) -> None: self.snapshot_disk_async = async_to_streamed_response_wrapper( devboxes.snapshot_disk_async, ) + self.suspend = async_to_streamed_response_wrapper( + devboxes.suspend, + ) self.upload_file = async_to_streamed_response_wrapper( devboxes.upload_file, ) diff --git a/tests/api_resources/test_devboxes.py b/tests/api_resources/test_devboxes.py index aef3aab61..358c675f0 100644 --- a/tests/api_resources/test_devboxes.py +++ b/tests/api_resources/test_devboxes.py @@ -584,6 +584,44 @@ def test_path_params_execute_sync(self, client: Runloop) -> None: command="command", ) + @parametrize + def test_method_keep_alive(self, client: Runloop) -> None: + devbox = client.devboxes.keep_alive( + "id", + ) + assert_matches_type(object, devbox, path=["response"]) + + @parametrize + def test_raw_response_keep_alive(self, client: Runloop) -> None: + response = client.devboxes.with_raw_response.keep_alive( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + devbox = response.parse() + assert_matches_type(object, devbox, path=["response"]) + + @parametrize + def test_streaming_response_keep_alive(self, client: Runloop) -> None: + with client.devboxes.with_streaming_response.keep_alive( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + devbox = response.parse() + assert_matches_type(object, devbox, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_keep_alive(self, client: Runloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.devboxes.with_raw_response.keep_alive( + "", + ) + @parametrize def test_method_list_disk_snapshots(self, client: Runloop) -> None: devbox = client.devboxes.list_disk_snapshots() @@ -705,6 +743,44 @@ def test_path_params_remove_tunnel(self, client: Runloop) -> None: port=0, ) + @parametrize + def test_method_resume(self, client: Runloop) -> None: + devbox = client.devboxes.resume( + "id", + ) + assert_matches_type(DevboxView, devbox, path=["response"]) + + @parametrize + def test_raw_response_resume(self, client: Runloop) -> None: + response = client.devboxes.with_raw_response.resume( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + devbox = response.parse() + assert_matches_type(DevboxView, devbox, path=["response"]) + + @parametrize + def test_streaming_response_resume(self, client: Runloop) -> None: + with client.devboxes.with_streaming_response.resume( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + devbox = response.parse() + assert_matches_type(DevboxView, devbox, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_resume(self, client: Runloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.devboxes.with_raw_response.resume( + "", + ) + @parametrize def test_method_shutdown(self, client: Runloop) -> None: devbox = client.devboxes.shutdown( @@ -839,6 +915,44 @@ def test_path_params_snapshot_disk_async(self, client: Runloop) -> None: id="", ) + @parametrize + def test_method_suspend(self, client: Runloop) -> None: + devbox = client.devboxes.suspend( + "id", + ) + assert_matches_type(DevboxView, devbox, path=["response"]) + + @parametrize + def test_raw_response_suspend(self, client: Runloop) -> None: + response = client.devboxes.with_raw_response.suspend( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + devbox = response.parse() + assert_matches_type(DevboxView, devbox, path=["response"]) + + @parametrize + def test_streaming_response_suspend(self, client: Runloop) -> None: + with client.devboxes.with_streaming_response.suspend( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + devbox = response.parse() + assert_matches_type(DevboxView, devbox, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_suspend(self, client: Runloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.devboxes.with_raw_response.suspend( + "", + ) + @parametrize def test_method_upload_file(self, client: Runloop) -> None: devbox = client.devboxes.upload_file( @@ -2019,6 +2133,44 @@ async def test_path_params_execute_sync(self, async_client: AsyncRunloop) -> Non command="command", ) + @parametrize + async def test_method_keep_alive(self, async_client: AsyncRunloop) -> None: + devbox = await async_client.devboxes.keep_alive( + "id", + ) + assert_matches_type(object, devbox, path=["response"]) + + @parametrize + async def test_raw_response_keep_alive(self, async_client: AsyncRunloop) -> None: + response = await async_client.devboxes.with_raw_response.keep_alive( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + devbox = await response.parse() + assert_matches_type(object, devbox, path=["response"]) + + @parametrize + async def test_streaming_response_keep_alive(self, async_client: AsyncRunloop) -> None: + async with async_client.devboxes.with_streaming_response.keep_alive( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + devbox = await response.parse() + assert_matches_type(object, devbox, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_keep_alive(self, async_client: AsyncRunloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.devboxes.with_raw_response.keep_alive( + "", + ) + @parametrize async def test_method_list_disk_snapshots(self, async_client: AsyncRunloop) -> None: devbox = await async_client.devboxes.list_disk_snapshots() @@ -2140,6 +2292,44 @@ async def test_path_params_remove_tunnel(self, async_client: AsyncRunloop) -> No port=0, ) + @parametrize + async def test_method_resume(self, async_client: AsyncRunloop) -> None: + devbox = await async_client.devboxes.resume( + "id", + ) + assert_matches_type(DevboxView, devbox, path=["response"]) + + @parametrize + async def test_raw_response_resume(self, async_client: AsyncRunloop) -> None: + response = await async_client.devboxes.with_raw_response.resume( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + devbox = await response.parse() + assert_matches_type(DevboxView, devbox, path=["response"]) + + @parametrize + async def test_streaming_response_resume(self, async_client: AsyncRunloop) -> None: + async with async_client.devboxes.with_streaming_response.resume( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + devbox = await response.parse() + assert_matches_type(DevboxView, devbox, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_resume(self, async_client: AsyncRunloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.devboxes.with_raw_response.resume( + "", + ) + @parametrize async def test_method_shutdown(self, async_client: AsyncRunloop) -> None: devbox = await async_client.devboxes.shutdown( @@ -2274,6 +2464,44 @@ async def test_path_params_snapshot_disk_async(self, async_client: AsyncRunloop) id="", ) + @parametrize + async def test_method_suspend(self, async_client: AsyncRunloop) -> None: + devbox = await async_client.devboxes.suspend( + "id", + ) + assert_matches_type(DevboxView, devbox, path=["response"]) + + @parametrize + async def test_raw_response_suspend(self, async_client: AsyncRunloop) -> None: + response = await async_client.devboxes.with_raw_response.suspend( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + devbox = await response.parse() + assert_matches_type(DevboxView, devbox, path=["response"]) + + @parametrize + async def test_streaming_response_suspend(self, async_client: AsyncRunloop) -> None: + async with async_client.devboxes.with_streaming_response.suspend( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + devbox = await response.parse() + assert_matches_type(DevboxView, devbox, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_suspend(self, async_client: AsyncRunloop) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.devboxes.with_raw_response.suspend( + "", + ) + @parametrize async def test_method_upload_file(self, async_client: AsyncRunloop) -> None: devbox = await async_client.devboxes.upload_file( From d202b942c07614ca954a8bbe3a9a6302e9a04216 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 25 Nov 2025 22:36:17 +0000 Subject: [PATCH 5/9] feat(blueprints): Add build context to the OpenAPI spec (#6494) --- .stats.yml | 4 ++-- src/runloop_api_client/resources/blueprints.py | 16 ++++++++++++++++ .../types/blueprint_build_parameters.py | 13 ++++++++++++- .../types/blueprint_create_params.py | 14 ++++++++++++-- .../types/blueprint_preview_params.py | 14 ++++++++++++-- src/runloop_api_client/types/shared/mount.py | 11 +++++------ .../types/shared/run_profile.py | 6 +++++- .../types/shared_params/mount.py | 11 +++++------ .../types/shared_params/run_profile.py | 6 +++++- tests/api_resources/test_benchmarks.py | 14 ++++++++++++++ tests/api_resources/test_blueprints.py | 16 ++++++++++++++++ tests/api_resources/test_scenarios.py | 14 ++++++++++++++ 12 files changed, 118 insertions(+), 21 deletions(-) diff --git a/.stats.yml b/.stats.yml index 21ba6b325..a7dd140f8 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 97 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/runloop-ai%2Frunloop-d2463b4c27719ea7275c8f587fa2c90e333471fdede11e5f944faa92dfb417d1.yml -openapi_spec_hash: 132c1d9f04583e997df5d7698e155fff +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/runloop-ai%2Frunloop-b1e4697ee11a301905abe34736d6a2e74a2200c2f9bade48b6f50ee2d65a814f.yml +openapi_spec_hash: 3ebe459b324ae2757ba3bee9d1484e90 config_hash: 2363f563f42501d2b1587a4f64bdccaf diff --git a/src/runloop_api_client/resources/blueprints.py b/src/runloop_api_client/resources/blueprints.py index 7456f762e..060e4ba1a 100644 --- a/src/runloop_api_client/resources/blueprints.py +++ b/src/runloop_api_client/resources/blueprints.py @@ -130,6 +130,7 @@ def create( base_blueprint_id: Optional[str] | Omit = omit, base_blueprint_name: Optional[str] | Omit = omit, build_args: Optional[Dict[str, str]] | Omit = omit, + build_context: Optional[blueprint_create_params.BuildContext] | Omit = omit, code_mounts: Optional[Iterable[CodeMountParameters]] | Omit = omit, dockerfile: Optional[str] | Omit = omit, file_mounts: Optional[Dict[str, str]] | Omit = omit, @@ -166,6 +167,8 @@ def create( build_args: (Optional) Arbitrary Docker build args to pass during build. + build_context: A build context backed by an Object. + code_mounts: A list of code mounts to be included in the Blueprint. dockerfile: Dockerfile contents to be used to build the Blueprint. @@ -209,6 +212,7 @@ def create( "base_blueprint_id": base_blueprint_id, "base_blueprint_name": base_blueprint_name, "build_args": build_args, + "build_context": build_context, "code_mounts": code_mounts, "dockerfile": dockerfile, "file_mounts": file_mounts, @@ -641,6 +645,7 @@ def preview( base_blueprint_id: Optional[str] | Omit = omit, base_blueprint_name: Optional[str] | Omit = omit, build_args: Optional[Dict[str, str]] | Omit = omit, + build_context: Optional[blueprint_preview_params.BuildContext] | Omit = omit, code_mounts: Optional[Iterable[CodeMountParameters]] | Omit = omit, dockerfile: Optional[str] | Omit = omit, file_mounts: Optional[Dict[str, str]] | Omit = omit, @@ -675,6 +680,8 @@ def preview( build_args: (Optional) Arbitrary Docker build args to pass during build. + build_context: A build context backed by an Object. + code_mounts: A list of code mounts to be included in the Blueprint. dockerfile: Dockerfile contents to be used to build the Blueprint. @@ -714,6 +721,7 @@ def preview( "base_blueprint_id": base_blueprint_id, "base_blueprint_name": base_blueprint_name, "build_args": build_args, + "build_context": build_context, "code_mounts": code_mounts, "dockerfile": dockerfile, "file_mounts": file_mounts, @@ -763,6 +771,7 @@ async def create( base_blueprint_id: Optional[str] | Omit = omit, base_blueprint_name: Optional[str] | Omit = omit, build_args: Optional[Dict[str, str]] | Omit = omit, + build_context: Optional[blueprint_create_params.BuildContext] | Omit = omit, code_mounts: Optional[Iterable[CodeMountParameters]] | Omit = omit, dockerfile: Optional[str] | Omit = omit, file_mounts: Optional[Dict[str, str]] | Omit = omit, @@ -799,6 +808,8 @@ async def create( build_args: (Optional) Arbitrary Docker build args to pass during build. + build_context: A build context backed by an Object. + code_mounts: A list of code mounts to be included in the Blueprint. dockerfile: Dockerfile contents to be used to build the Blueprint. @@ -842,6 +853,7 @@ async def create( "base_blueprint_id": base_blueprint_id, "base_blueprint_name": base_blueprint_name, "build_args": build_args, + "build_context": build_context, "code_mounts": code_mounts, "dockerfile": dockerfile, "file_mounts": file_mounts, @@ -1274,6 +1286,7 @@ async def preview( base_blueprint_id: Optional[str] | Omit = omit, base_blueprint_name: Optional[str] | Omit = omit, build_args: Optional[Dict[str, str]] | Omit = omit, + build_context: Optional[blueprint_preview_params.BuildContext] | Omit = omit, code_mounts: Optional[Iterable[CodeMountParameters]] | Omit = omit, dockerfile: Optional[str] | Omit = omit, file_mounts: Optional[Dict[str, str]] | Omit = omit, @@ -1308,6 +1321,8 @@ async def preview( build_args: (Optional) Arbitrary Docker build args to pass during build. + build_context: A build context backed by an Object. + code_mounts: A list of code mounts to be included in the Blueprint. dockerfile: Dockerfile contents to be used to build the Blueprint. @@ -1347,6 +1362,7 @@ async def preview( "base_blueprint_id": base_blueprint_id, "base_blueprint_name": base_blueprint_name, "build_args": build_args, + "build_context": build_context, "code_mounts": code_mounts, "dockerfile": dockerfile, "file_mounts": file_mounts, diff --git a/src/runloop_api_client/types/blueprint_build_parameters.py b/src/runloop_api_client/types/blueprint_build_parameters.py index 63a92f146..129a8047a 100644 --- a/src/runloop_api_client/types/blueprint_build_parameters.py +++ b/src/runloop_api_client/types/blueprint_build_parameters.py @@ -1,12 +1,20 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. from typing import Dict, List, Optional +from typing_extensions import Literal from .._models import BaseModel from .shared.launch_parameters import LaunchParameters from .shared.code_mount_parameters import CodeMountParameters -__all__ = ["BlueprintBuildParameters", "Service", "ServiceCredentials"] +__all__ = ["BlueprintBuildParameters", "BuildContext", "Service", "ServiceCredentials"] + + +class BuildContext(BaseModel): + object_id: str + """The ID of an object, whose contents are to be used as a build context.""" + + type: Literal["object"] class ServiceCredentials(BaseModel): @@ -61,6 +69,9 @@ class BlueprintBuildParameters(BaseModel): build_args: Optional[Dict[str, str]] = None """(Optional) Arbitrary Docker build args to pass during build.""" + build_context: Optional[BuildContext] = None + """A build context backed by an Object.""" + code_mounts: Optional[List[CodeMountParameters]] = None """A list of code mounts to be included in the Blueprint.""" diff --git a/src/runloop_api_client/types/blueprint_create_params.py b/src/runloop_api_client/types/blueprint_create_params.py index 9d0a15848..d82de7f35 100644 --- a/src/runloop_api_client/types/blueprint_create_params.py +++ b/src/runloop_api_client/types/blueprint_create_params.py @@ -3,13 +3,13 @@ from __future__ import annotations from typing import Dict, Iterable, Optional -from typing_extensions import Required, TypedDict +from typing_extensions import Literal, Required, TypedDict from .._types import SequenceNotStr from .shared_params.launch_parameters import LaunchParameters from .shared_params.code_mount_parameters import CodeMountParameters -__all__ = ["BlueprintCreateParams", "Service", "ServiceCredentials"] +__all__ = ["BlueprintCreateParams", "BuildContext", "Service", "ServiceCredentials"] class BlueprintCreateParams(TypedDict, total=False): @@ -33,6 +33,9 @@ class BlueprintCreateParams(TypedDict, total=False): build_args: Optional[Dict[str, str]] """(Optional) Arbitrary Docker build args to pass during build.""" + build_context: Optional[BuildContext] + """A build context backed by an Object.""" + code_mounts: Optional[Iterable[CodeMountParameters]] """A list of code mounts to be included in the Blueprint.""" @@ -67,6 +70,13 @@ class BlueprintCreateParams(TypedDict, total=False): """A list of commands to run to set up your system.""" +class BuildContext(TypedDict, total=False): + object_id: Required[str] + """The ID of an object, whose contents are to be used as a build context.""" + + type: Required[Literal["object"]] + + class ServiceCredentials(TypedDict, total=False): password: Required[str] """The password of the container service.""" diff --git a/src/runloop_api_client/types/blueprint_preview_params.py b/src/runloop_api_client/types/blueprint_preview_params.py index 5c1e257f2..9f6c4d9bc 100644 --- a/src/runloop_api_client/types/blueprint_preview_params.py +++ b/src/runloop_api_client/types/blueprint_preview_params.py @@ -3,13 +3,13 @@ from __future__ import annotations from typing import Dict, Iterable, Optional -from typing_extensions import Required, TypedDict +from typing_extensions import Literal, Required, TypedDict from .._types import SequenceNotStr from .shared_params.launch_parameters import LaunchParameters from .shared_params.code_mount_parameters import CodeMountParameters -__all__ = ["BlueprintPreviewParams", "Service", "ServiceCredentials"] +__all__ = ["BlueprintPreviewParams", "BuildContext", "Service", "ServiceCredentials"] class BlueprintPreviewParams(TypedDict, total=False): @@ -33,6 +33,9 @@ class BlueprintPreviewParams(TypedDict, total=False): build_args: Optional[Dict[str, str]] """(Optional) Arbitrary Docker build args to pass during build.""" + build_context: Optional[BuildContext] + """A build context backed by an Object.""" + code_mounts: Optional[Iterable[CodeMountParameters]] """A list of code mounts to be included in the Blueprint.""" @@ -67,6 +70,13 @@ class BlueprintPreviewParams(TypedDict, total=False): """A list of commands to run to set up your system.""" +class BuildContext(TypedDict, total=False): + object_id: Required[str] + """The ID of an object, whose contents are to be used as a build context.""" + + type: Required[Literal["object"]] + + class ServiceCredentials(TypedDict, total=False): password: Required[str] """The password of the container service.""" diff --git a/src/runloop_api_client/types/shared/mount.py b/src/runloop_api_client/types/shared/mount.py index 4ebc3eafb..9f8186386 100644 --- a/src/runloop_api_client/types/shared/mount.py +++ b/src/runloop_api_client/types/shared/mount.py @@ -1,6 +1,6 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import Dict, Union +from typing import Union from typing_extensions import Literal, Annotated, TypeAlias from ..._utils import PropertyInfo @@ -13,12 +13,11 @@ class FileMountParameters(BaseModel): - files: Dict[str, str] - """Map of file paths to file contents to be written before setup. + content: str + """Content of the file to mount.""" - Keys are absolute paths where files should be created, values are the file - contents. - """ + target: str + """Target path where the file should be mounted.""" type: Literal["file_mount"] diff --git a/src/runloop_api_client/types/shared/run_profile.py b/src/runloop_api_client/types/shared/run_profile.py index 21a29ef38..21cf31f92 100644 --- a/src/runloop_api_client/types/shared/run_profile.py +++ b/src/runloop_api_client/types/shared/run_profile.py @@ -1,9 +1,10 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import Dict, Optional +from typing import Dict, List, Optional from pydantic import Field as FieldInfo +from .mount import Mount from ..._models import BaseModel from .launch_parameters import LaunchParameters @@ -21,6 +22,9 @@ class RunProfile(BaseModel): launch_parameters: Optional[LaunchParameters] = FieldInfo(alias="launchParameters", default=None) """Additional runtime LaunchParameters to apply after the devbox starts.""" + mounts: Optional[List[Mount]] = None + """A list of mounts to be included in the scenario run.""" + purpose: Optional[str] = None """Purpose of the run.""" diff --git a/src/runloop_api_client/types/shared_params/mount.py b/src/runloop_api_client/types/shared_params/mount.py index 9f9631013..1b680e810 100644 --- a/src/runloop_api_client/types/shared_params/mount.py +++ b/src/runloop_api_client/types/shared_params/mount.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import Dict, Union +from typing import Union from typing_extensions import Literal, Required, TypeAlias, TypedDict from .code_mount_parameters import CodeMountParameters @@ -13,12 +13,11 @@ class FileMountParameters(TypedDict, total=False): - files: Required[Dict[str, str]] - """Map of file paths to file contents to be written before setup. + content: Required[str] + """Content of the file to mount.""" - Keys are absolute paths where files should be created, values are the file - contents. - """ + target: Required[str] + """Target path where the file should be mounted.""" type: Required[Literal["file_mount"]] diff --git a/src/runloop_api_client/types/shared_params/run_profile.py b/src/runloop_api_client/types/shared_params/run_profile.py index 20816c36d..10f82d5f7 100644 --- a/src/runloop_api_client/types/shared_params/run_profile.py +++ b/src/runloop_api_client/types/shared_params/run_profile.py @@ -2,9 +2,10 @@ from __future__ import annotations -from typing import Dict, Optional +from typing import Dict, Iterable, Optional from typing_extensions import Annotated, TypedDict +from .mount import Mount from ..._utils import PropertyInfo from .launch_parameters import LaunchParameters @@ -22,6 +23,9 @@ class RunProfile(TypedDict, total=False): launch_parameters: Annotated[Optional[LaunchParameters], PropertyInfo(alias="launchParameters")] """Additional runtime LaunchParameters to apply after the devbox starts.""" + mounts: Optional[Iterable[Mount]] + """A list of mounts to be included in the scenario run.""" + purpose: Optional[str] """Purpose of the run.""" diff --git a/tests/api_resources/test_benchmarks.py b/tests/api_resources/test_benchmarks.py index 0a40742ee..891756def 100644 --- a/tests/api_resources/test_benchmarks.py +++ b/tests/api_resources/test_benchmarks.py @@ -307,6 +307,13 @@ def test_method_start_run_with_all_params(self, client: Runloop) -> None: "username": "username", }, }, + "mounts": [ + { + "object_id": "object_id", + "object_path": "object_path", + "type": "object_mount", + } + ], "purpose": "purpose", "secrets": {"foo": "string"}, }, @@ -628,6 +635,13 @@ async def test_method_start_run_with_all_params(self, async_client: AsyncRunloop "username": "username", }, }, + "mounts": [ + { + "object_id": "object_id", + "object_path": "object_path", + "type": "object_mount", + } + ], "purpose": "purpose", "secrets": {"foo": "string"}, }, diff --git a/tests/api_resources/test_blueprints.py b/tests/api_resources/test_blueprints.py index 729747a74..c38517715 100644 --- a/tests/api_resources/test_blueprints.py +++ b/tests/api_resources/test_blueprints.py @@ -36,6 +36,10 @@ def test_method_create_with_all_params(self, client: Runloop) -> None: base_blueprint_id="base_blueprint_id", base_blueprint_name="base_blueprint_name", build_args={"foo": "string"}, + build_context={ + "object_id": "object_id", + "type": "object", + }, code_mounts=[ { "repo_name": "repo_name", @@ -395,6 +399,10 @@ def test_method_preview_with_all_params(self, client: Runloop) -> None: base_blueprint_id="base_blueprint_id", base_blueprint_name="base_blueprint_name", build_args={"foo": "string"}, + build_context={ + "object_id": "object_id", + "type": "object", + }, code_mounts=[ { "repo_name": "repo_name", @@ -488,6 +496,10 @@ async def test_method_create_with_all_params(self, async_client: AsyncRunloop) - base_blueprint_id="base_blueprint_id", base_blueprint_name="base_blueprint_name", build_args={"foo": "string"}, + build_context={ + "object_id": "object_id", + "type": "object", + }, code_mounts=[ { "repo_name": "repo_name", @@ -847,6 +859,10 @@ async def test_method_preview_with_all_params(self, async_client: AsyncRunloop) base_blueprint_id="base_blueprint_id", base_blueprint_name="base_blueprint_name", build_args={"foo": "string"}, + build_context={ + "object_id": "object_id", + "type": "object", + }, code_mounts=[ { "repo_name": "repo_name", diff --git a/tests/api_resources/test_scenarios.py b/tests/api_resources/test_scenarios.py index be50c0a77..b9dadb8b9 100644 --- a/tests/api_resources/test_scenarios.py +++ b/tests/api_resources/test_scenarios.py @@ -383,6 +383,13 @@ def test_method_start_run_with_all_params(self, client: Runloop) -> None: "username": "username", }, }, + "mounts": [ + { + "object_id": "object_id", + "object_path": "object_path", + "type": "object_mount", + } + ], "purpose": "purpose", "secrets": {"foo": "string"}, }, @@ -781,6 +788,13 @@ async def test_method_start_run_with_all_params(self, async_client: AsyncRunloop "username": "username", }, }, + "mounts": [ + { + "object_id": "object_id", + "object_path": "object_path", + "type": "object_mount", + } + ], "purpose": "purpose", "secrets": {"foo": "string"}, }, From 4936844989ec7a0d37c835dd37b8007e8caba944 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 25 Nov 2025 23:45:27 +0000 Subject: [PATCH 6/9] chore(mounts): Update documentation for deprecated fields to direct the user to the replacement API --- .stats.yml | 4 ++-- src/runloop_api_client/resources/devboxes/devboxes.py | 8 ++++---- src/runloop_api_client/types/devbox_create_params.py | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.stats.yml b/.stats.yml index a7dd140f8..0ab1f506b 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 97 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/runloop-ai%2Frunloop-b1e4697ee11a301905abe34736d6a2e74a2200c2f9bade48b6f50ee2d65a814f.yml -openapi_spec_hash: 3ebe459b324ae2757ba3bee9d1484e90 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/runloop-ai%2Frunloop-cb2d725f71e87810cd872eacd70e867ca10f94980fdf9c78bb2844c02ee47bf3.yml +openapi_spec_hash: 16ce3e9184fc2afdee66db18a83a96e8 config_hash: 2363f563f42501d2b1587a4f64bdccaf diff --git a/src/runloop_api_client/resources/devboxes/devboxes.py b/src/runloop_api_client/resources/devboxes/devboxes.py index dbddfde64..fc13c722d 100644 --- a/src/runloop_api_client/resources/devboxes/devboxes.py +++ b/src/runloop_api_client/resources/devboxes/devboxes.py @@ -217,7 +217,7 @@ def create( successfully built Blueprint with the given name. Only one of (Snapshot ID, Blueprint ID, Blueprint name) should be specified. - code_mounts: A list of code mounts to be included in the Devbox. + code_mounts: A list of code mounts to be included in the Devbox. Use mounts instead. entrypoint: (Optional) When specified, the Devbox will run this script as its main executable. The devbox lifecycle will be bound to entrypoint, shutting down when @@ -225,7 +225,7 @@ def create( environment_variables: (Optional) Environment variables used to configure your Devbox. - file_mounts: (Optional) Map of paths and file contents to write before setup.. + file_mounts: Map of paths and file contents to write before setup. Use mounts instead. launch_parameters: Parameters to configure the resources and launch time behavior of the Devbox. @@ -1755,7 +1755,7 @@ async def create( successfully built Blueprint with the given name. Only one of (Snapshot ID, Blueprint ID, Blueprint name) should be specified. - code_mounts: A list of code mounts to be included in the Devbox. + code_mounts: A list of code mounts to be included in the Devbox. Use mounts instead. entrypoint: (Optional) When specified, the Devbox will run this script as its main executable. The devbox lifecycle will be bound to entrypoint, shutting down when @@ -1763,7 +1763,7 @@ async def create( environment_variables: (Optional) Environment variables used to configure your Devbox. - file_mounts: (Optional) Map of paths and file contents to write before setup.. + file_mounts: Map of paths and file contents to write before setup. Use mounts instead. launch_parameters: Parameters to configure the resources and launch time behavior of the Devbox. diff --git a/src/runloop_api_client/types/devbox_create_params.py b/src/runloop_api_client/types/devbox_create_params.py index f211e0444..91651ad87 100644 --- a/src/runloop_api_client/types/devbox_create_params.py +++ b/src/runloop_api_client/types/devbox_create_params.py @@ -22,7 +22,7 @@ # create methods. class DevboxBaseCreateParams(TypedDict, total=False): code_mounts: Optional[Iterable[CodeMountParameters]] - """A list of code mounts to be included in the Devbox.""" + """A list of code mounts to be included in the Devbox. Use mounts instead.""" entrypoint: Optional[str] """ @@ -35,7 +35,7 @@ class DevboxBaseCreateParams(TypedDict, total=False): """(Optional) Environment variables used to configure your Devbox.""" file_mounts: Optional[Dict[str, str]] - """(Optional) Map of paths and file contents to write before setup..""" + """Map of paths and file contents to write before setup. Use mounts instead.""" launch_parameters: Optional[LaunchParameters] """Parameters to configure the resources and launch time behavior of the Devbox.""" From 6e6392864b3cde20dfea5d173fed9a156b960ccd Mon Sep 17 00:00:00 2001 From: Adam Lesinski Date: Tue, 25 Nov 2025 16:07:53 -0800 Subject: [PATCH 7/9] chore(blueprints): Add build context examples (#694) --- src/runloop_api_client/sdk/async_.py | 17 +++++++++++++++++ src/runloop_api_client/sdk/sync.py | 17 +++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/src/runloop_api_client/sdk/async_.py b/src/runloop_api_client/sdk/async_.py index 2dc4562c2..2f78ba795 100644 --- a/src/runloop_api_client/sdk/async_.py +++ b/src/runloop_api_client/sdk/async_.py @@ -216,6 +216,23 @@ class AsyncBlueprintOps: ... dockerfile="FROM ubuntu:22.04\\nRUN apt-get update", ... ) >>> blueprints = await runloop.blueprint.list() + + To use a local directory as a build context, use an object. + + Example: + >>> from datetime import timedelta + >>> from runloop_api_client.types.blueprint_build_parameters import BuildContext + >>> + >>> runloop = AsyncRunloopSDK() + >>> obj = await runloop.object_storage.upload_from_dir( + ... "./", + ... ttl=timedelta(hours=1), + ... ) + >>> blueprint = await runloop.blueprint.create( + ... name="my-blueprint", + ... dockerfile="FROM ubuntu:22.04\\nCOPY . .\\n", + ... build_context=BuildContext(type="object", object_id=obj.id), + ... ) """ def __init__(self, client: AsyncRunloop) -> None: diff --git a/src/runloop_api_client/sdk/sync.py b/src/runloop_api_client/sdk/sync.py index 94715cca4..0bd7f68e7 100644 --- a/src/runloop_api_client/sdk/sync.py +++ b/src/runloop_api_client/sdk/sync.py @@ -215,6 +215,23 @@ class BlueprintOps: ... name="my-blueprint", dockerfile="FROM ubuntu:22.04\\nRUN apt-get update" ... ) >>> blueprints = runloop.blueprint.list() + + To use a local directory as a build context, use an object. + + Example: + >>> from datetime import timedelta + >>> from runloop_api_client.types.blueprint_build_parameters import BuildContext + >>> + >>> runloop = RunloopSDK() + >>> obj = runloop.object_storage.upload_from_dir( + ... "./", + ... ttl=timedelta(hours=1), + ... ) + >>> blueprint = runloop.blueprint.create( + ... name="my-blueprint", + ... dockerfile="FROM ubuntu:22.04\\nCOPY . .\\n", + ... build_context=BuildContext(type="object", object_id=obj.id), + ... ) """ def __init__(self, client: Runloop) -> None: From 85f798f2d8a7727b783e01a260ff0a52bdf01d78 Mon Sep 17 00:00:00 2001 From: sid-rl Date: Mon, 1 Dec 2025 17:24:17 -0700 Subject: [PATCH 8/9] feat(sdk): added scorer classes to sdk (#698) * added scorer class (kept create and list as static methods for now since we don't know how we're creating scorers yet) * refactored static methods to ScorerOps class * fix example docstrings to use correct scorer create params * scorer tests * fixed scorer unit test parameters for update and validate * update scorer and scorer ops docstrings to be more helpful while not exposing system internals * update docs with scorer classes, methods and types * remove verbose request options in unit test parameters * rename client to ops in client test * rename client test file to ops * added list_empty, list_single and list_multiple unit tests to all ops class tests * fix assert_called to assert_awaited * remove duplicate tests --- docs/sdk/async/index.rst | 1 + docs/sdk/async/scorer.rst | 9 + docs/sdk/sync/index.rst | 1 + docs/sdk/sync/scorer.rst | 9 + docs/sdk/types.rst | 13 + src/runloop_api_client/sdk/__init__.py | 9 +- src/runloop_api_client/sdk/_types.py | 17 + src/runloop_api_client/sdk/async_.py | 55 +++ src/runloop_api_client/sdk/async_scorer.py | 77 ++++ src/runloop_api_client/sdk/scorer.py | 77 ++++ src/runloop_api_client/sdk/sync.py | 55 +++ tests/sdk/conftest.py | 16 + tests/sdk/test_async_blueprint.py | 8 +- tests/sdk/test_async_execution_result.py | 4 +- ...est_async_clients.py => test_async_ops.py} | 344 ++++++++++++++---- tests/sdk/test_async_scorer.py | 69 ++++ tests/sdk/test_async_snapshot.py | 10 +- tests/sdk/test_async_storage_object.py | 10 +- tests/sdk/{test_clients.py => test_ops.py} | 308 ++++++++++++---- tests/sdk/test_scorer.py | 71 ++++ tests/smoketests/sdk/test_async_scorer.py | 122 +++++++ tests/smoketests/sdk/test_scorer.py | 122 +++++++ 22 files changed, 1259 insertions(+), 148 deletions(-) create mode 100644 docs/sdk/async/scorer.rst create mode 100644 docs/sdk/sync/scorer.rst create mode 100644 src/runloop_api_client/sdk/async_scorer.py create mode 100644 src/runloop_api_client/sdk/scorer.py rename tests/sdk/{test_async_clients.py => test_async_ops.py} (59%) create mode 100644 tests/sdk/test_async_scorer.py rename tests/sdk/{test_clients.py => test_ops.py} (60%) create mode 100644 tests/sdk/test_scorer.py create mode 100644 tests/smoketests/sdk/test_async_scorer.py create mode 100644 tests/smoketests/sdk/test_scorer.py diff --git a/docs/sdk/async/index.rst b/docs/sdk/async/index.rst index 1d92ea76f..0ea16d5e4 100644 --- a/docs/sdk/async/index.rst +++ b/docs/sdk/async/index.rst @@ -26,4 +26,5 @@ Asynchronous resource classes for working with devboxes, blueprints, snapshots, blueprint snapshot storage_object + scorer diff --git a/docs/sdk/async/scorer.rst b/docs/sdk/async/scorer.rst new file mode 100644 index 000000000..7564092d8 --- /dev/null +++ b/docs/sdk/async/scorer.rst @@ -0,0 +1,9 @@ +Scorer +====== + +The ``AsyncScorer`` class provides asynchronous methods for managing custom scenario scorers. + +.. automodule:: runloop_api_client.sdk.async_scorer + :members: + + diff --git a/docs/sdk/sync/index.rst b/docs/sdk/sync/index.rst index c77646f2a..e7d1ca616 100644 --- a/docs/sdk/sync/index.rst +++ b/docs/sdk/sync/index.rst @@ -26,4 +26,5 @@ Synchronous resource classes for working with devboxes, blueprints, snapshots, a blueprint snapshot storage_object + scorer diff --git a/docs/sdk/sync/scorer.rst b/docs/sdk/sync/scorer.rst new file mode 100644 index 000000000..09b98dfb2 --- /dev/null +++ b/docs/sdk/sync/scorer.rst @@ -0,0 +1,9 @@ +Scorer +====== + +The ``Scorer`` class provides synchronous methods for managing custom scenario scorers. + +.. automodule:: runloop_api_client.sdk.scorer + :members: + + diff --git a/docs/sdk/types.rst b/docs/sdk/types.rst index 4de60fb5e..9d2983cd0 100644 --- a/docs/sdk/types.rst +++ b/docs/sdk/types.rst @@ -78,6 +78,19 @@ These TypeDicts define parameters for storage object creation, listing, and down .. autotypeddict:: runloop_api_client.sdk._types.SDKObjectDownloadParams +Scorer Parameters +----------------- + +These TypeDicts define parameters for scorer creation, listing, updating, and validation. + +.. autotypeddict:: runloop_api_client.sdk._types.SDKScorerCreateParams + +.. autotypeddict:: runloop_api_client.sdk._types.SDKScorerListParams + +.. autotypeddict:: runloop_api_client.sdk._types.SDKScorerUpdateParams + +.. autotypeddict:: runloop_api_client.sdk._types.SDKScorerValidateParams + Core Request Options -------------------- diff --git a/src/runloop_api_client/sdk/__init__.py b/src/runloop_api_client/sdk/__init__.py index b08b5bf87..48b5e3103 100644 --- a/src/runloop_api_client/sdk/__init__.py +++ b/src/runloop_api_client/sdk/__init__.py @@ -5,19 +5,22 @@ from __future__ import annotations -from .sync import DevboxOps, RunloopSDK, SnapshotOps, BlueprintOps, StorageObjectOps +from .sync import DevboxOps, ScorerOps, RunloopSDK, SnapshotOps, BlueprintOps, StorageObjectOps from .async_ import ( AsyncDevboxOps, + AsyncScorerOps, AsyncRunloopSDK, AsyncSnapshotOps, AsyncBlueprintOps, AsyncStorageObjectOps, ) from .devbox import Devbox, NamedShell +from .scorer import Scorer from .snapshot import Snapshot from .blueprint import Blueprint from .execution import Execution from .async_devbox import AsyncDevbox, AsyncNamedShell +from .async_scorer import AsyncScorer from .async_snapshot import AsyncSnapshot from .storage_object import StorageObject from .async_blueprint import AsyncBlueprint @@ -35,6 +38,8 @@ "AsyncDevboxOps", "BlueprintOps", "AsyncBlueprintOps", + "ScorerOps", + "AsyncScorerOps", "SnapshotOps", "AsyncSnapshotOps", "StorageObjectOps", @@ -48,6 +53,8 @@ "AsyncExecutionResult", "Blueprint", "AsyncBlueprint", + "Scorer", + "AsyncScorer", "Snapshot", "AsyncSnapshot", "StorageObject", diff --git a/src/runloop_api_client/sdk/_types.py b/src/runloop_api_client/sdk/_types.py index 9cb0e21a9..028cb1805 100644 --- a/src/runloop_api_client/sdk/_types.py +++ b/src/runloop_api_client/sdk/_types.py @@ -4,6 +4,7 @@ from .._types import Body, Query, Headers, Timeout, NotGiven from ..lib.polling import PollingConfig from ..types.devboxes import DiskSnapshotListParams, DiskSnapshotUpdateParams +from ..types.scenarios import ScorerListParams, ScorerCreateParams, ScorerUpdateParams, ScorerValidateParams from ..types.devbox_list_params import DevboxListParams from ..types.object_list_params import ObjectListParams from ..types.devbox_create_params import DevboxCreateParams, DevboxBaseCreateParams @@ -140,3 +141,19 @@ class SDKObjectCreateParams(ObjectCreateParams, LongRequestOptions): class SDKObjectDownloadParams(ObjectDownloadParams, BaseRequestOptions): pass + + +class SDKScorerCreateParams(ScorerCreateParams, LongRequestOptions): + pass + + +class SDKScorerListParams(ScorerListParams, BaseRequestOptions): + pass + + +class SDKScorerUpdateParams(ScorerUpdateParams, LongRequestOptions): + pass + + +class SDKScorerValidateParams(ScorerValidateParams, LongRequestOptions): + pass diff --git a/src/runloop_api_client/sdk/async_.py b/src/runloop_api_client/sdk/async_.py index 2f78ba795..23f87d6e9 100644 --- a/src/runloop_api_client/sdk/async_.py +++ b/src/runloop_api_client/sdk/async_.py @@ -16,8 +16,10 @@ LongRequestOptions, SDKDevboxListParams, SDKObjectListParams, + SDKScorerListParams, SDKDevboxCreateParams, SDKObjectCreateParams, + SDKScorerCreateParams, SDKBlueprintListParams, SDKBlueprintCreateParams, SDKDiskSnapshotListParams, @@ -27,6 +29,7 @@ from .._client import DEFAULT_MAX_RETRIES, AsyncRunloop from ._helpers import detect_content_type from .async_devbox import AsyncDevbox +from .async_scorer import AsyncScorer from .async_snapshot import AsyncSnapshot from .async_blueprint import AsyncBlueprint from .async_storage_object import AsyncStorageObject @@ -492,6 +495,54 @@ async def upload_from_bytes( return obj +class AsyncScorerOps: + """Create and manage custom scorers (async). Access via ``runloop.scorer``. + + Example: + >>> runloop = AsyncRunloopSDK() + >>> scorer = await runloop.scorer.create(type="my_scorer", bash_script="echo 'score=1.0'") + >>> all_scorers = await runloop.scorer.list() + """ + + def __init__(self, client: AsyncRunloop) -> None: + """Initialize AsyncScorerOps. + + :param client: AsyncRunloop client instance + :type client: AsyncRunloop + """ + self._client = client + + async def create(self, **params: Unpack[SDKScorerCreateParams]) -> AsyncScorer: + """Create a new scorer with the given type and bash script. + + :param params: See :typeddict:`~runloop_api_client.sdk._types.SDKScorerCreateParams` for available parameters + :return: The newly created scorer + :rtype: AsyncScorer + """ + response = await self._client.scenarios.scorers.create(**params) + return AsyncScorer(self._client, response.id) + + def from_id(self, scorer_id: str) -> AsyncScorer: + """Get an AsyncScorer instance for an existing scorer ID. + + :param scorer_id: ID of the scorer + :type scorer_id: str + :return: AsyncScorer instance for the given ID + :rtype: AsyncScorer + """ + return AsyncScorer(self._client, scorer_id) + + async def list(self, **params: Unpack[SDKScorerListParams]) -> list[AsyncScorer]: + """List all scorers, optionally filtered by parameters. + + :param params: See :typeddict:`~runloop_api_client.sdk._types.SDKScorerListParams` for available parameters + :return: List of scorers + :rtype: list[AsyncScorer] + """ + page = await self._client.scenarios.scorers.list(**params) + return [AsyncScorer(self._client, item.id) async for item in page] + + class AsyncRunloopSDK: """High-level asynchronous entry point for the Runloop SDK. @@ -505,6 +556,8 @@ class AsyncRunloopSDK: :vartype devbox: AsyncDevboxOps :ivar blueprint: High-level async interface for blueprint management :vartype blueprint: AsyncBlueprintOps + :ivar scorer: High-level async interface for scorer management + :vartype scorer: AsyncScorerOps :ivar snapshot: High-level async interface for snapshot management :vartype snapshot: AsyncSnapshotOps :ivar storage_object: High-level async interface for storage object management @@ -521,6 +574,7 @@ class AsyncRunloopSDK: api: AsyncRunloop devbox: AsyncDevboxOps blueprint: AsyncBlueprintOps + scorer: AsyncScorerOps snapshot: AsyncSnapshotOps storage_object: AsyncStorageObjectOps @@ -564,6 +618,7 @@ def __init__( self.devbox = AsyncDevboxOps(self.api) self.blueprint = AsyncBlueprintOps(self.api) + self.scorer = AsyncScorerOps(self.api) self.snapshot = AsyncSnapshotOps(self.api) self.storage_object = AsyncStorageObjectOps(self.api) diff --git a/src/runloop_api_client/sdk/async_scorer.py b/src/runloop_api_client/sdk/async_scorer.py new file mode 100644 index 000000000..3df4fb4e0 --- /dev/null +++ b/src/runloop_api_client/sdk/async_scorer.py @@ -0,0 +1,77 @@ +"""Scorer resource class for asynchronous operations.""" + +from __future__ import annotations + +from typing_extensions import Unpack, override + +from ._types import ( + BaseRequestOptions, + SDKScorerUpdateParams, + SDKScorerValidateParams, +) +from .._client import AsyncRunloop +from ..types.scenarios import ScorerUpdateResponse, ScorerRetrieveResponse, ScorerValidateResponse + + +class AsyncScorer: + """A custom scorer for evaluating scenario outputs (async). + + Scorers define bash scripts that produce a score (0.0-1.0) for scenario runs. + Obtain instances via ``runloop.scorer.create()`` or ``runloop.scorer.from_id()``. + + Example: + >>> runloop = AsyncRunloopSDK() + >>> scorer = await runloop.scorer.create(type="my_scorer", bash_script="echo 'score=1.0'") + >>> await scorer.validate(scoring_context={"output": "test"}) + """ + + def __init__(self, client: AsyncRunloop, scorer_id: str) -> None: + """Create an AsyncScorer instance. + + :param client: AsyncRunloop client instance + :type client: AsyncRunloop + :param scorer_id: ID of the scorer + :type scorer_id: str + """ + self._client = client + self._id = scorer_id + + @override + def __repr__(self) -> str: + return f"" + + @property + def id(self) -> str: + """The scorer's unique identifier. + + :return: Scorer ID + :rtype: str + """ + return self._id + + async def get_info(self, **options: Unpack[BaseRequestOptions]) -> ScorerRetrieveResponse: + """Fetch current scorer details from the API. + + :param options: See :typeddict:`~runloop_api_client.sdk._types.BaseRequestOptions` for available options + :return: Current scorer details + :rtype: ScorerRetrieveResponse + """ + return await self._client.scenarios.scorers.retrieve(self._id, **options) + + async def update(self, **params: Unpack[SDKScorerUpdateParams]) -> ScorerUpdateResponse: + """Update the scorer's type or bash script. + + :param params: See :typeddict:`~runloop_api_client.sdk._types.SDKScorerUpdateParams` for available parameters + :return: Updated scorer details + :rtype: ScorerUpdateResponse + """ + return await self._client.scenarios.scorers.update(self._id, **params) + + async def validate(self, **params: Unpack[SDKScorerValidateParams]) -> ScorerValidateResponse: + """Run the scorer against the provided context and return the result. + + :param params: See :typeddict:`~runloop_api_client.sdk._types.SDKScorerValidateParams` for available parameters + :return: Validation result with score + :rtype: ScorerValidateResponse + """ + return await self._client.scenarios.scorers.validate(self._id, **params) diff --git a/src/runloop_api_client/sdk/scorer.py b/src/runloop_api_client/sdk/scorer.py new file mode 100644 index 000000000..a25bb44a8 --- /dev/null +++ b/src/runloop_api_client/sdk/scorer.py @@ -0,0 +1,77 @@ +"""Scorer resource class for synchronous operations.""" + +from __future__ import annotations + +from typing_extensions import Unpack, override + +from ._types import ( + BaseRequestOptions, + SDKScorerUpdateParams, + SDKScorerValidateParams, +) +from .._client import Runloop +from ..types.scenarios import ScorerUpdateResponse, ScorerRetrieveResponse, ScorerValidateResponse + + +class Scorer: + """A custom scorer for evaluating scenario outputs. + + Scorers define bash scripts that produce a score (0.0-1.0) for scenario runs. + Obtain instances via ``runloop.scorer.create()`` or ``runloop.scorer.from_id()``. + + Example: + >>> runloop = RunloopSDK() + >>> scorer = runloop.scorer.create(type="my_scorer", bash_script="echo 'score=1.0'") + >>> scorer.validate(scoring_context={"output": "test"}) + """ + + def __init__(self, client: Runloop, scorer_id: str) -> None: + """Create a Scorer instance. + + :param client: Runloop client instance + :type client: Runloop + :param scorer_id: ID of the scorer + :type scorer_id: str + """ + self._client = client + self._id = scorer_id + + @override + def __repr__(self) -> str: + return f"" + + @property + def id(self) -> str: + """The scorer's unique identifier. + + :return: Scorer ID + :rtype: str + """ + return self._id + + def get_info(self, **options: Unpack[BaseRequestOptions]) -> ScorerRetrieveResponse: + """Fetch current scorer details from the API. + + :param options: See :typeddict:`~runloop_api_client.sdk._types.BaseRequestOptions` for available options + :return: Current scorer details + :rtype: ScorerRetrieveResponse + """ + return self._client.scenarios.scorers.retrieve(self._id, **options) + + def update(self, **params: Unpack[SDKScorerUpdateParams]) -> ScorerUpdateResponse: + """Update the scorer's type or bash script. + + :param params: See :typeddict:`~runloop_api_client.sdk._types.SDKScorerUpdateParams` for available parameters + :return: Updated scorer details + :rtype: ScorerUpdateResponse + """ + return self._client.scenarios.scorers.update(self._id, **params) + + def validate(self, **params: Unpack[SDKScorerValidateParams]) -> ScorerValidateResponse: + """Run the scorer against the provided context and return the result. + + :param params: See :typeddict:`~runloop_api_client.sdk._types.SDKScorerValidateParams` for available parameters + :return: Validation result with score + :rtype: ScorerValidateResponse + """ + return self._client.scenarios.scorers.validate(self._id, **params) diff --git a/src/runloop_api_client/sdk/sync.py b/src/runloop_api_client/sdk/sync.py index 0bd7f68e7..99410c2d0 100644 --- a/src/runloop_api_client/sdk/sync.py +++ b/src/runloop_api_client/sdk/sync.py @@ -15,14 +15,17 @@ LongRequestOptions, SDKDevboxListParams, SDKObjectListParams, + SDKScorerListParams, SDKDevboxCreateParams, SDKObjectCreateParams, + SDKScorerCreateParams, SDKBlueprintListParams, SDKBlueprintCreateParams, SDKDiskSnapshotListParams, SDKDevboxCreateFromImageParams, ) from .devbox import Devbox +from .scorer import Scorer from .._types import Timeout, NotGiven, not_given from .._client import DEFAULT_MAX_RETRIES, Runloop from ._helpers import detect_content_type @@ -487,6 +490,54 @@ def upload_from_bytes( return obj +class ScorerOps: + """Create and manage custom scorers. Access via ``runloop.scorer``. + + Example: + >>> runloop = RunloopSDK() + >>> scorer = runloop.scorer.create(type="my_scorer", bash_script="echo 'score=1.0'") + >>> all_scorers = runloop.scorer.list() + """ + + def __init__(self, client: Runloop) -> None: + """Initialize ScorerOps. + + :param client: Runloop client instance + :type client: Runloop + """ + self._client = client + + def create(self, **params: Unpack[SDKScorerCreateParams]) -> Scorer: + """Create a new scorer with the given type and bash script. + + :param params: See :typeddict:`~runloop_api_client.sdk._types.SDKScorerCreateParams` for available parameters + :return: The newly created scorer + :rtype: Scorer + """ + response = self._client.scenarios.scorers.create(**params) + return Scorer(self._client, response.id) + + def from_id(self, scorer_id: str) -> Scorer: + """Get a Scorer instance for an existing scorer ID. + + :param scorer_id: ID of the scorer + :type scorer_id: str + :return: Scorer instance for the given ID + :rtype: Scorer + """ + return Scorer(self._client, scorer_id) + + def list(self, **params: Unpack[SDKScorerListParams]) -> list[Scorer]: + """List all scorers, optionally filtered by parameters. + + :param params: See :typeddict:`~runloop_api_client.sdk._types.SDKScorerListParams` for available parameters + :return: List of scorers + :rtype: list[Scorer] + """ + page = self._client.scenarios.scorers.list(**params) + return [Scorer(self._client, item.id) for item in page] + + class RunloopSDK: """High-level synchronous entry point for the Runloop SDK. @@ -500,6 +551,8 @@ class RunloopSDK: :vartype devbox: DevboxOps :ivar blueprint: High-level interface for blueprint management :vartype blueprint: BlueprintOps + :ivar scorer: High-level interface for scorer management + :vartype scorer: ScorerOps :ivar snapshot: High-level interface for snapshot management :vartype snapshot: SnapshotOps :ivar storage_object: High-level interface for storage object management @@ -516,6 +569,7 @@ class RunloopSDK: api: Runloop devbox: DevboxOps blueprint: BlueprintOps + scorer: ScorerOps snapshot: SnapshotOps storage_object: StorageObjectOps @@ -559,6 +613,7 @@ def __init__( self.devbox = DevboxOps(self.api) self.blueprint = BlueprintOps(self.api) + self.scorer = ScorerOps(self.api) self.snapshot = SnapshotOps(self.api) self.storage_object = StorageObjectOps(self.api) diff --git a/tests/sdk/conftest.py b/tests/sdk/conftest.py index 436c4de53..b61a93301 100644 --- a/tests/sdk/conftest.py +++ b/tests/sdk/conftest.py @@ -20,6 +20,7 @@ "snapshot": "snap_123", "blueprint": "bp_123", "object": "obj_123", + "scorer": "scorer_123", } # Test URL constants @@ -86,6 +87,15 @@ class MockObjectView: name: str = "test-object" +@dataclass +class MockScorerView: + """Mock ScorerView for testing.""" + + id: str = "scorer_123" + bash_script: str = "echo 'score=1.0'" + type: str = "test_scorer" + + def create_mock_httpx_client(methods: dict[str, Any] | None = None) -> AsyncMock: """ Create a mock httpx.AsyncClient with proper context manager setup. @@ -170,6 +180,12 @@ def object_view() -> MockObjectView: return MockObjectView() +@pytest.fixture +def scorer_view() -> MockScorerView: + """Create a mock ScorerView.""" + return MockScorerView() + + @pytest.fixture def mock_httpx_response() -> Mock: """Create a mock httpx.Response.""" diff --git a/tests/sdk/test_async_blueprint.py b/tests/sdk/test_async_blueprint.py index 8f638c18f..75901a445 100644 --- a/tests/sdk/test_async_blueprint.py +++ b/tests/sdk/test_async_blueprint.py @@ -38,7 +38,7 @@ async def test_get_info(self, mock_async_client: AsyncMock, blueprint_view: Mock ) assert result == blueprint_view - mock_async_client.blueprints.retrieve.assert_called_once() + mock_async_client.blueprints.retrieve.assert_awaited_once() @pytest.mark.asyncio async def test_logs(self, mock_async_client: AsyncMock) -> None: @@ -55,7 +55,7 @@ async def test_logs(self, mock_async_client: AsyncMock) -> None: ) assert result == logs_view - mock_async_client.blueprints.logs.assert_called_once() + mock_async_client.blueprints.logs.assert_awaited_once() @pytest.mark.asyncio async def test_delete(self, mock_async_client: AsyncMock) -> None: @@ -71,7 +71,7 @@ async def test_delete(self, mock_async_client: AsyncMock) -> None: ) assert result is not None # Verify return value is propagated - mock_async_client.blueprints.delete.assert_called_once() + mock_async_client.blueprints.delete.assert_awaited_once() @pytest.mark.asyncio async def test_create_devbox(self, mock_async_client: AsyncMock, devbox_view: MockDevboxView) -> None: @@ -87,4 +87,4 @@ async def test_create_devbox(self, mock_async_client: AsyncMock, devbox_view: Mo ) assert devbox.id == "dev_123" - mock_async_client.devboxes.create_and_await_running.assert_called_once() + mock_async_client.devboxes.create_and_await_running.assert_awaited_once() diff --git a/tests/sdk/test_async_execution_result.py b/tests/sdk/test_async_execution_result.py index a4df5bac9..2a71da1c7 100644 --- a/tests/sdk/test_async_execution_result.py +++ b/tests/sdk/test_async_execution_result.py @@ -190,7 +190,7 @@ async def mock_iter(): # Should stream full output output = await result.stdout() assert output == "line1\nline2\nline3\n" - mock_async_client.devboxes.executions.stream_stdout_updates.assert_called_once_with( + mock_async_client.devboxes.executions.stream_stdout_updates.assert_awaited_once_with( "exec_123", devbox_id="dev_123" ) @@ -226,7 +226,7 @@ async def mock_iter(): # Should stream full output output = await result.stderr() assert output == "error1\nerror2\n" - mock_async_client.devboxes.executions.stream_stderr_updates.assert_called_once_with( + mock_async_client.devboxes.executions.stream_stderr_updates.assert_awaited_once_with( "exec_123", devbox_id="dev_123" ) diff --git a/tests/sdk/test_async_clients.py b/tests/sdk/test_async_ops.py similarity index 59% rename from tests/sdk/test_async_clients.py rename to tests/sdk/test_async_ops.py index ebc8aba4a..340d86647 100644 --- a/tests/sdk/test_async_clients.py +++ b/tests/sdk/test_async_ops.py @@ -13,13 +13,15 @@ from tests.sdk.conftest import ( MockDevboxView, MockObjectView, + MockScorerView, MockSnapshotView, MockBlueprintView, create_mock_httpx_response, ) -from runloop_api_client.sdk import AsyncDevbox, AsyncSnapshot, AsyncBlueprint, AsyncStorageObject +from runloop_api_client.sdk import AsyncDevbox, AsyncScorer, AsyncSnapshot, AsyncBlueprint, AsyncStorageObject from runloop_api_client.sdk.async_ import ( AsyncDevboxOps, + AsyncScorerOps, AsyncRunloopSDK, AsyncSnapshotOps, AsyncBlueprintOps, @@ -28,16 +30,16 @@ from runloop_api_client.lib.polling import PollingConfig -class TestAsyncDevboxClient: - """Tests for AsyncDevboxClient class.""" +class TestAsyncDevboxOps: + """Tests for AsyncDevboxOps class.""" @pytest.mark.asyncio async def test_create(self, mock_async_client: AsyncMock, devbox_view: MockDevboxView) -> None: """Test create method.""" mock_async_client.devboxes.create_and_await_running = AsyncMock(return_value=devbox_view) - client = AsyncDevboxOps(mock_async_client) - devbox = await client.create( + ops = AsyncDevboxOps(mock_async_client) + devbox = await ops.create( name="test-devbox", metadata={"key": "value"}, polling_config=PollingConfig(timeout_seconds=60.0), @@ -45,15 +47,15 @@ async def test_create(self, mock_async_client: AsyncMock, devbox_view: MockDevbo assert isinstance(devbox, AsyncDevbox) assert devbox.id == "dev_123" - mock_async_client.devboxes.create_and_await_running.assert_called_once() + mock_async_client.devboxes.create_and_await_running.assert_awaited_once() @pytest.mark.asyncio async def test_create_from_blueprint_id(self, mock_async_client: AsyncMock, devbox_view: MockDevboxView) -> None: """Test create_from_blueprint_id method.""" mock_async_client.devboxes.create_and_await_running = AsyncMock(return_value=devbox_view) - client = AsyncDevboxOps(mock_async_client) - devbox = await client.create_from_blueprint_id( + ops = AsyncDevboxOps(mock_async_client) + devbox = await ops.create_from_blueprint_id( "bp_123", name="test-devbox", ) @@ -67,8 +69,8 @@ async def test_create_from_blueprint_name(self, mock_async_client: AsyncMock, de """Test create_from_blueprint_name method.""" mock_async_client.devboxes.create_and_await_running = AsyncMock(return_value=devbox_view) - client = AsyncDevboxOps(mock_async_client) - devbox = await client.create_from_blueprint_name( + ops = AsyncDevboxOps(mock_async_client) + devbox = await ops.create_from_blueprint_name( "my-blueprint", name="test-devbox", ) @@ -82,8 +84,8 @@ async def test_create_from_snapshot(self, mock_async_client: AsyncMock, devbox_v """Test create_from_snapshot method.""" mock_async_client.devboxes.create_and_await_running = AsyncMock(return_value=devbox_view) - client = AsyncDevboxOps(mock_async_client) - devbox = await client.create_from_snapshot( + ops = AsyncDevboxOps(mock_async_client) + devbox = await ops.create_from_snapshot( "snap_123", name="test-devbox", ) @@ -94,8 +96,8 @@ async def test_create_from_snapshot(self, mock_async_client: AsyncMock, devbox_v def test_from_id(self, mock_async_client: AsyncMock) -> None: """Test from_id method.""" - client = AsyncDevboxOps(mock_async_client) - devbox = client.from_id("dev_123") + ops = AsyncDevboxOps(mock_async_client) + devbox = ops.from_id("dev_123") assert isinstance(devbox, AsyncDevbox) assert devbox.id == "dev_123" @@ -104,13 +106,25 @@ def test_from_id(self, mock_async_client: AsyncMock) -> None: assert not mock_async_client.devboxes.await_running.called @pytest.mark.asyncio - async def test_list(self, mock_async_client: AsyncMock, devbox_view: MockDevboxView) -> None: - """Test list method.""" + async def test_list_empty(self, mock_async_client: AsyncMock) -> None: + """Test list method with empty results.""" + page = SimpleNamespace(devboxes=[]) + mock_async_client.devboxes.list = AsyncMock(return_value=page) + + ops = AsyncDevboxOps(mock_async_client) + devboxes = await ops.list(limit=10, status="running") + + assert len(devboxes) == 0 + mock_async_client.devboxes.list.assert_awaited_once() + + @pytest.mark.asyncio + async def test_list_single(self, mock_async_client: AsyncMock, devbox_view: MockDevboxView) -> None: + """Test list method with single result.""" page = SimpleNamespace(devboxes=[devbox_view]) mock_async_client.devboxes.list = AsyncMock(return_value=page) - client = AsyncDevboxOps(mock_async_client) - devboxes = await client.list( + ops = AsyncDevboxOps(mock_async_client) + devboxes = await ops.list( limit=10, status="running", starting_after="dev_000", @@ -119,20 +133,50 @@ async def test_list(self, mock_async_client: AsyncMock, devbox_view: MockDevboxV assert len(devboxes) == 1 assert isinstance(devboxes[0], AsyncDevbox) assert devboxes[0].id == "dev_123" - mock_async_client.devboxes.list.assert_called_once() + mock_async_client.devboxes.list.assert_awaited_once() + @pytest.mark.asyncio + async def test_list_multiple(self, mock_async_client: AsyncMock) -> None: + """Test list method with multiple results.""" + devbox_view1 = MockDevboxView(id="dev_001", name="devbox-1") + devbox_view2 = MockDevboxView(id="dev_002", name="devbox-2") + page = SimpleNamespace(devboxes=[devbox_view1, devbox_view2]) + mock_async_client.devboxes.list = AsyncMock(return_value=page) + + ops = AsyncDevboxOps(mock_async_client) + devboxes = await ops.list(limit=10, status="running") + + assert len(devboxes) == 2 + assert isinstance(devboxes[0], AsyncDevbox) + assert isinstance(devboxes[1], AsyncDevbox) + assert devboxes[0].id == "dev_001" + assert devboxes[1].id == "dev_002" + mock_async_client.devboxes.list.assert_awaited_once() -class TestAsyncSnapshotClient: - """Tests for AsyncSnapshotClient class.""" + +class TestAsyncSnapshotOps: + """Tests for AsyncSnapshotOps class.""" @pytest.mark.asyncio - async def test_list(self, mock_async_client: AsyncMock, snapshot_view: MockSnapshotView) -> None: - """Test list method.""" + async def test_list_empty(self, mock_async_client: AsyncMock) -> None: + """Test list method with empty results.""" + page = SimpleNamespace(snapshots=[]) + mock_async_client.devboxes.disk_snapshots.list = AsyncMock(return_value=page) + + ops = AsyncSnapshotOps(mock_async_client) + snapshots = await ops.list(devbox_id="dev_123", limit=10) + + assert len(snapshots) == 0 + mock_async_client.devboxes.disk_snapshots.list.assert_awaited_once() + + @pytest.mark.asyncio + async def test_list_single(self, mock_async_client: AsyncMock, snapshot_view: MockSnapshotView) -> None: + """Test list method with single result.""" page = SimpleNamespace(snapshots=[snapshot_view]) mock_async_client.devboxes.disk_snapshots.list = AsyncMock(return_value=page) - client = AsyncSnapshotOps(mock_async_client) - snapshots = await client.list( + ops = AsyncSnapshotOps(mock_async_client) + snapshots = await ops.list( devbox_id="dev_123", limit=10, starting_after="snap_000", @@ -141,51 +185,81 @@ async def test_list(self, mock_async_client: AsyncMock, snapshot_view: MockSnaps assert len(snapshots) == 1 assert isinstance(snapshots[0], AsyncSnapshot) assert snapshots[0].id == "snap_123" - mock_async_client.devboxes.disk_snapshots.list.assert_called_once() + mock_async_client.devboxes.disk_snapshots.list.assert_awaited_once() + + @pytest.mark.asyncio + async def test_list_multiple(self, mock_async_client: AsyncMock) -> None: + """Test list method with multiple results.""" + snapshot_view1 = MockSnapshotView(id="snap_001", name="snapshot-1") + snapshot_view2 = MockSnapshotView(id="snap_002", name="snapshot-2") + page = SimpleNamespace(snapshots=[snapshot_view1, snapshot_view2]) + mock_async_client.devboxes.disk_snapshots.list = AsyncMock(return_value=page) + + ops = AsyncSnapshotOps(mock_async_client) + snapshots = await ops.list(devbox_id="dev_123", limit=10) + + assert len(snapshots) == 2 + assert isinstance(snapshots[0], AsyncSnapshot) + assert isinstance(snapshots[1], AsyncSnapshot) + assert snapshots[0].id == "snap_001" + assert snapshots[1].id == "snap_002" + mock_async_client.devboxes.disk_snapshots.list.assert_awaited_once() def test_from_id(self, mock_async_client: AsyncMock) -> None: """Test from_id method.""" - client = AsyncSnapshotOps(mock_async_client) - snapshot = client.from_id("snap_123") + ops = AsyncSnapshotOps(mock_async_client) + snapshot = ops.from_id("snap_123") assert isinstance(snapshot, AsyncSnapshot) assert snapshot.id == "snap_123" -class TestAsyncBlueprintClient: - """Tests for AsyncBlueprintClient class.""" +class TestAsyncBlueprintOps: + """Tests for AsyncBlueprintOps class.""" @pytest.mark.asyncio async def test_create(self, mock_async_client: AsyncMock, blueprint_view: MockBlueprintView) -> None: """Test create method.""" mock_async_client.blueprints.create_and_await_build_complete = AsyncMock(return_value=blueprint_view) - client = AsyncBlueprintOps(mock_async_client) - blueprint = await client.create( + ops = AsyncBlueprintOps(mock_async_client) + blueprint = await ops.create( name="test-blueprint", polling_config=PollingConfig(timeout_seconds=60.0), ) assert isinstance(blueprint, AsyncBlueprint) assert blueprint.id == "bp_123" - mock_async_client.blueprints.create_and_await_build_complete.assert_called_once() + mock_async_client.blueprints.create_and_await_build_complete.assert_awaited_once() def test_from_id(self, mock_async_client: AsyncMock) -> None: """Test from_id method.""" - client = AsyncBlueprintOps(mock_async_client) - blueprint = client.from_id("bp_123") + ops = AsyncBlueprintOps(mock_async_client) + blueprint = ops.from_id("bp_123") assert isinstance(blueprint, AsyncBlueprint) assert blueprint.id == "bp_123" @pytest.mark.asyncio - async def test_list(self, mock_async_client: AsyncMock, blueprint_view: MockBlueprintView) -> None: - """Test list method.""" + async def test_list_empty(self, mock_async_client: AsyncMock) -> None: + """Test list method with empty results.""" + page = SimpleNamespace(blueprints=[]) + mock_async_client.blueprints.list = AsyncMock(return_value=page) + + ops = AsyncBlueprintOps(mock_async_client) + blueprints = await ops.list(limit=10) + + assert len(blueprints) == 0 + mock_async_client.blueprints.list.assert_awaited_once() + + @pytest.mark.asyncio + async def test_list_single(self, mock_async_client: AsyncMock, blueprint_view: MockBlueprintView) -> None: + """Test list method with single result.""" page = SimpleNamespace(blueprints=[blueprint_view]) mock_async_client.blueprints.list = AsyncMock(return_value=page) - client = AsyncBlueprintOps(mock_async_client) - blueprints = await client.list( + ops = AsyncBlueprintOps(mock_async_client) + blueprints = await ops.list( limit=10, name="test", starting_after="bp_000", @@ -194,19 +268,37 @@ async def test_list(self, mock_async_client: AsyncMock, blueprint_view: MockBlue assert len(blueprints) == 1 assert isinstance(blueprints[0], AsyncBlueprint) assert blueprints[0].id == "bp_123" - mock_async_client.blueprints.list.assert_called_once() + mock_async_client.blueprints.list.assert_awaited_once() + @pytest.mark.asyncio + async def test_list_multiple(self, mock_async_client: AsyncMock) -> None: + """Test list method with multiple results.""" + blueprint_view1 = MockBlueprintView(id="bp_001", name="blueprint-1") + blueprint_view2 = MockBlueprintView(id="bp_002", name="blueprint-2") + page = SimpleNamespace(blueprints=[blueprint_view1, blueprint_view2]) + mock_async_client.blueprints.list = AsyncMock(return_value=page) -class TestAsyncStorageObjectClient: - """Tests for AsyncStorageObjectClient class.""" + ops = AsyncBlueprintOps(mock_async_client) + blueprints = await ops.list(limit=10) + + assert len(blueprints) == 2 + assert isinstance(blueprints[0], AsyncBlueprint) + assert isinstance(blueprints[1], AsyncBlueprint) + assert blueprints[0].id == "bp_001" + assert blueprints[1].id == "bp_002" + mock_async_client.blueprints.list.assert_awaited_once() + + +class TestAsyncStorageObjectOps: + """Tests for AsyncStorageObjectOps class.""" @pytest.mark.asyncio async def test_create(self, mock_async_client: AsyncMock, object_view: MockObjectView) -> None: """Test create method.""" mock_async_client.objects.create = AsyncMock(return_value=object_view) - client = AsyncStorageObjectOps(mock_async_client) - obj = await client.create(name="test.txt", content_type="text", metadata={"key": "value"}) + ops = AsyncStorageObjectOps(mock_async_client) + obj = await ops.create(name="test.txt", content_type="text", metadata={"key": "value"}) assert isinstance(obj, AsyncStorageObject) assert obj.id == "obj_123" @@ -219,21 +311,33 @@ async def test_create(self, mock_async_client: AsyncMock, object_view: MockObjec def test_from_id(self, mock_async_client: AsyncMock) -> None: """Test from_id method.""" - client = AsyncStorageObjectOps(mock_async_client) - obj = client.from_id("obj_123") + ops = AsyncStorageObjectOps(mock_async_client) + obj = ops.from_id("obj_123") assert isinstance(obj, AsyncStorageObject) assert obj.id == "obj_123" assert obj.upload_url is None @pytest.mark.asyncio - async def test_list(self, mock_async_client: AsyncMock, object_view: MockObjectView) -> None: - """Test list method.""" + async def test_list_empty(self, mock_async_client: AsyncMock) -> None: + """Test list method with empty results.""" + page = SimpleNamespace(objects=[]) + mock_async_client.objects.list = AsyncMock(return_value=page) + + ops = AsyncStorageObjectOps(mock_async_client) + objects = await ops.list(limit=10) + + assert len(objects) == 0 + mock_async_client.objects.list.assert_awaited_once() + + @pytest.mark.asyncio + async def test_list_single(self, mock_async_client: AsyncMock, object_view: MockObjectView) -> None: + """Test list method with single result.""" page = SimpleNamespace(objects=[object_view]) mock_async_client.objects.list = AsyncMock(return_value=page) - client = AsyncStorageObjectOps(mock_async_client) - objects = await client.list( + ops = AsyncStorageObjectOps(mock_async_client) + objects = await ops.list( content_type="text", limit=10, name="test", @@ -254,6 +358,24 @@ async def test_list(self, mock_async_client: AsyncMock, object_view: MockObjectV state="READ_ONLY", ) + @pytest.mark.asyncio + async def test_list_multiple(self, mock_async_client: AsyncMock) -> None: + """Test list method with multiple results.""" + object_view1 = MockObjectView(id="obj_001", name="object-1") + object_view2 = MockObjectView(id="obj_002", name="object-2") + page = SimpleNamespace(objects=[object_view1, object_view2]) + mock_async_client.objects.list = AsyncMock(return_value=page) + + ops = AsyncStorageObjectOps(mock_async_client) + objects = await ops.list(limit=10) + + assert len(objects) == 2 + assert isinstance(objects[0], AsyncStorageObject) + assert isinstance(objects[1], AsyncStorageObject) + assert objects[0].id == "obj_001" + assert objects[1].id == "obj_002" + mock_async_client.objects.list.assert_awaited_once() + @pytest.mark.asyncio async def test_upload_from_file( self, mock_async_client: AsyncMock, object_view: MockObjectView, tmp_path: Path @@ -270,8 +392,8 @@ async def test_upload_from_file( http_client.put = AsyncMock(return_value=mock_response) mock_async_client._client = http_client - client = AsyncStorageObjectOps(mock_async_client) - obj = await client.upload_from_file(temp_file, name="test.txt") + ops = AsyncStorageObjectOps(mock_async_client) + obj = await ops.upload_from_file(temp_file, name="test.txt") assert isinstance(obj, AsyncStorageObject) assert obj.id == "obj_123" @@ -295,8 +417,8 @@ async def test_upload_from_text(self, mock_async_client: AsyncMock, object_view: http_client.put = AsyncMock(return_value=mock_response) mock_async_client._client = http_client - client = AsyncStorageObjectOps(mock_async_client) - obj = await client.upload_from_text("test content", name="test.txt", metadata={"key": "value"}) + ops = AsyncStorageObjectOps(mock_async_client) + obj = await ops.upload_from_text("test content", name="test.txt", metadata={"key": "value"}) assert isinstance(obj, AsyncStorageObject) assert obj.id == "obj_123" @@ -320,8 +442,8 @@ async def test_upload_from_bytes(self, mock_async_client: AsyncMock, object_view http_client.put = AsyncMock(return_value=mock_response) mock_async_client._client = http_client - client = AsyncStorageObjectOps(mock_async_client) - obj = await client.upload_from_bytes(b"test content", name="test.bin", content_type="binary") + ops = AsyncStorageObjectOps(mock_async_client) + obj = await ops.upload_from_bytes(b"test content", name="test.bin", content_type="binary") assert isinstance(obj, AsyncStorageObject) assert obj.id == "obj_123" @@ -337,11 +459,11 @@ async def test_upload_from_bytes(self, mock_async_client: AsyncMock, object_view @pytest.mark.asyncio async def test_upload_from_file_missing_path(self, mock_async_client: AsyncMock, tmp_path: Path) -> None: """upload_from_file should raise when file cannot be read.""" - client = AsyncStorageObjectOps(mock_async_client) + ops = AsyncStorageObjectOps(mock_async_client) missing_file = tmp_path / "missing.txt" with pytest.raises(OSError, match="Failed to read file"): - await client.upload_from_file(missing_file) + await ops.upload_from_file(missing_file) @pytest.mark.asyncio async def test_upload_from_dir( @@ -365,8 +487,8 @@ async def test_upload_from_dir( http_client.put = AsyncMock(return_value=mock_response) mock_async_client._client = http_client - client = AsyncStorageObjectOps(mock_async_client) - obj = await client.upload_from_dir(test_dir, name="archive.tar.gz", metadata={"key": "value"}) + ops = AsyncStorageObjectOps(mock_async_client) + obj = await ops.upload_from_dir(test_dir, name="archive.tar.gz", metadata={"key": "value"}) assert isinstance(obj, AsyncStorageObject) assert obj.id == "obj_123" @@ -410,8 +532,8 @@ async def test_upload_from_dir_default_name( http_client.put = AsyncMock(return_value=mock_response) mock_async_client._client = http_client - client = AsyncStorageObjectOps(mock_async_client) - obj = await client.upload_from_dir(test_dir) + ops = AsyncStorageObjectOps(mock_async_client) + obj = await ops.upload_from_dir(test_dir) assert isinstance(obj, AsyncStorageObject) # Name should be directory name + .tar.gz @@ -441,8 +563,8 @@ async def test_upload_from_dir_with_ttl( http_client.put = AsyncMock(return_value=mock_response) mock_async_client._client = http_client - client = AsyncStorageObjectOps(mock_async_client) - obj = await client.upload_from_dir(test_dir, ttl=timedelta(hours=2)) + ops = AsyncStorageObjectOps(mock_async_client) + obj = await ops.upload_from_dir(test_dir, ttl=timedelta(hours=2)) assert isinstance(obj, AsyncStorageObject) mock_async_client.objects.create.assert_awaited_once_with( @@ -468,8 +590,8 @@ async def test_upload_from_dir_empty_directory( http_client.put = AsyncMock(return_value=mock_response) mock_async_client._client = http_client - client = AsyncStorageObjectOps(mock_async_client) - obj = await client.upload_from_dir(test_dir) + ops = AsyncStorageObjectOps(mock_async_client) + obj = await ops.upload_from_dir(test_dir) assert isinstance(obj, AsyncStorageObject) assert obj.id == "obj_123" @@ -499,9 +621,9 @@ async def test_upload_from_dir_with_string_path( http_client.put = AsyncMock(return_value=mock_response) mock_async_client._client = http_client - client = AsyncStorageObjectOps(mock_async_client) + ops = AsyncStorageObjectOps(mock_async_client) # Pass string path instead of Path object - obj = await client.upload_from_dir(str(test_dir)) + obj = await ops.upload_from_dir(str(test_dir)) assert isinstance(obj, AsyncStorageObject) assert obj.id == "obj_123" @@ -515,6 +637,91 @@ async def test_upload_from_dir_with_string_path( mock_async_client.objects.complete.assert_awaited_once() +class TestAsyncScorerOps: + """Tests for AsyncScorerOps class.""" + + @pytest.mark.asyncio + async def test_create(self, mock_async_client: AsyncMock, scorer_view: MockScorerView) -> None: + """Test create method.""" + mock_async_client.scenarios.scorers.create = AsyncMock(return_value=scorer_view) + + ops = AsyncScorerOps(mock_async_client) + scorer = await ops.create( + bash_script="echo 'score=1.0'", + type="test_scorer", + ) + + assert isinstance(scorer, AsyncScorer) + assert scorer.id == "scorer_123" + mock_async_client.scenarios.scorers.create.assert_awaited_once() + + def test_from_id(self, mock_async_client: AsyncMock) -> None: + """Test from_id method.""" + ops = AsyncScorerOps(mock_async_client) + scorer = ops.from_id("scorer_123") + + assert isinstance(scorer, AsyncScorer) + assert scorer.id == "scorer_123" + + @pytest.mark.asyncio + async def test_list_empty(self, mock_async_client: AsyncMock) -> None: + """Test list method with empty results.""" + + async def async_iter(): + return + yield # Make this a generator + + mock_async_client.scenarios.scorers.list = AsyncMock(return_value=async_iter()) + + ops = AsyncScorerOps(mock_async_client) + scorers = await ops.list(limit=10) + + assert len(scorers) == 0 + mock_async_client.scenarios.scorers.list.assert_awaited_once() + + @pytest.mark.asyncio + async def test_list_single(self, mock_async_client: AsyncMock, scorer_view: MockScorerView) -> None: + """Test list method with single result.""" + + async def async_iter(): + yield scorer_view + + mock_async_client.scenarios.scorers.list = AsyncMock(return_value=async_iter()) + + ops = AsyncScorerOps(mock_async_client) + scorers = await ops.list( + limit=10, + starting_after="scorer_000", + ) + + assert len(scorers) == 1 + assert isinstance(scorers[0], AsyncScorer) + assert scorers[0].id == "scorer_123" + mock_async_client.scenarios.scorers.list.assert_awaited_once() + + @pytest.mark.asyncio + async def test_list_multiple(self, mock_async_client: AsyncMock) -> None: + """Test list method with multiple results.""" + scorer_view1 = MockScorerView(id="scorer_001", type="scorer-1") + scorer_view2 = MockScorerView(id="scorer_002", type="scorer-2") + + async def async_iter(): + yield scorer_view1 + yield scorer_view2 + + mock_async_client.scenarios.scorers.list = AsyncMock(return_value=async_iter()) + + ops = AsyncScorerOps(mock_async_client) + scorers = await ops.list(limit=10) + + assert len(scorers) == 2 + assert isinstance(scorers[0], AsyncScorer) + assert isinstance(scorers[1], AsyncScorer) + assert scorers[0].id == "scorer_001" + assert scorers[1].id == "scorer_002" + mock_async_client.scenarios.scorers.list.assert_awaited_once() + + class TestAsyncRunloopSDK: """Tests for AsyncRunloopSDK class.""" @@ -523,6 +730,7 @@ def test_init(self) -> None: sdk = AsyncRunloopSDK(bearer_token="test-token") assert sdk.api is not None assert isinstance(sdk.devbox, AsyncDevboxOps) + assert isinstance(sdk.scorer, AsyncScorerOps) assert isinstance(sdk.snapshot, AsyncSnapshotOps) assert isinstance(sdk.blueprint, AsyncBlueprintOps) assert isinstance(sdk.storage_object, AsyncStorageObjectOps) diff --git a/tests/sdk/test_async_scorer.py b/tests/sdk/test_async_scorer.py new file mode 100644 index 000000000..a3eeea884 --- /dev/null +++ b/tests/sdk/test_async_scorer.py @@ -0,0 +1,69 @@ +"""Comprehensive tests for async AsyncScorer class.""" + +from __future__ import annotations + +from types import SimpleNamespace +from unittest.mock import AsyncMock + +import pytest + +from tests.sdk.conftest import MockScorerView +from runloop_api_client.sdk import AsyncScorer + + +class TestAsyncScorer: + """Tests for AsyncScorer class.""" + + def test_init(self, mock_async_client: AsyncMock) -> None: + """Test AsyncScorer initialization.""" + scorer = AsyncScorer(mock_async_client, "scorer_123") + assert scorer.id == "scorer_123" + + def test_repr(self, mock_async_client: AsyncMock) -> None: + """Test AsyncScorer string representation.""" + scorer = AsyncScorer(mock_async_client, "scorer_123") + assert repr(scorer) == "" + + @pytest.mark.asyncio + async def test_get_info(self, mock_async_client: AsyncMock, scorer_view: MockScorerView) -> None: + """Test get_info method.""" + mock_async_client.scenarios.scorers.retrieve = AsyncMock(return_value=scorer_view) + + scorer = AsyncScorer(mock_async_client, "scorer_123") + result = await scorer.get_info() + + assert result == scorer_view + mock_async_client.scenarios.scorers.retrieve.assert_awaited_once() + + @pytest.mark.asyncio + async def test_update(self, mock_async_client: AsyncMock) -> None: + """Test update method.""" + update_response = SimpleNamespace(id="scorer_123", type="updated_scorer", bash_script="echo 'score=1.0'") + mock_async_client.scenarios.scorers.update = AsyncMock(return_value=update_response) + + scorer = AsyncScorer(mock_async_client, "scorer_123") + result = await scorer.update( + type="updated_scorer", + bash_script="echo 'score=1.0'", + ) + + assert result == update_response + mock_async_client.scenarios.scorers.update.assert_awaited_once() + + @pytest.mark.asyncio + async def test_validate(self, mock_async_client: AsyncMock) -> None: + """Test validate method.""" + validate_response = SimpleNamespace( + name="test_scorer", + scoring_context={}, + scoring_result=SimpleNamespace(score=0.95), + ) + mock_async_client.scenarios.scorers.validate = AsyncMock(return_value=validate_response) + + scorer = AsyncScorer(mock_async_client, "scorer_123") + result = await scorer.validate( + scoring_context={"test": "context"}, + ) + + assert result == validate_response + mock_async_client.scenarios.scorers.validate.assert_awaited_once() diff --git a/tests/sdk/test_async_snapshot.py b/tests/sdk/test_async_snapshot.py index 7bca2ad95..a7b946c11 100644 --- a/tests/sdk/test_async_snapshot.py +++ b/tests/sdk/test_async_snapshot.py @@ -39,7 +39,7 @@ async def test_get_info(self, mock_async_client: AsyncMock, snapshot_view: MockS ) assert result == snapshot_view - mock_async_client.devboxes.disk_snapshots.query_status.assert_called_once() + mock_async_client.devboxes.disk_snapshots.query_status.assert_awaited_once() @pytest.mark.asyncio async def test_update(self, mock_async_client: AsyncMock) -> None: @@ -60,7 +60,7 @@ async def test_update(self, mock_async_client: AsyncMock) -> None: ) assert result == updated_snapshot - mock_async_client.devboxes.disk_snapshots.update.assert_called_once() + mock_async_client.devboxes.disk_snapshots.update.assert_awaited_once() @pytest.mark.asyncio async def test_delete(self, mock_async_client: AsyncMock) -> None: @@ -77,7 +77,7 @@ async def test_delete(self, mock_async_client: AsyncMock) -> None: ) assert result is not None # Verify return value is propagated - mock_async_client.devboxes.disk_snapshots.delete.assert_called_once() + mock_async_client.devboxes.disk_snapshots.delete.assert_awaited_once() @pytest.mark.asyncio async def test_await_completed(self, mock_async_client: AsyncMock, snapshot_view: MockSnapshotView) -> None: @@ -95,7 +95,7 @@ async def test_await_completed(self, mock_async_client: AsyncMock, snapshot_view ) assert result == snapshot_view - mock_async_client.devboxes.disk_snapshots.await_completed.assert_called_once() + mock_async_client.devboxes.disk_snapshots.await_completed.assert_awaited_once() @pytest.mark.asyncio async def test_create_devbox(self, mock_async_client: AsyncMock, devbox_view: MockDevboxView) -> None: @@ -111,4 +111,4 @@ async def test_create_devbox(self, mock_async_client: AsyncMock, devbox_view: Mo ) assert devbox.id == "dev_123" - mock_async_client.devboxes.create_and_await_running.assert_called_once() + mock_async_client.devboxes.create_and_await_running.assert_awaited_once() diff --git a/tests/sdk/test_async_storage_object.py b/tests/sdk/test_async_storage_object.py index b4623a95a..434be5221 100644 --- a/tests/sdk/test_async_storage_object.py +++ b/tests/sdk/test_async_storage_object.py @@ -45,7 +45,7 @@ async def test_refresh(self, mock_async_client: AsyncMock, object_view: MockObje ) assert result == object_view - mock_async_client.objects.retrieve.assert_called_once() + mock_async_client.objects.retrieve.assert_awaited_once() @pytest.mark.asyncio async def test_complete(self, mock_async_client: AsyncMock) -> None: @@ -66,7 +66,7 @@ async def test_complete(self, mock_async_client: AsyncMock) -> None: assert result == completed_view assert obj.upload_url is None - mock_async_client.objects.complete.assert_called_once() + mock_async_client.objects.complete.assert_awaited_once() @pytest.mark.asyncio async def test_get_download_url_without_duration(self, mock_async_client: AsyncMock) -> None: @@ -83,7 +83,7 @@ async def test_get_download_url_without_duration(self, mock_async_client: AsyncM ) assert result == download_url_view - mock_async_client.objects.download.assert_called_once() + mock_async_client.objects.download.assert_awaited_once() @pytest.mark.asyncio async def test_get_download_url_with_duration(self, mock_async_client: AsyncMock) -> None: @@ -101,7 +101,7 @@ async def test_get_download_url_with_duration(self, mock_async_client: AsyncMock ) assert result == download_url_view - mock_async_client.objects.download.assert_called_once() + mock_async_client.objects.download.assert_awaited_once() @pytest.mark.asyncio async def test_download_as_bytes(self, mock_async_client: AsyncMock) -> None: @@ -160,7 +160,7 @@ async def test_delete(self, mock_async_client: AsyncMock, object_view: MockObjec ) assert result == object_view - mock_async_client.objects.delete.assert_called_once() + mock_async_client.objects.delete.assert_awaited_once() @pytest.mark.asyncio async def test_upload_content_string(self, mock_async_client: AsyncMock) -> None: diff --git a/tests/sdk/test_clients.py b/tests/sdk/test_ops.py similarity index 60% rename from tests/sdk/test_clients.py rename to tests/sdk/test_ops.py index 18e1342e4..83c9117f4 100644 --- a/tests/sdk/test_clients.py +++ b/tests/sdk/test_ops.py @@ -11,13 +11,15 @@ from tests.sdk.conftest import ( MockDevboxView, MockObjectView, + MockScorerView, MockSnapshotView, MockBlueprintView, create_mock_httpx_response, ) -from runloop_api_client.sdk import Devbox, Snapshot, Blueprint, StorageObject +from runloop_api_client.sdk import Devbox, Scorer, Snapshot, Blueprint, StorageObject from runloop_api_client.sdk.sync import ( DevboxOps, + ScorerOps, RunloopSDK, SnapshotOps, BlueprintOps, @@ -26,15 +28,15 @@ from runloop_api_client.lib.polling import PollingConfig -class TestDevboxClient: - """Tests for DevboxClient class.""" +class TestDevboxOps: + """Tests for DevboxOps class.""" def test_create(self, mock_client: Mock, devbox_view: MockDevboxView) -> None: """Test create method.""" mock_client.devboxes.create_and_await_running.return_value = devbox_view - client = DevboxOps(mock_client) - devbox = client.create( + ops = DevboxOps(mock_client) + devbox = ops.create( name="test-devbox", metadata={"key": "value"}, polling_config=PollingConfig(timeout_seconds=60.0), @@ -48,8 +50,8 @@ def test_create_from_blueprint_id(self, mock_client: Mock, devbox_view: MockDevb """Test create_from_blueprint_id method.""" mock_client.devboxes.create_and_await_running.return_value = devbox_view - client = DevboxOps(mock_client) - devbox = client.create_from_blueprint_id( + ops = DevboxOps(mock_client) + devbox = ops.create_from_blueprint_id( "bp_123", name="test-devbox", metadata={"key": "value"}, @@ -64,8 +66,8 @@ def test_create_from_blueprint_name(self, mock_client: Mock, devbox_view: MockDe """Test create_from_blueprint_name method.""" mock_client.devboxes.create_and_await_running.return_value = devbox_view - client = DevboxOps(mock_client) - devbox = client.create_from_blueprint_name( + ops = DevboxOps(mock_client) + devbox = ops.create_from_blueprint_name( "my-blueprint", name="test-devbox", ) @@ -78,8 +80,8 @@ def test_create_from_snapshot(self, mock_client: Mock, devbox_view: MockDevboxVi """Test create_from_snapshot method.""" mock_client.devboxes.create_and_await_running.return_value = devbox_view - client = DevboxOps(mock_client) - devbox = client.create_from_snapshot( + ops = DevboxOps(mock_client) + devbox = ops.create_from_snapshot( "snap_123", name="test-devbox", ) @@ -92,20 +94,31 @@ def test_from_id(self, mock_client: Mock, devbox_view: MockDevboxView) -> None: """Test from_id method waits for running.""" mock_client.devboxes.await_running.return_value = devbox_view - client = DevboxOps(mock_client) - devbox = client.from_id("dev_123") + ops = DevboxOps(mock_client) + devbox = ops.from_id("dev_123") assert isinstance(devbox, Devbox) assert devbox.id == "dev_123" mock_client.devboxes.await_running.assert_called_once_with("dev_123") - def test_list(self, mock_client: Mock, devbox_view: MockDevboxView) -> None: - """Test list method.""" + def test_list_empty(self, mock_client: Mock) -> None: + """Test list method with empty results.""" + page = SimpleNamespace(devboxes=[]) + mock_client.devboxes.list.return_value = page + + ops = DevboxOps(mock_client) + devboxes = ops.list(limit=10, status="running") + + assert len(devboxes) == 0 + mock_client.devboxes.list.assert_called_once() + + def test_list_single(self, mock_client: Mock, devbox_view: MockDevboxView) -> None: + """Test list method with single result.""" page = SimpleNamespace(devboxes=[devbox_view]) mock_client.devboxes.list.return_value = page - client = DevboxOps(mock_client) - devboxes = client.list( + ops = DevboxOps(mock_client) + devboxes = ops.list( limit=10, status="running", starting_after="dev_000", @@ -116,17 +129,45 @@ def test_list(self, mock_client: Mock, devbox_view: MockDevboxView) -> None: assert devboxes[0].id == "dev_123" mock_client.devboxes.list.assert_called_once() + def test_list_multiple(self, mock_client: Mock) -> None: + """Test list method with multiple results.""" + devbox_view1 = MockDevboxView(id="dev_001", name="devbox-1") + devbox_view2 = MockDevboxView(id="dev_002", name="devbox-2") + page = SimpleNamespace(devboxes=[devbox_view1, devbox_view2]) + mock_client.devboxes.list.return_value = page + + ops = DevboxOps(mock_client) + devboxes = ops.list(limit=10, status="running") + + assert len(devboxes) == 2 + assert isinstance(devboxes[0], Devbox) + assert isinstance(devboxes[1], Devbox) + assert devboxes[0].id == "dev_001" + assert devboxes[1].id == "dev_002" + mock_client.devboxes.list.assert_called_once() + + +class TestSnapshotOps: + """Tests for SnapshotOps class.""" -class TestSnapshotClient: - """Tests for SnapshotClient class.""" + def test_list_empty(self, mock_client: Mock) -> None: + """Test list method with empty results.""" + page = SimpleNamespace(snapshots=[]) + mock_client.devboxes.disk_snapshots.list.return_value = page + + ops = SnapshotOps(mock_client) + snapshots = ops.list(devbox_id="dev_123", limit=10) + + assert len(snapshots) == 0 + mock_client.devboxes.disk_snapshots.list.assert_called_once() - def test_list(self, mock_client: Mock, snapshot_view: MockSnapshotView) -> None: - """Test list method.""" + def test_list_single(self, mock_client: Mock, snapshot_view: MockSnapshotView) -> None: + """Test list method with single result.""" page = SimpleNamespace(snapshots=[snapshot_view]) mock_client.devboxes.disk_snapshots.list.return_value = page - client = SnapshotOps(mock_client) - snapshots = client.list( + ops = SnapshotOps(mock_client) + snapshots = ops.list( devbox_id="dev_123", limit=10, starting_after="snap_000", @@ -137,24 +178,41 @@ def test_list(self, mock_client: Mock, snapshot_view: MockSnapshotView) -> None: assert snapshots[0].id == "snap_123" mock_client.devboxes.disk_snapshots.list.assert_called_once() + def test_list_multiple(self, mock_client: Mock) -> None: + """Test list method with multiple results.""" + snapshot_view1 = MockSnapshotView(id="snap_001", name="snapshot-1") + snapshot_view2 = MockSnapshotView(id="snap_002", name="snapshot-2") + page = SimpleNamespace(snapshots=[snapshot_view1, snapshot_view2]) + mock_client.devboxes.disk_snapshots.list.return_value = page + + ops = SnapshotOps(mock_client) + snapshots = ops.list(devbox_id="dev_123", limit=10) + + assert len(snapshots) == 2 + assert isinstance(snapshots[0], Snapshot) + assert isinstance(snapshots[1], Snapshot) + assert snapshots[0].id == "snap_001" + assert snapshots[1].id == "snap_002" + mock_client.devboxes.disk_snapshots.list.assert_called_once() + def test_from_id(self, mock_client: Mock) -> None: """Test from_id method.""" - client = SnapshotOps(mock_client) - snapshot = client.from_id("snap_123") + ops = SnapshotOps(mock_client) + snapshot = ops.from_id("snap_123") assert isinstance(snapshot, Snapshot) assert snapshot.id == "snap_123" -class TestBlueprintClient: - """Tests for BlueprintClient class.""" +class TestBlueprintOps: + """Tests for BlueprintOps class.""" def test_create(self, mock_client: Mock, blueprint_view: MockBlueprintView) -> None: """Test create method.""" mock_client.blueprints.create_and_await_build_complete.return_value = blueprint_view - client = BlueprintOps(mock_client) - blueprint = client.create( + ops = BlueprintOps(mock_client) + blueprint = ops.create( name="test-blueprint", polling_config=PollingConfig(timeout_seconds=60.0), ) @@ -165,19 +223,30 @@ def test_create(self, mock_client: Mock, blueprint_view: MockBlueprintView) -> N def test_from_id(self, mock_client: Mock) -> None: """Test from_id method.""" - client = BlueprintOps(mock_client) - blueprint = client.from_id("bp_123") + ops = BlueprintOps(mock_client) + blueprint = ops.from_id("bp_123") assert isinstance(blueprint, Blueprint) assert blueprint.id == "bp_123" - def test_list(self, mock_client: Mock, blueprint_view: MockBlueprintView) -> None: - """Test list method.""" + def test_list_empty(self, mock_client: Mock) -> None: + """Test list method with empty results.""" + page = SimpleNamespace(blueprints=[]) + mock_client.blueprints.list.return_value = page + + ops = BlueprintOps(mock_client) + blueprints = ops.list(limit=10) + + assert len(blueprints) == 0 + mock_client.blueprints.list.assert_called_once() + + def test_list_single(self, mock_client: Mock, blueprint_view: MockBlueprintView) -> None: + """Test list method with single result.""" page = SimpleNamespace(blueprints=[blueprint_view]) mock_client.blueprints.list.return_value = page - client = BlueprintOps(mock_client) - blueprints = client.list( + ops = BlueprintOps(mock_client) + blueprints = ops.list( limit=10, name="test", starting_after="bp_000", @@ -188,16 +257,33 @@ def test_list(self, mock_client: Mock, blueprint_view: MockBlueprintView) -> Non assert blueprints[0].id == "bp_123" mock_client.blueprints.list.assert_called_once() + def test_list_multiple(self, mock_client: Mock) -> None: + """Test list method with multiple results.""" + blueprint_view1 = MockBlueprintView(id="bp_001", name="blueprint-1") + blueprint_view2 = MockBlueprintView(id="bp_002", name="blueprint-2") + page = SimpleNamespace(blueprints=[blueprint_view1, blueprint_view2]) + mock_client.blueprints.list.return_value = page + + ops = BlueprintOps(mock_client) + blueprints = ops.list(limit=10) + + assert len(blueprints) == 2 + assert isinstance(blueprints[0], Blueprint) + assert isinstance(blueprints[1], Blueprint) + assert blueprints[0].id == "bp_001" + assert blueprints[1].id == "bp_002" + mock_client.blueprints.list.assert_called_once() + -class TestStorageObjectClient: - """Tests for StorageObjectClient class.""" +class TestStorageObjectOps: + """Tests for StorageObjectOps class.""" def test_create(self, mock_client: Mock, object_view: MockObjectView) -> None: """Test create method.""" mock_client.objects.create.return_value = object_view - client = StorageObjectOps(mock_client) - obj = client.create(name="test.txt", content_type="text", metadata={"key": "value"}) + ops = StorageObjectOps(mock_client) + obj = ops.create(name="test.txt", content_type="text", metadata={"key": "value"}) assert isinstance(obj, StorageObject) assert obj.id == "obj_123" @@ -210,20 +296,31 @@ def test_create(self, mock_client: Mock, object_view: MockObjectView) -> None: def test_from_id(self, mock_client: Mock) -> None: """Test from_id method.""" - client = StorageObjectOps(mock_client) - obj = client.from_id("obj_123") + ops = StorageObjectOps(mock_client) + obj = ops.from_id("obj_123") assert isinstance(obj, StorageObject) assert obj.id == "obj_123" assert obj.upload_url is None - def test_list(self, mock_client: Mock, object_view: MockObjectView) -> None: - """Test list method.""" + def test_list_empty(self, mock_client: Mock) -> None: + """Test list method with empty results.""" + page = SimpleNamespace(objects=[]) + mock_client.objects.list.return_value = page + + ops = StorageObjectOps(mock_client) + objects = ops.list(limit=10) + + assert len(objects) == 0 + mock_client.objects.list.assert_called_once() + + def test_list_single(self, mock_client: Mock, object_view: MockObjectView) -> None: + """Test list method with single result.""" page = SimpleNamespace(objects=[object_view]) mock_client.objects.list.return_value = page - client = StorageObjectOps(mock_client) - objects = client.list( + ops = StorageObjectOps(mock_client) + objects = ops.list( content_type="text", limit=10, name="test", @@ -244,6 +341,23 @@ def test_list(self, mock_client: Mock, object_view: MockObjectView) -> None: state="READ_ONLY", ) + def test_list_multiple(self, mock_client: Mock) -> None: + """Test list method with multiple results.""" + object_view1 = MockObjectView(id="obj_001", name="object-1") + object_view2 = MockObjectView(id="obj_002", name="object-2") + page = SimpleNamespace(objects=[object_view1, object_view2]) + mock_client.objects.list.return_value = page + + ops = StorageObjectOps(mock_client) + objects = ops.list(limit=10) + + assert len(objects) == 2 + assert isinstance(objects[0], StorageObject) + assert isinstance(objects[1], StorageObject) + assert objects[0].id == "obj_001" + assert objects[1].id == "obj_002" + mock_client.objects.list.assert_called_once() + def test_upload_from_file(self, mock_client: Mock, object_view: MockObjectView, tmp_path: Path) -> None: """Test upload_from_file method.""" mock_client.objects.create.return_value = object_view @@ -256,8 +370,8 @@ def test_upload_from_file(self, mock_client: Mock, object_view: MockObjectView, http_client.put.return_value = mock_response mock_client._client = http_client - client = StorageObjectOps(mock_client) - obj = client.upload_from_file(temp_file, name="test.txt") + ops = StorageObjectOps(mock_client) + obj = ops.upload_from_file(temp_file, name="test.txt") assert isinstance(obj, StorageObject) assert obj.id == "obj_123" @@ -279,8 +393,8 @@ def test_upload_from_text(self, mock_client: Mock, object_view: MockObjectView) http_client.put.return_value = mock_response mock_client._client = http_client - client = StorageObjectOps(mock_client) - obj = client.upload_from_text("test content", name="test.txt", metadata={"key": "value"}) + ops = StorageObjectOps(mock_client) + obj = ops.upload_from_text("test content", name="test.txt", metadata={"key": "value"}) assert isinstance(obj, StorageObject) assert obj.id == "obj_123" @@ -302,8 +416,8 @@ def test_upload_from_bytes(self, mock_client: Mock, object_view: MockObjectView) http_client.put.return_value = mock_response mock_client._client = http_client - client = StorageObjectOps(mock_client) - obj = client.upload_from_bytes(b"test content", name="test.bin", content_type="binary") + ops = StorageObjectOps(mock_client) + obj = ops.upload_from_bytes(b"test content", name="test.bin", content_type="binary") assert isinstance(obj, StorageObject) assert obj.id == "obj_123" @@ -318,11 +432,11 @@ def test_upload_from_bytes(self, mock_client: Mock, object_view: MockObjectView) def test_upload_from_file_missing_path(self, mock_client: Mock, tmp_path: Path) -> None: """upload_from_file should raise when file cannot be read.""" - client = StorageObjectOps(mock_client) + ops = StorageObjectOps(mock_client) missing_file = tmp_path / "missing.txt" with pytest.raises(OSError, match="Failed to read file"): - client.upload_from_file(missing_file) + ops.upload_from_file(missing_file) def test_upload_from_dir(self, mock_client: Mock, object_view: MockObjectView, tmp_path: Path) -> None: """Test upload_from_dir method.""" @@ -342,8 +456,8 @@ def test_upload_from_dir(self, mock_client: Mock, object_view: MockObjectView, t http_client.put.return_value = mock_response mock_client._client = http_client - client = StorageObjectOps(mock_client) - obj = client.upload_from_dir(test_dir, name="archive.tar.gz", metadata={"key": "value"}) + ops = StorageObjectOps(mock_client) + obj = ops.upload_from_dir(test_dir, name="archive.tar.gz", metadata={"key": "value"}) assert isinstance(obj, StorageObject) assert obj.id == "obj_123" @@ -375,8 +489,8 @@ def test_upload_from_dir_default_name(self, mock_client: Mock, object_view: Mock http_client.put.return_value = mock_response mock_client._client = http_client - client = StorageObjectOps(mock_client) - obj = client.upload_from_dir(test_dir) + ops = StorageObjectOps(mock_client) + obj = ops.upload_from_dir(test_dir) assert isinstance(obj, StorageObject) # Name should be directory name + .tar.gz @@ -402,8 +516,8 @@ def test_upload_from_dir_with_ttl(self, mock_client: Mock, object_view: MockObje http_client.put.return_value = mock_response mock_client._client = http_client - client = StorageObjectOps(mock_client) - obj = client.upload_from_dir(test_dir, ttl=timedelta(hours=2)) + ops = StorageObjectOps(mock_client) + obj = ops.upload_from_dir(test_dir, ttl=timedelta(hours=2)) assert isinstance(obj, StorageObject) mock_client.objects.create.assert_called_once_with( @@ -427,8 +541,8 @@ def test_upload_from_dir_empty_directory( http_client.put.return_value = mock_response mock_client._client = http_client - client = StorageObjectOps(mock_client) - obj = client.upload_from_dir(test_dir) + ops = StorageObjectOps(mock_client) + obj = ops.upload_from_dir(test_dir) assert isinstance(obj, StorageObject) assert obj.id == "obj_123" @@ -456,9 +570,9 @@ def test_upload_from_dir_with_string_path( http_client.put.return_value = mock_response mock_client._client = http_client - client = StorageObjectOps(mock_client) + ops = StorageObjectOps(mock_client) # Pass string path instead of Path object - obj = client.upload_from_dir(str(test_dir)) + obj = ops.upload_from_dir(str(test_dir)) assert isinstance(obj, StorageObject) assert obj.id == "obj_123" @@ -472,6 +586,73 @@ def test_upload_from_dir_with_string_path( mock_client.objects.complete.assert_called_once() +class TestScorerOps: + """Tests for ScorerOps class.""" + + def test_create(self, mock_client: Mock, scorer_view: MockScorerView) -> None: + """Test create method.""" + mock_client.scenarios.scorers.create.return_value = scorer_view + + ops = ScorerOps(mock_client) + scorer = ops.create( + bash_script="echo 'score=1.0'", + type="test_scorer", + ) + + assert isinstance(scorer, Scorer) + assert scorer.id == "scorer_123" + mock_client.scenarios.scorers.create.assert_called_once() + + def test_from_id(self, mock_client: Mock) -> None: + """Test from_id method.""" + ops = ScorerOps(mock_client) + scorer = ops.from_id("scorer_123") + + assert isinstance(scorer, Scorer) + assert scorer.id == "scorer_123" + + def test_list_empty(self, mock_client: Mock) -> None: + """Test list method with empty results.""" + mock_client.scenarios.scorers.list.return_value = [] + + ops = ScorerOps(mock_client) + scorers = ops.list(limit=10) + + assert len(scorers) == 0 + mock_client.scenarios.scorers.list.assert_called_once() + + def test_list_single(self, mock_client: Mock, scorer_view: MockScorerView) -> None: + """Test list method with single result.""" + mock_client.scenarios.scorers.list.return_value = [scorer_view] + + ops = ScorerOps(mock_client) + scorers = ops.list( + limit=10, + starting_after="scorer_000", + ) + + assert len(scorers) == 1 + assert isinstance(scorers[0], Scorer) + assert scorers[0].id == "scorer_123" + mock_client.scenarios.scorers.list.assert_called_once() + + def test_list_multiple(self, mock_client: Mock) -> None: + """Test list method with multiple results.""" + scorer_view1 = MockScorerView(id="scorer_001", type="scorer-1") + scorer_view2 = MockScorerView(id="scorer_002", type="scorer-2") + mock_client.scenarios.scorers.list.return_value = [scorer_view1, scorer_view2] + + ops = ScorerOps(mock_client) + scorers = ops.list(limit=10) + + assert len(scorers) == 2 + assert isinstance(scorers[0], Scorer) + assert isinstance(scorers[1], Scorer) + assert scorers[0].id == "scorer_001" + assert scorers[1].id == "scorer_002" + mock_client.scenarios.scorers.list.assert_called_once() + + class TestRunloopSDK: """Tests for RunloopSDK class.""" @@ -480,6 +661,7 @@ def test_init(self) -> None: sdk = RunloopSDK(bearer_token="test-token") assert sdk.api is not None assert isinstance(sdk.devbox, DevboxOps) + assert isinstance(sdk.scorer, ScorerOps) assert isinstance(sdk.snapshot, SnapshotOps) assert isinstance(sdk.blueprint, BlueprintOps) assert isinstance(sdk.storage_object, StorageObjectOps) diff --git a/tests/sdk/test_scorer.py b/tests/sdk/test_scorer.py new file mode 100644 index 000000000..761a487cb --- /dev/null +++ b/tests/sdk/test_scorer.py @@ -0,0 +1,71 @@ +"""Comprehensive tests for sync Scorer class.""" + +from __future__ import annotations + +from types import SimpleNamespace +from unittest.mock import Mock + +from tests.sdk.conftest import MockScorerView +from runloop_api_client.sdk import Scorer + + +class TestScorer: + """Tests for Scorer class.""" + + def test_init(self, mock_client: Mock) -> None: + """Test Scorer initialization.""" + scorer = Scorer(mock_client, "scorer_123") + assert scorer.id == "scorer_123" + + def test_repr(self, mock_client: Mock) -> None: + """Test Scorer string representation.""" + scorer = Scorer(mock_client, "scorer_123") + assert repr(scorer) == "" + + def test_get_info(self, mock_client: Mock, scorer_view: MockScorerView) -> None: + """Test get_info method.""" + mock_client.scenarios.scorers.retrieve.return_value = scorer_view + + scorer = Scorer(mock_client, "scorer_123") + result = scorer.get_info() + + assert result == scorer_view + mock_client.scenarios.scorers.retrieve.assert_called_once_with("scorer_123") + + def test_update(self, mock_client: Mock) -> None: + """Test update method.""" + update_response = SimpleNamespace(id="scorer_123", type="updated_scorer", bash_script="echo 'score=1.0'") + mock_client.scenarios.scorers.update.return_value = update_response + + scorer = Scorer(mock_client, "scorer_123") + result = scorer.update( + type="updated_scorer", + bash_script="echo 'score=1.0'", + ) + + assert result == update_response + mock_client.scenarios.scorers.update.assert_called_once_with( + "scorer_123", + type="updated_scorer", + bash_script="echo 'score=1.0'", + ) + + def test_validate(self, mock_client: Mock) -> None: + """Test validate method.""" + validate_response = SimpleNamespace( + name="test_scorer", + scoring_context={}, + scoring_result=SimpleNamespace(score=0.95), + ) + mock_client.scenarios.scorers.validate.return_value = validate_response + + scorer = Scorer(mock_client, "scorer_123") + result = scorer.validate( + scoring_context={"test": "context"}, + ) + + assert result == validate_response + mock_client.scenarios.scorers.validate.assert_called_once_with( + "scorer_123", + scoring_context={"test": "context"}, + ) diff --git a/tests/smoketests/sdk/test_async_scorer.py b/tests/smoketests/sdk/test_async_scorer.py new file mode 100644 index 000000000..ce6603d64 --- /dev/null +++ b/tests/smoketests/sdk/test_async_scorer.py @@ -0,0 +1,122 @@ +"""Asynchronous SDK smoke tests for Scorer operations.""" + +from __future__ import annotations + +import pytest + +from runloop_api_client import InternalServerError +from runloop_api_client.sdk import AsyncRunloopSDK +from tests.smoketests.utils import unique_name + +pytestmark = [pytest.mark.smoketest, pytest.mark.asyncio] + +THIRTY_SECOND_TIMEOUT = 30 +ONE_MINUTE_TIMEOUT = 60 + + +class TestAsyncScorerLifecycle: + """Test basic async scorer lifecycle operations.""" + + @pytest.mark.timeout(ONE_MINUTE_TIMEOUT) + async def test_scorer_create_basic(self, async_sdk_client: AsyncRunloopSDK) -> None: + """Test creating a basic scorer.""" + scorer_type = unique_name("sdk-async-scorer-basic") + scorer = await async_sdk_client.scorer.create( + type=scorer_type, + bash_script="echo 'score=1.0'", + ) + + assert scorer is not None + assert scorer.id is not None + assert len(scorer.id) > 0 + + # Verify it's created successfully + info = await scorer.get_info() + assert info.type == scorer_type + assert info.bash_script == "echo 'score=1.0'" + + @pytest.mark.timeout(ONE_MINUTE_TIMEOUT) + async def test_scorer_get_info(self, async_sdk_client: AsyncRunloopSDK) -> None: + """Test retrieving scorer information.""" + scorer_type = unique_name("sdk-async-scorer-info") + scorer = await async_sdk_client.scorer.create( + type=scorer_type, + bash_script="echo 'score=0.5'", + ) + + info = await scorer.get_info() + + assert info.id == scorer.id + assert info.type == scorer_type + + @pytest.mark.timeout(ONE_MINUTE_TIMEOUT) + async def test_scorer_update(self, async_sdk_client: AsyncRunloopSDK) -> None: + """Test updating a scorer.""" + scorer_type = unique_name("sdk-async-scorer-update") + scorer = await async_sdk_client.scorer.create( + type=scorer_type, + bash_script="echo 'score=0.0'", + ) + + updated_type = unique_name("sdk-async-scorer-updated") + result = await scorer.update( + type=updated_type, + bash_script="echo 'score=1.0'", + ) + + assert result is not None + + # Verify the update + info = await scorer.get_info() + assert info.type == updated_type + assert info.bash_script == "echo 'score=1.0'" + + @pytest.mark.timeout(ONE_MINUTE_TIMEOUT) + async def test_scorer_validate(self, async_sdk_client: AsyncRunloopSDK) -> None: + """Test validating a scorer.""" + scorer_type = unique_name("sdk-async-scorer-validate") + scorer = await async_sdk_client.scorer.create( + type=scorer_type, + bash_script="echo 'score=1.0'", + ) + + try: + result = await scorer.validate( + scoring_context={}, + ) + assert result is not None + except InternalServerError: + # Backend may return 500 for validate endpoint - skip if this happens + pytest.skip("Backend returned 500 for scorer validate endpoint") + + +class TestAsyncScorerListing: + """Test async scorer listing and retrieval operations.""" + + @pytest.mark.timeout(THIRTY_SECOND_TIMEOUT) + async def test_list_scorers(self, async_sdk_client: AsyncRunloopSDK) -> None: + """Test listing scorers.""" + scorers = await async_sdk_client.scorer.list(limit=10) + + assert isinstance(scorers, list) + # List might be empty, that's okay + assert len(scorers) >= 0 + + @pytest.mark.timeout(ONE_MINUTE_TIMEOUT) + async def test_get_scorer_by_id(self, async_sdk_client: AsyncRunloopSDK) -> None: + """Test retrieving scorer by ID.""" + # Create a scorer + scorer_type = unique_name("sdk-async-scorer-retrieve") + created = await async_sdk_client.scorer.create( + type=scorer_type, + bash_script="echo 'score=1.0'", + ) + + # Retrieve it by ID + retrieved = async_sdk_client.scorer.from_id(created.id) + assert retrieved.id == created.id + + # Verify it's the same scorer + info = await retrieved.get_info() + assert info.id == created.id + assert info.type == scorer_type diff --git a/tests/smoketests/sdk/test_scorer.py b/tests/smoketests/sdk/test_scorer.py new file mode 100644 index 000000000..01df84df9 --- /dev/null +++ b/tests/smoketests/sdk/test_scorer.py @@ -0,0 +1,122 @@ +"""Synchronous SDK smoke tests for Scorer operations.""" + +from __future__ import annotations + +import pytest + +from runloop_api_client import InternalServerError +from runloop_api_client.sdk import RunloopSDK +from tests.smoketests.utils import unique_name + +pytestmark = [pytest.mark.smoketest] + +THIRTY_SECOND_TIMEOUT = 30 +ONE_MINUTE_TIMEOUT = 60 + + +class TestScorerLifecycle: + """Test basic scorer lifecycle operations.""" + + @pytest.mark.timeout(ONE_MINUTE_TIMEOUT) + def test_scorer_create_basic(self, sdk_client: RunloopSDK) -> None: + """Test creating a basic scorer.""" + scorer_type = unique_name("sdk-scorer-basic") + scorer = sdk_client.scorer.create( + type=scorer_type, + bash_script="echo 'score=1.0'", + ) + + assert scorer is not None + assert scorer.id is not None + assert len(scorer.id) > 0 + + # Verify it's created successfully + info = scorer.get_info() + assert info.type == scorer_type + assert info.bash_script == "echo 'score=1.0'" + + @pytest.mark.timeout(ONE_MINUTE_TIMEOUT) + def test_scorer_get_info(self, sdk_client: RunloopSDK) -> None: + """Test retrieving scorer information.""" + scorer_type = unique_name("sdk-scorer-info") + scorer = sdk_client.scorer.create( + type=scorer_type, + bash_script="echo 'score=0.5'", + ) + + info = scorer.get_info() + + assert info.id == scorer.id + assert info.type == scorer_type + + @pytest.mark.timeout(ONE_MINUTE_TIMEOUT) + def test_scorer_update(self, sdk_client: RunloopSDK) -> None: + """Test updating a scorer.""" + scorer_type = unique_name("sdk-scorer-update") + scorer = sdk_client.scorer.create( + type=scorer_type, + bash_script="echo 'score=0.0'", + ) + + updated_type = unique_name("sdk-scorer-updated") + result = scorer.update( + type=updated_type, + bash_script="echo 'score=1.0'", + ) + + assert result is not None + + # Verify the update + info = scorer.get_info() + assert info.type == updated_type + assert info.bash_script == "echo 'score=1.0'" + + @pytest.mark.timeout(ONE_MINUTE_TIMEOUT) + def test_scorer_validate(self, sdk_client: RunloopSDK) -> None: + """Test validating a scorer.""" + scorer_type = unique_name("sdk-scorer-validate") + scorer = sdk_client.scorer.create( + type=scorer_type, + bash_script="echo 'score=1.0'", + ) + + try: + result = scorer.validate( + scoring_context={}, + ) + assert result is not None + except InternalServerError: + # Backend may return 500 for validate endpoint - skip if this happens + pytest.skip("Backend returned 500 for scorer validate endpoint") + + +class TestScorerListing: + """Test scorer listing and retrieval operations.""" + + @pytest.mark.timeout(THIRTY_SECOND_TIMEOUT) + def test_list_scorers(self, sdk_client: RunloopSDK) -> None: + """Test listing scorers.""" + scorers = sdk_client.scorer.list(limit=10) + + assert isinstance(scorers, list) + # List might be empty, that's okay + assert len(scorers) >= 0 + + @pytest.mark.timeout(ONE_MINUTE_TIMEOUT) + def test_get_scorer_by_id(self, sdk_client: RunloopSDK) -> None: + """Test retrieving scorer by ID.""" + # Create a scorer + scorer_type = unique_name("sdk-scorer-retrieve") + created = sdk_client.scorer.create( + type=scorer_type, + bash_script="echo 'score=1.0'", + ) + + # Retrieve it by ID + retrieved = sdk_client.scorer.from_id(created.id) + assert retrieved.id == created.id + + # Verify it's the same scorer + info = retrieved.get_info() + assert info.id == created.id + assert info.type == scorer_type From 3f6bcc8239e030580ea8ed554e16a5550154b042 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 2 Dec 2025 00:24:33 +0000 Subject: [PATCH 9/9] release: 1.0.0 --- .release-please-manifest.json | 2 +- CHANGELOG.md | 30 ++++++++++++++++++++++++++++++ pyproject.toml | 2 +- src/runloop_api_client/_version.py | 2 +- 4 files changed, 33 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index a9d0cc147..fea345409 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.69.0" + ".": "1.0.0" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index c322055c7..e62ef10bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,35 @@ # Changelog +## 1.0.0 (2025-12-02) + +Full Changelog: [v0.69.0...v1.0.0](https://github.com/runloopai/api-client-python/compare/v0.69.0...v1.0.0) + +### ⚠ BREAKING CHANGES + +* **devbox:** made command positional arg in exec and exec_async ([#695](https://github.com/runloopai/api-client-python/issues/695)) + +### Features + +* **blueprints:** Add build context to the OpenAPI spec ([#6494](https://github.com/runloopai/api-client-python/issues/6494)) ([d202b94](https://github.com/runloopai/api-client-python/commit/d202b942c07614ca954a8bbe3a9a6302e9a04216)) +* **devbox:** added devbox.shell(shellName) command and stateful shell class to SDK ([#696](https://github.com/runloopai/api-client-python/issues/696)) ([c1e8f09](https://github.com/runloopai/api-client-python/commit/c1e8f0965a419ff53d830ba3c43a1c9a29dae5c7)) +* **devbox:** made command positional arg in exec and exec_async ([#695](https://github.com/runloopai/api-client-python/issues/695)) ([6cc8c2f](https://github.com/runloopai/api-client-python/commit/6cc8c2fd4f904e8cc4386d81558157ca6fb69bfa)) +* **sdk:** added scorer classes to sdk ([#698](https://github.com/runloopai/api-client-python/issues/698)) ([85f798f](https://github.com/runloopai/api-client-python/commit/85f798f2d8a7727b783e01a260ff0a52bdf01d78)) + + +### Bug Fixes + +* **api:** don't ignore devbox keep_alive, suspend and resume in api ([fe3589f](https://github.com/runloopai/api-client-python/commit/fe3589f5fbb36a5b79f1d4a25e86f88676556fdb)) +* **devbox:** launch parameter typo ([1c9c346](https://github.com/runloopai/api-client-python/commit/1c9c346e475b64fc389928fee0f7140e532c4f9c)) +* **scenarios:** update parameters for manually maintained start_run_and_await_env_ready methods ([#692](https://github.com/runloopai/api-client-python/issues/692)) ([8000495](https://github.com/runloopai/api-client-python/commit/8000495f70b2e6f4f12742fb8a6d641dbbc088ca)) +* **scorer:** fixed RL_TEST_CONTEXT to RL_SCORER_CONTEXT ([df43a42](https://github.com/runloopai/api-client-python/commit/df43a42a45b9ce67aba27835a41c9a0ebfc6a407)) + + +### Chores + +* **blueprints:** Add build context examples ([#694](https://github.com/runloopai/api-client-python/issues/694)) ([6e63928](https://github.com/runloopai/api-client-python/commit/6e6392864b3cde20dfea5d173fed9a156b960ccd)) +* hide build context APIs ([159a38f](https://github.com/runloopai/api-client-python/commit/159a38f0980c00430a1b949541076b0d63df2df2)) +* **mounts:** Update documentation for deprecated fields to direct the user to the replacement API ([4936844](https://github.com/runloopai/api-client-python/commit/4936844989ec7a0d37c835dd37b8007e8caba944)) + ## 0.69.0 (2025-11-21) Full Changelog: [v0.68.0...v0.69.0](https://github.com/runloopai/api-client-python/compare/v0.68.0...v0.69.0) diff --git a/pyproject.toml b/pyproject.toml index 79ffe90b5..ee4842bdd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "runloop_api_client" -version = "0.69.0" +version = "1.0.0" description = "The official Python library for the runloop API" dynamic = ["readme"] license = "MIT" diff --git a/src/runloop_api_client/_version.py b/src/runloop_api_client/_version.py index 42253e2b3..5378c8c64 100644 --- a/src/runloop_api_client/_version.py +++ b/src/runloop_api_client/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "runloop_api_client" -__version__ = "0.69.0" # x-release-please-version +__version__ = "1.0.0" # x-release-please-version