Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
".": "1.20.3"
".": "1.21.0"
}
8 changes: 4 additions & 4 deletions .stats.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
configured_endpoints: 116
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/runloop-ai/runloop-6cf4d9a6afac92d72787088b3aefa941f5240ee522d9e98e1160eea2e29f87f4.yml
openapi_spec_hash: e07fc8349cf507b083830b4e2b0caca0
config_hash: 436c8d4e665915db22b5d98fe58382c1
configured_endpoints: 119
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/runloop-ai/runloop-6ec03cfc156036a0758aebc523b469f3007de431312c536c434efe795e1892e7.yml
openapi_spec_hash: 092761a0209e0950cfd8f262a981adb1
config_hash: 06faf3176c48bf2b258730ce50809f0f
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,21 @@
# Changelog

## 1.21.0 (2026-05-13)

Full Changelog: [v1.20.3...v1.21.0](https://github.com/runloopai/api-client-python/compare/v1.20.3...v1.21.0)

### Features

* **internal/types:** support eagerly validating pydantic iterators ([f089d05](https://github.com/runloopai/api-client-python/commit/f089d05c5e00c2ba980d0afdea79ed026242a174))
* **metadata:** add object metadata viewing, discovery, and validation ([#9124](https://github.com/runloopai/api-client-python/issues/9124)) ([ba0b5dc](https://github.com/runloopai/api-client-python/commit/ba0b5dc873a47073c453b168fe4d966754cccc0e))


### Bug Fixes

* **client:** add missing f-string prefix in file type error message ([930b1e2](https://github.com/runloopai/api-client-python/commit/930b1e2d169a291415007f2897fec5a410ecf993))
* update openapi.stainless.yaml for PTY server methods ([#9218](https://github.com/runloopai/api-client-python/issues/9218)) ([ac1cb22](https://github.com/runloopai/api-client-python/commit/ac1cb2264b8e39da8aa65d8fdcd4595bce55def6))
* update types for pty control, use int instead of str ([#9235](https://github.com/runloopai/api-client-python/issues/9235)) ([7ceb2bf](https://github.com/runloopai/api-client-python/commit/7ceb2bf09c3241a27892fbab2272d63ef76b1a8a))

## 1.20.3 (2026-05-08)

Full Changelog: [v1.20.2...v1.20.3](https://github.com/runloopai/api-client-python/compare/v1.20.2...v1.20.3)
Expand Down
15 changes: 15 additions & 0 deletions api.md
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@ from runloop_api_client.types import (
DevboxSnapshotListView,
DevboxSnapshotView,
DevboxView,
PtyTunnelView,
TunnelView,
DevboxCreateSSHKeyResponse,
DevboxReadFileContentsResponse,
Expand All @@ -211,6 +212,7 @@ Methods:
- <code title="get /v1/devboxes/{id}">client.devboxes.<a href="./src/runloop_api_client/resources/devboxes/devboxes.py">retrieve</a>(id) -> <a href="./src/runloop_api_client/types/devbox_view.py">DevboxView</a></code>
- <code title="post /v1/devboxes/{id}">client.devboxes.<a href="./src/runloop_api_client/resources/devboxes/devboxes.py">update</a>(id, \*\*<a href="src/runloop_api_client/types/devbox_update_params.py">params</a>) -> <a href="./src/runloop_api_client/types/devbox_view.py">DevboxView</a></code>
- <code title="get /v1/devboxes">client.devboxes.<a href="./src/runloop_api_client/resources/devboxes/devboxes.py">list</a>(\*\*<a href="src/runloop_api_client/types/devbox_list_params.py">params</a>) -> <a href="./src/runloop_api_client/types/devbox_view.py">SyncDevboxesCursorIDPage[DevboxView]</a></code>
- <code title="post /v1/devboxes/{id}/create_pty_tunnel">client.devboxes.<a href="./src/runloop_api_client/resources/devboxes/devboxes.py">create_pty_tunnel</a>(id) -> <a href="./src/runloop_api_client/types/pty_tunnel_view.py">PtyTunnelView</a></code>
- <code title="post /v1/devboxes/{id}/create_ssh_key">client.devboxes.<a href="./src/runloop_api_client/resources/devboxes/devboxes.py">create_ssh_key</a>(id) -> <a href="./src/runloop_api_client/types/devbox_create_ssh_key_response.py">DevboxCreateSSHKeyResponse</a></code>
- <code title="post /v1/devboxes/disk_snapshots/{id}/delete">client.devboxes.<a href="./src/runloop_api_client/resources/devboxes/devboxes.py">delete_disk_snapshot</a>(id) -> object</code>
- <code title="post /v1/devboxes/{id}/download_file">client.devboxes.<a href="./src/runloop_api_client/resources/devboxes/devboxes.py">download_file</a>(id, \*\*<a href="src/runloop_api_client/types/devbox_download_file_params.py">params</a>) -> BinaryAPIResponse</code>
Expand Down Expand Up @@ -278,6 +280,19 @@ Methods:
- <code title="get /v1/devboxes/{devbox_id}/executions/{execution_id}/stream_stderr_updates">client.devboxes.executions.<a href="./src/runloop_api_client/resources/devboxes/executions.py">stream_stderr_updates</a>(execution_id, \*, devbox_id, \*\*<a href="src/runloop_api_client/types/devboxes/execution_stream_stderr_updates_params.py">params</a>) -> <a href="./src/runloop_api_client/types/devboxes/execution_update_chunk.py">ExecutionUpdateChunk</a></code>
- <code title="get /v1/devboxes/{devbox_id}/executions/{execution_id}/stream_stdout_updates">client.devboxes.executions.<a href="./src/runloop_api_client/resources/devboxes/executions.py">stream_stdout_updates</a>(execution_id, \*, devbox_id, \*\*<a href="src/runloop_api_client/types/devboxes/execution_stream_stdout_updates_params.py">params</a>) -> <a href="./src/runloop_api_client/types/devboxes/execution_update_chunk.py">ExecutionUpdateChunk</a></code>

# Pty

Types:

```python
from runloop_api_client.types import PtyConnectView, PtyControlParams, PtyControlResultView
```

Methods:

- <code title="get /pty/{session_name}">client.pty.<a href="./src/runloop_api_client/resources/pty.py">connect</a>(session_name, \*\*<a href="src/runloop_api_client/types/pty_connect_params.py">params</a>) -> <a href="./src/runloop_api_client/types/pty_connect_view.py">PtyConnectView</a></code>
- <code title="post /pty/{session_name}/control">client.pty.<a href="./src/runloop_api_client/resources/pty.py">control</a>(session_name, \*\*<a href="src/runloop_api_client/types/pty_control_params.py">params</a>) -> <a href="./src/runloop_api_client/types/pty_control_result_view.py">PtyControlResultView</a></code>

# Scenarios

Types:
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "runloop_api_client"
version = "1.20.3"
version = "1.21.0"
description = "The official Python library for the runloop API"
dynamic = ["readme"]
license = "MIT"
Expand Down
38 changes: 38 additions & 0 deletions src/runloop_api_client/_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@

if TYPE_CHECKING:
from .resources import (
pty,
axons,
agents,
apikeys,
Expand All @@ -52,6 +53,7 @@
restricted_keys,
network_policies,
)
from .resources.pty import PtyResource, AsyncPtyResource
from .resources.agents import AgentsResource, AsyncAgentsResource
from .resources.apikeys import ApikeysResource, AsyncApikeysResource
from .resources.objects import ObjectsResource, AsyncObjectsResource
Expand Down Expand Up @@ -184,6 +186,12 @@ def devboxes(self) -> DevboxesResource:

return DevboxesResource(self)

@cached_property
def pty(self) -> PtyResource:
from .resources.pty import PtyResource

return PtyResource(self)

@cached_property
def scenarios(self) -> ScenariosResource:
from .resources.scenarios import ScenariosResource
Expand Down Expand Up @@ -466,6 +474,12 @@ def devboxes(self) -> AsyncDevboxesResource:

return AsyncDevboxesResource(self)

@cached_property
def pty(self) -> AsyncPtyResource:
from .resources.pty import AsyncPtyResource

return AsyncPtyResource(self)

@cached_property
def scenarios(self) -> AsyncScenariosResource:
from .resources.scenarios import AsyncScenariosResource
Expand Down Expand Up @@ -683,6 +697,12 @@ def devboxes(self) -> devboxes.DevboxesResourceWithRawResponse:

return DevboxesResourceWithRawResponse(self._client.devboxes)

@cached_property
def pty(self) -> pty.PtyResourceWithRawResponse:
from .resources.pty import PtyResourceWithRawResponse

return PtyResourceWithRawResponse(self._client.pty)

@cached_property
def scenarios(self) -> scenarios.ScenariosResourceWithRawResponse:
from .resources.scenarios import ScenariosResourceWithRawResponse
Expand Down Expand Up @@ -780,6 +800,12 @@ def devboxes(self) -> devboxes.AsyncDevboxesResourceWithRawResponse:

return AsyncDevboxesResourceWithRawResponse(self._client.devboxes)

@cached_property
def pty(self) -> pty.AsyncPtyResourceWithRawResponse:
from .resources.pty import AsyncPtyResourceWithRawResponse

return AsyncPtyResourceWithRawResponse(self._client.pty)

@cached_property
def scenarios(self) -> scenarios.AsyncScenariosResourceWithRawResponse:
from .resources.scenarios import AsyncScenariosResourceWithRawResponse
Expand Down Expand Up @@ -877,6 +903,12 @@ def devboxes(self) -> devboxes.DevboxesResourceWithStreamingResponse:

return DevboxesResourceWithStreamingResponse(self._client.devboxes)

@cached_property
def pty(self) -> pty.PtyResourceWithStreamingResponse:
from .resources.pty import PtyResourceWithStreamingResponse

return PtyResourceWithStreamingResponse(self._client.pty)

@cached_property
def scenarios(self) -> scenarios.ScenariosResourceWithStreamingResponse:
from .resources.scenarios import ScenariosResourceWithStreamingResponse
Expand Down Expand Up @@ -974,6 +1006,12 @@ def devboxes(self) -> devboxes.AsyncDevboxesResourceWithStreamingResponse:

return AsyncDevboxesResourceWithStreamingResponse(self._client.devboxes)

@cached_property
def pty(self) -> pty.AsyncPtyResourceWithStreamingResponse:
from .resources.pty import AsyncPtyResourceWithStreamingResponse

return AsyncPtyResourceWithStreamingResponse(self._client.pty)

@cached_property
def scenarios(self) -> scenarios.AsyncScenariosResourceWithStreamingResponse:
from .resources.scenarios import AsyncScenariosResourceWithStreamingResponse
Expand Down
2 changes: 1 addition & 1 deletion src/runloop_api_client/_files.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ async def async_to_httpx_files(files: RequestFiles | None) -> HttpxRequestFiles
elif is_sequence_t(files):
files = [(key, await _async_transform_file(file)) for key, file in files]
else:
raise TypeError("Unexpected file type input {type(files)}, expected mapping or sequence")
raise TypeError(f"Unexpected file type input {type(files)}, expected mapping or sequence")

return files

Expand Down
80 changes: 80 additions & 0 deletions src/runloop_api_client/_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@
ClassVar,
Protocol,
Required,
Annotated,
ParamSpec,
TypeAlias,
TypedDict,
TypeGuard,
final,
Expand Down Expand Up @@ -79,7 +81,15 @@
from ._constants import RAW_RESPONSE_HEADER

if TYPE_CHECKING:
from pydantic import GetCoreSchemaHandler, ValidatorFunctionWrapHandler
from pydantic_core import CoreSchema, core_schema
from pydantic_core.core_schema import ModelField, ModelSchema, LiteralSchema, ModelFieldsSchema
else:
try:
from pydantic_core import CoreSchema, core_schema
except ImportError:
CoreSchema = None
core_schema = None

__all__ = ["BaseModel", "GenericModel"]

Expand Down Expand Up @@ -396,6 +406,76 @@ def model_dump_json(
)


class _EagerIterable(list[_T], Generic[_T]):
"""
Accepts any Iterable[T] input (including generators), consumes it
eagerly, and validates all items upfront.

Validation preserves the original container type where possible
(e.g. a set[T] stays a set[T]). Serialization (model_dump / JSON)
always emits a list — round-tripping through model_dump() will not
restore the original container type.
"""

@classmethod
def __get_pydantic_core_schema__(
cls,
source_type: Any,
handler: GetCoreSchemaHandler,
) -> CoreSchema:
(item_type,) = get_args(source_type) or (Any,)
item_schema: CoreSchema = handler.generate_schema(item_type)
list_of_items_schema: CoreSchema = core_schema.list_schema(item_schema)

return core_schema.no_info_wrap_validator_function(
cls._validate,
list_of_items_schema,
serialization=core_schema.plain_serializer_function_ser_schema(
cls._serialize,
info_arg=False,
),
)

@staticmethod
def _validate(v: Iterable[_T], handler: "ValidatorFunctionWrapHandler") -> Any:
original_type: type[Any] = type(v)

# Normalize to list so list_schema can validate each item
if isinstance(v, list):
items: list[_T] = v
else:
try:
items = list(v)
except TypeError as e:
raise TypeError("Value is not iterable") from e

# Validate items against the inner schema
validated: list[_T] = handler(items)

# Reconstruct original container type
if original_type is list:
return validated
# str(list) produces the list's repr, not a string built from items,
# so skip reconstruction for str and its subclasses.
if issubclass(original_type, str):
return validated
try:
return original_type(validated)
except (TypeError, ValueError):
# If the type cannot be reconstructed, just return the validated list
return validated

@staticmethod
def _serialize(v: Iterable[_T]) -> list[_T]:
"""Always serialize as a list so Pydantic's JSON encoder is happy."""
if isinstance(v, list):
return v
return list(v)


EagerIterable: TypeAlias = Annotated[Iterable[_T], _EagerIterable]


def _construct_field(value: object, field: FieldInfo, key: str) -> object:
if value is None:
return field_get_default(field)
Expand Down
2 changes: 1 addition & 1 deletion src/runloop_api_client/_version.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.

__title__ = "runloop_api_client"
__version__ = "1.20.3" # x-release-please-version
__version__ = "1.21.0" # x-release-please-version
14 changes: 14 additions & 0 deletions src/runloop_api_client/resources/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.

from .pty import (
PtyResource,
AsyncPtyResource,
PtyResourceWithRawResponse,
AsyncPtyResourceWithRawResponse,
PtyResourceWithStreamingResponse,
AsyncPtyResourceWithStreamingResponse,
)
from .axons import (
AxonsResource,
AsyncAxonsResource,
Expand Down Expand Up @@ -164,6 +172,12 @@
"AsyncDevboxesResourceWithRawResponse",
"DevboxesResourceWithStreamingResponse",
"AsyncDevboxesResourceWithStreamingResponse",
"PtyResource",
"AsyncPtyResource",
"PtyResourceWithRawResponse",
"AsyncPtyResourceWithRawResponse",
"PtyResourceWithStreamingResponse",
"AsyncPtyResourceWithStreamingResponse",
"ScenariosResource",
"AsyncScenariosResource",
"ScenariosResourceWithRawResponse",
Expand Down
Loading