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