From 99c65b82133b06f93d22997568d1750275746dce Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Thu, 30 Apr 2026 05:21:21 +0000
Subject: [PATCH 1/9] codegen metadata
---
.stats.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.stats.yml b/.stats.yml
index ddaaf63..61db5f9 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 30
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/miru-ml%2Fmiru-platform-17499bf3c547af4a5621b6681e2bfd3affd3f723f5d093b06dd359e074e2ef11.yml
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/miru-ml/miru-platform-17499bf3c547af4a5621b6681e2bfd3affd3f723f5d093b06dd359e074e2ef11.yml
openapi_spec_hash: 422d94e0e3ad00df785039db896b320f
config_hash: 44dad6a95136246af502e80b91c194b9
From ac56eb4305574586a11197fdebd0f351cebe216c Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 1 May 2026 04:12:15 +0000
Subject: [PATCH 2/9] codegen metadata
---
.stats.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.stats.yml b/.stats.yml
index 61db5f9..e70f206 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 30
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/miru-ml/miru-platform-17499bf3c547af4a5621b6681e2bfd3affd3f723f5d093b06dd359e074e2ef11.yml
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/miru-ml/miru-platform-f7598d0f96a0a7b61e83fb3dc1f049e47a585ba9427517aba4294ff487fb79e8.yml
openapi_spec_hash: 422d94e0e3ad00df785039db896b320f
config_hash: 44dad6a95136246af502e80b91c194b9
From bd0e40e96fecdfa1a46e4cff6260aadcde3aaf0e Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 1 May 2026 04:17:14 +0000
Subject: [PATCH 3/9] chore(internal): reformat pyproject.toml
---
pyproject.toml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pyproject.toml b/pyproject.toml
index 177fc44..f9b2cc1 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -154,7 +154,7 @@ show_error_codes = true
#
# We also exclude our `tests` as mypy doesn't always infer
# types correctly and Pyright will still catch any type errors.
-exclude = ['src/miru_platform_sdk/_files.py', '_dev/.*.py', 'tests/.*']
+exclude = ["src/miru_platform_sdk/_files.py", "_dev/.*.py", "tests/.*"]
strict_equality = true
implicit_reexport = true
From 2dbcc652ba6c201d997a87c6bbf9810cb8c5eabb Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 6 May 2026 22:42:25 +0000
Subject: [PATCH 4/9] feat(api): upgrade to 2026-05-06.rainier-beta.3
---
.stats.yml | 6 +-
api.md | 22 ++-
src/miru_platform_sdk/_client.py | 38 ++++
src/miru_platform_sdk/resources/__init__.py | 14 ++
.../resources/config_schemas.py | 4 +-
.../resources/deployments.py | 22 ++-
src/miru_platform_sdk/resources/devices.py | 173 +++++++-----------
.../resources/provisioning_tokens.py | 135 ++++++++++++++
src/miru_platform_sdk/types/__init__.py | 19 +-
.../types/config_instance.py | 6 +-
src/miru_platform_sdk/types/config_schema.py | 5 +-
.../types/config_schema_create_params.py | 2 +-
src/miru_platform_sdk/types/deployment.py | 61 +++---
.../types/deployment_create_params.py | 9 +-
.../types/deployment_list.py | 8 +-
.../types/deployment_list_params.py | 2 +-
src/miru_platform_sdk/types/device.py | 12 ++
.../types/device_create_params.py | 6 +-
.../device_issue_activation_token_params.py | 15 --
src/miru_platform_sdk/types/device_list.py | 8 +-
.../types/device_list_params.py | 5 +-
.../types/device_retrieve_params.py | 13 ++
.../types/device_update_params.py | 6 +-
.../types/instance_content.py | 2 +-
.../types/instance_content_param.py | 2 +-
...oken_response.py => provisioning_token.py} | 6 +-
tests/api_resources/test_config_schemas.py | 4 +-
tests/api_resources/test_devices.py | 163 +++++------------
.../api_resources/test_provisioning_tokens.py | 80 ++++++++
29 files changed, 532 insertions(+), 316 deletions(-)
create mode 100644 src/miru_platform_sdk/resources/provisioning_tokens.py
delete mode 100644 src/miru_platform_sdk/types/device_issue_activation_token_params.py
create mode 100644 src/miru_platform_sdk/types/device_retrieve_params.py
rename src/miru_platform_sdk/types/{device_issue_activation_token_response.py => provisioning_token.py} (61%)
create mode 100644 tests/api_resources/test_provisioning_tokens.py
diff --git a/.stats.yml b/.stats.yml
index e70f206..a0b90df 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 30
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/miru-ml/miru-platform-f7598d0f96a0a7b61e83fb3dc1f049e47a585ba9427517aba4294ff487fb79e8.yml
-openapi_spec_hash: 422d94e0e3ad00df785039db896b320f
-config_hash: 44dad6a95136246af502e80b91c194b9
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/miru-ml/miru-platform-ea08d7548c36e9295af05b1f7e60b6e8c15c8c8ceb558449a87f20976f1fc256.yml
+openapi_spec_hash: 6a803b3d8bc3d2030749d4f504392d98
+config_hash: df24eb8211667ea5c8651e1e5e66e441
diff --git a/api.md b/api.md
index 4ffced5..811ddd6 100644
--- a/api.md
+++ b/api.md
@@ -70,23 +70,29 @@ Methods:
Types:
```python
-from miru_platform_sdk.types import (
- Device,
- DeviceList,
- DeviceIssueActivationTokenResponse,
- DevicePingResponse,
-)
+from miru_platform_sdk.types import Device, DeviceList, DevicePingResponse
```
Methods:
- client.devices.create(\*\*params) -> Device
-- client.devices.retrieve(device_id) -> Device
+- client.devices.retrieve(device_id, \*\*params) -> Device
- client.devices.update(device_id, \*\*params) -> Device
- client.devices.list(\*\*params) -> DeviceList
-- client.devices.issue_activation_token(device_id, \*\*params) -> DeviceIssueActivationTokenResponse
- client.devices.ping(device_id, \*\*params) -> DevicePingResponse
+# ProvisioningTokens
+
+Types:
+
+```python
+from miru_platform_sdk.types import ProvisioningToken
+```
+
+Methods:
+
+- client.provisioning_tokens.create() -> ProvisioningToken
+
# GitCommits
Types:
diff --git a/src/miru_platform_sdk/_client.py b/src/miru_platform_sdk/_client.py
index 95ccfb8..eee16f5 100644
--- a/src/miru_platform_sdk/_client.py
+++ b/src/miru_platform_sdk/_client.py
@@ -44,6 +44,7 @@
config_types,
config_schemas,
config_instances,
+ provisioning_tokens,
)
from .resources.devices import DevicesResource, AsyncDevicesResource
from .resources.releases import ReleasesResource, AsyncReleasesResource
@@ -53,6 +54,7 @@
from .resources.config_types import ConfigTypesResource, AsyncConfigTypesResource
from .resources.config_schemas import ConfigSchemasResource, AsyncConfigSchemasResource
from .resources.config_instances import ConfigInstancesResource, AsyncConfigInstancesResource
+ from .resources.provisioning_tokens import ProvisioningTokensResource, AsyncProvisioningTokensResource
__all__ = [
"ENVIRONMENTS",
@@ -200,6 +202,12 @@ def devices(self) -> DevicesResource:
return DevicesResource(self)
+ @cached_property
+ def provisioning_tokens(self) -> ProvisioningTokensResource:
+ from .resources.provisioning_tokens import ProvisioningTokensResource
+
+ return ProvisioningTokensResource(self)
+
@cached_property
def git_commits(self) -> GitCommitsResource:
from .resources.git_commits import GitCommitsResource
@@ -462,6 +470,12 @@ def devices(self) -> AsyncDevicesResource:
return AsyncDevicesResource(self)
+ @cached_property
+ def provisioning_tokens(self) -> AsyncProvisioningTokensResource:
+ from .resources.provisioning_tokens import AsyncProvisioningTokensResource
+
+ return AsyncProvisioningTokensResource(self)
+
@cached_property
def git_commits(self) -> AsyncGitCommitsResource:
from .resources.git_commits import AsyncGitCommitsResource
@@ -634,6 +648,12 @@ def devices(self) -> devices.DevicesResourceWithRawResponse:
return DevicesResourceWithRawResponse(self._client.devices)
+ @cached_property
+ def provisioning_tokens(self) -> provisioning_tokens.ProvisioningTokensResourceWithRawResponse:
+ from .resources.provisioning_tokens import ProvisioningTokensResourceWithRawResponse
+
+ return ProvisioningTokensResourceWithRawResponse(self._client.provisioning_tokens)
+
@cached_property
def git_commits(self) -> git_commits.GitCommitsResourceWithRawResponse:
from .resources.git_commits import GitCommitsResourceWithRawResponse
@@ -689,6 +709,12 @@ def devices(self) -> devices.AsyncDevicesResourceWithRawResponse:
return AsyncDevicesResourceWithRawResponse(self._client.devices)
+ @cached_property
+ def provisioning_tokens(self) -> provisioning_tokens.AsyncProvisioningTokensResourceWithRawResponse:
+ from .resources.provisioning_tokens import AsyncProvisioningTokensResourceWithRawResponse
+
+ return AsyncProvisioningTokensResourceWithRawResponse(self._client.provisioning_tokens)
+
@cached_property
def git_commits(self) -> git_commits.AsyncGitCommitsResourceWithRawResponse:
from .resources.git_commits import AsyncGitCommitsResourceWithRawResponse
@@ -744,6 +770,12 @@ def devices(self) -> devices.DevicesResourceWithStreamingResponse:
return DevicesResourceWithStreamingResponse(self._client.devices)
+ @cached_property
+ def provisioning_tokens(self) -> provisioning_tokens.ProvisioningTokensResourceWithStreamingResponse:
+ from .resources.provisioning_tokens import ProvisioningTokensResourceWithStreamingResponse
+
+ return ProvisioningTokensResourceWithStreamingResponse(self._client.provisioning_tokens)
+
@cached_property
def git_commits(self) -> git_commits.GitCommitsResourceWithStreamingResponse:
from .resources.git_commits import GitCommitsResourceWithStreamingResponse
@@ -799,6 +831,12 @@ def devices(self) -> devices.AsyncDevicesResourceWithStreamingResponse:
return AsyncDevicesResourceWithStreamingResponse(self._client.devices)
+ @cached_property
+ def provisioning_tokens(self) -> provisioning_tokens.AsyncProvisioningTokensResourceWithStreamingResponse:
+ from .resources.provisioning_tokens import AsyncProvisioningTokensResourceWithStreamingResponse
+
+ return AsyncProvisioningTokensResourceWithStreamingResponse(self._client.provisioning_tokens)
+
@cached_property
def git_commits(self) -> git_commits.AsyncGitCommitsResourceWithStreamingResponse:
from .resources.git_commits import AsyncGitCommitsResourceWithStreamingResponse
diff --git a/src/miru_platform_sdk/resources/__init__.py b/src/miru_platform_sdk/resources/__init__.py
index 120ee3c..b027fcf 100644
--- a/src/miru_platform_sdk/resources/__init__.py
+++ b/src/miru_platform_sdk/resources/__init__.py
@@ -64,6 +64,14 @@
ConfigInstancesResourceWithStreamingResponse,
AsyncConfigInstancesResourceWithStreamingResponse,
)
+from .provisioning_tokens import (
+ ProvisioningTokensResource,
+ AsyncProvisioningTokensResource,
+ ProvisioningTokensResourceWithRawResponse,
+ AsyncProvisioningTokensResourceWithRawResponse,
+ ProvisioningTokensResourceWithStreamingResponse,
+ AsyncProvisioningTokensResourceWithStreamingResponse,
+)
__all__ = [
"ConfigInstancesResource",
@@ -96,6 +104,12 @@
"AsyncDevicesResourceWithRawResponse",
"DevicesResourceWithStreamingResponse",
"AsyncDevicesResourceWithStreamingResponse",
+ "ProvisioningTokensResource",
+ "AsyncProvisioningTokensResource",
+ "ProvisioningTokensResourceWithRawResponse",
+ "AsyncProvisioningTokensResourceWithRawResponse",
+ "ProvisioningTokensResourceWithStreamingResponse",
+ "AsyncProvisioningTokensResourceWithStreamingResponse",
"GitCommitsResource",
"AsyncGitCommitsResource",
"GitCommitsResourceWithRawResponse",
diff --git a/src/miru_platform_sdk/resources/config_schemas.py b/src/miru_platform_sdk/resources/config_schemas.py
index 16ee51b..bd1c161 100644
--- a/src/miru_platform_sdk/resources/config_schemas.py
+++ b/src/miru_platform_sdk/resources/config_schemas.py
@@ -81,7 +81,7 @@ def create(
git_commit: The git commit to link to this config schema.
- instance_filepath: The file path for config instances created from this schema.
+ instance_filepath: The absolute file system path config instances for this schema are written to.
extra_headers: Send extra headers
@@ -269,7 +269,7 @@ async def create(
git_commit: The git commit to link to this config schema.
- instance_filepath: The file path for config instances created from this schema.
+ instance_filepath: The absolute file system path config instances for this schema are written to.
extra_headers: Send extra headers
diff --git a/src/miru_platform_sdk/resources/deployments.py b/src/miru_platform_sdk/resources/deployments.py
index 1c95546..ecc4905 100644
--- a/src/miru_platform_sdk/resources/deployments.py
+++ b/src/miru_platform_sdk/resources/deployments.py
@@ -84,10 +84,11 @@ def create(
target_status: Desired state of the deployment.
- - Staged: ready for deployment. Deployments can only be staged if their release
- is not the current release for the device.
- - Deployed: deployed to the device. Deployments can only be deployed if their
- release is the device's current release.
+ `staged` means the deployment is ready for deployment. Deployments can only be
+ staged if their release is not the device's current release.
+
+ `deployed` means the deployment should be deployed to the device. Deployments
+ can only be deployed if their release is the device's current release.
expand: Fields to expand on the deployment resource.
@@ -168,7 +169,7 @@ def list(
self,
*,
id: SequenceNotStr[str] | Omit = omit,
- activity_status: List[Literal["drifted", "staged", "queued", "deployed", "archived"]] | Omit = omit,
+ activity_status: List[Literal["drifted", "staged", "queued", "deployed", "removing", "archived"]] | Omit = omit,
device_id: SequenceNotStr[str] | Omit = omit,
error_status: List[Literal["none", "failed", "retrying"]] | Omit = omit,
expand: List[Literal["total_count", "device", "release", "config_instances"]] | Omit = omit,
@@ -417,10 +418,11 @@ async def create(
target_status: Desired state of the deployment.
- - Staged: ready for deployment. Deployments can only be staged if their release
- is not the current release for the device.
- - Deployed: deployed to the device. Deployments can only be deployed if their
- release is the device's current release.
+ `staged` means the deployment is ready for deployment. Deployments can only be
+ staged if their release is not the device's current release.
+
+ `deployed` means the deployment should be deployed to the device. Deployments
+ can only be deployed if their release is the device's current release.
expand: Fields to expand on the deployment resource.
@@ -503,7 +505,7 @@ async def list(
self,
*,
id: SequenceNotStr[str] | Omit = omit,
- activity_status: List[Literal["drifted", "staged", "queued", "deployed", "archived"]] | Omit = omit,
+ activity_status: List[Literal["drifted", "staged", "queued", "deployed", "removing", "archived"]] | Omit = omit,
device_id: SequenceNotStr[str] | Omit = omit,
error_status: List[Literal["none", "failed", "retrying"]] | Omit = omit,
expand: List[Literal["total_count", "device", "release", "config_instances"]] | Omit = omit,
diff --git a/src/miru_platform_sdk/resources/devices.py b/src/miru_platform_sdk/resources/devices.py
index 30d2174..ce9d543 100644
--- a/src/miru_platform_sdk/resources/devices.py
+++ b/src/miru_platform_sdk/resources/devices.py
@@ -12,7 +12,7 @@
device_ping_params,
device_create_params,
device_update_params,
- device_issue_activation_token_params,
+ device_retrieve_params,
)
from .._types import Body, Omit, Query, Headers, NotGiven, SequenceNotStr, omit, not_given
from .._utils import path_template, maybe_transform, async_maybe_transform
@@ -28,7 +28,6 @@
from ..types.device import Device
from ..types.device_list import DeviceList
from ..types.device_ping_response import DevicePingResponse
-from ..types.device_issue_activation_token_response import DeviceIssueActivationTokenResponse
__all__ = ["DevicesResource", "AsyncDevicesResource"]
@@ -57,6 +56,7 @@ def create(
self,
*,
name: str,
+ expand: List[Literal["current_deployment", "current_release"]] | Omit = omit,
# 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,
@@ -70,6 +70,8 @@ def create(
Args:
name: The name of the device.
+ expand: Fields to expand on the device resource.
+
extra_headers: Send extra headers
extra_query: Add additional query parameters to the request
@@ -82,7 +84,11 @@ def create(
"/devices",
body=maybe_transform({"name": name}, device_create_params.DeviceCreateParams),
options=make_request_options(
- extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=maybe_transform({"expand": expand}, device_create_params.DeviceCreateParams),
),
cast_to=Device,
)
@@ -91,6 +97,7 @@ def retrieve(
self,
device_id: str,
*,
+ expand: List[Literal["current_deployment", "current_release"]] | Omit = omit,
# 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,
@@ -102,6 +109,8 @@ def retrieve(
Get a device by ID.
Args:
+ expand: Fields to expand on the device resource.
+
extra_headers: Send extra headers
extra_query: Add additional query parameters to the request
@@ -115,7 +124,11 @@ def retrieve(
return self._get(
path_template("/devices/{device_id}", device_id=device_id),
options=make_request_options(
- extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=maybe_transform({"expand": expand}, device_retrieve_params.DeviceRetrieveParams),
),
cast_to=Device,
)
@@ -124,6 +137,7 @@ def update(
self,
device_id: str,
*,
+ expand: List[Literal["current_deployment", "current_release"]] | Omit = omit,
name: str | Omit = omit,
# 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.
@@ -132,12 +146,13 @@ def update(
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> Device:
- """Update a device by ID.
+ """
+ Update a device by ID.
Args:
- name: The new name of the device.
+ expand: Fields to expand on the device resource.
- If excluded from the request, the device name is not
+ name: The new name of the device. If excluded from the request, the device name is not
updated.
extra_headers: Send extra headers
@@ -154,7 +169,11 @@ def update(
path_template("/devices/{device_id}", device_id=device_id),
body=maybe_transform({"name": name}, device_update_params.DeviceUpdateParams),
options=make_request_options(
- extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=maybe_transform({"expand": expand}, device_update_params.DeviceUpdateParams),
),
cast_to=Device,
)
@@ -164,7 +183,8 @@ def list(
*,
id: SequenceNotStr[str] | Omit = omit,
agent_version: SequenceNotStr[str] | Omit = omit,
- expand: List[Literal["total_count"]] | Omit = omit,
+ current_release_id: SequenceNotStr[str] | Omit = omit,
+ expand: List[Literal["total_count", "current_deployment", "current_release"]] | Omit = omit,
limit: int | Omit = omit,
name: SequenceNotStr[str] | Omit = omit,
offset: int | Omit = omit,
@@ -184,6 +204,8 @@ def list(
agent_version: The agent versions to filter by.
+ current_release_id: The release IDs to filter devices by their current release.
+
expand: Fields to expand on each device in the list.
limit: The maximum number of items to return. A limit of 15 with an offset of 0 returns
@@ -215,6 +237,7 @@ def list(
{
"id": id,
"agent_version": agent_version,
+ "current_release_id": current_release_id,
"expand": expand,
"limit": limit,
"name": name,
@@ -227,47 +250,6 @@ def list(
cast_to=DeviceList,
)
- def issue_activation_token(
- self,
- device_id: str,
- *,
- allow_reactivation: bool | Omit = omit,
- # 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,
- ) -> DeviceIssueActivationTokenResponse:
- """
- Create a new device activation token.
-
- Args:
- allow_reactivation: Whether this token can reactivate already activated devices. If false, the token
- is unable to activate devices which are already activated.
-
- 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
- """
- if not device_id:
- raise ValueError(f"Expected a non-empty value for `device_id` but received {device_id!r}")
- return self._post(
- path_template("/devices/{device_id}/activation_token", device_id=device_id),
- body=maybe_transform(
- {"allow_reactivation": allow_reactivation},
- device_issue_activation_token_params.DeviceIssueActivationTokenParams,
- ),
- options=make_request_options(
- extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
- ),
- cast_to=DeviceIssueActivationTokenResponse,
- )
-
def ping(
self,
device_id: str,
@@ -331,6 +313,7 @@ async def create(
self,
*,
name: str,
+ expand: List[Literal["current_deployment", "current_release"]] | Omit = omit,
# 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,
@@ -344,6 +327,8 @@ async def create(
Args:
name: The name of the device.
+ expand: Fields to expand on the device resource.
+
extra_headers: Send extra headers
extra_query: Add additional query parameters to the request
@@ -356,7 +341,11 @@ async def create(
"/devices",
body=await async_maybe_transform({"name": name}, device_create_params.DeviceCreateParams),
options=make_request_options(
- extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=await async_maybe_transform({"expand": expand}, device_create_params.DeviceCreateParams),
),
cast_to=Device,
)
@@ -365,6 +354,7 @@ async def retrieve(
self,
device_id: str,
*,
+ expand: List[Literal["current_deployment", "current_release"]] | Omit = omit,
# 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,
@@ -376,6 +366,8 @@ async def retrieve(
Get a device by ID.
Args:
+ expand: Fields to expand on the device resource.
+
extra_headers: Send extra headers
extra_query: Add additional query parameters to the request
@@ -389,7 +381,11 @@ async def retrieve(
return await self._get(
path_template("/devices/{device_id}", device_id=device_id),
options=make_request_options(
- extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=await async_maybe_transform({"expand": expand}, device_retrieve_params.DeviceRetrieveParams),
),
cast_to=Device,
)
@@ -398,6 +394,7 @@ async def update(
self,
device_id: str,
*,
+ expand: List[Literal["current_deployment", "current_release"]] | Omit = omit,
name: str | Omit = omit,
# 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.
@@ -406,12 +403,13 @@ async def update(
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> Device:
- """Update a device by ID.
+ """
+ Update a device by ID.
Args:
- name: The new name of the device.
+ expand: Fields to expand on the device resource.
- If excluded from the request, the device name is not
+ name: The new name of the device. If excluded from the request, the device name is not
updated.
extra_headers: Send extra headers
@@ -428,7 +426,11 @@ async def update(
path_template("/devices/{device_id}", device_id=device_id),
body=await async_maybe_transform({"name": name}, device_update_params.DeviceUpdateParams),
options=make_request_options(
- extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ query=await async_maybe_transform({"expand": expand}, device_update_params.DeviceUpdateParams),
),
cast_to=Device,
)
@@ -438,7 +440,8 @@ async def list(
*,
id: SequenceNotStr[str] | Omit = omit,
agent_version: SequenceNotStr[str] | Omit = omit,
- expand: List[Literal["total_count"]] | Omit = omit,
+ current_release_id: SequenceNotStr[str] | Omit = omit,
+ expand: List[Literal["total_count", "current_deployment", "current_release"]] | Omit = omit,
limit: int | Omit = omit,
name: SequenceNotStr[str] | Omit = omit,
offset: int | Omit = omit,
@@ -458,6 +461,8 @@ async def list(
agent_version: The agent versions to filter by.
+ current_release_id: The release IDs to filter devices by their current release.
+
expand: Fields to expand on each device in the list.
limit: The maximum number of items to return. A limit of 15 with an offset of 0 returns
@@ -489,6 +494,7 @@ async def list(
{
"id": id,
"agent_version": agent_version,
+ "current_release_id": current_release_id,
"expand": expand,
"limit": limit,
"name": name,
@@ -501,47 +507,6 @@ async def list(
cast_to=DeviceList,
)
- async def issue_activation_token(
- self,
- device_id: str,
- *,
- allow_reactivation: bool | Omit = omit,
- # 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,
- ) -> DeviceIssueActivationTokenResponse:
- """
- Create a new device activation token.
-
- Args:
- allow_reactivation: Whether this token can reactivate already activated devices. If false, the token
- is unable to activate devices which are already activated.
-
- 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
- """
- if not device_id:
- raise ValueError(f"Expected a non-empty value for `device_id` but received {device_id!r}")
- return await self._post(
- path_template("/devices/{device_id}/activation_token", device_id=device_id),
- body=await async_maybe_transform(
- {"allow_reactivation": allow_reactivation},
- device_issue_activation_token_params.DeviceIssueActivationTokenParams,
- ),
- options=make_request_options(
- extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
- ),
- cast_to=DeviceIssueActivationTokenResponse,
- )
-
async def ping(
self,
device_id: str,
@@ -597,9 +562,6 @@ def __init__(self, devices: DevicesResource) -> None:
self.list = to_raw_response_wrapper(
devices.list,
)
- self.issue_activation_token = to_raw_response_wrapper(
- devices.issue_activation_token,
- )
self.ping = to_raw_response_wrapper(
devices.ping,
)
@@ -621,9 +583,6 @@ def __init__(self, devices: AsyncDevicesResource) -> None:
self.list = async_to_raw_response_wrapper(
devices.list,
)
- self.issue_activation_token = async_to_raw_response_wrapper(
- devices.issue_activation_token,
- )
self.ping = async_to_raw_response_wrapper(
devices.ping,
)
@@ -645,9 +604,6 @@ def __init__(self, devices: DevicesResource) -> None:
self.list = to_streamed_response_wrapper(
devices.list,
)
- self.issue_activation_token = to_streamed_response_wrapper(
- devices.issue_activation_token,
- )
self.ping = to_streamed_response_wrapper(
devices.ping,
)
@@ -669,9 +625,6 @@ def __init__(self, devices: AsyncDevicesResource) -> None:
self.list = async_to_streamed_response_wrapper(
devices.list,
)
- self.issue_activation_token = async_to_streamed_response_wrapper(
- devices.issue_activation_token,
- )
self.ping = async_to_streamed_response_wrapper(
devices.ping,
)
diff --git a/src/miru_platform_sdk/resources/provisioning_tokens.py b/src/miru_platform_sdk/resources/provisioning_tokens.py
new file mode 100644
index 0000000..a27d49d
--- /dev/null
+++ b/src/miru_platform_sdk/resources/provisioning_tokens.py
@@ -0,0 +1,135 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+import httpx
+
+from .._types import Body, Query, Headers, NotGiven, not_given
+from .._compat import cached_property
+from .._resource import SyncAPIResource, AsyncAPIResource
+from .._response import (
+ to_raw_response_wrapper,
+ to_streamed_response_wrapper,
+ async_to_raw_response_wrapper,
+ async_to_streamed_response_wrapper,
+)
+from .._base_client import make_request_options
+from ..types.provisioning_token import ProvisioningToken
+
+__all__ = ["ProvisioningTokensResource", "AsyncProvisioningTokensResource"]
+
+
+class ProvisioningTokensResource(SyncAPIResource):
+ @cached_property
+ def with_raw_response(self) -> ProvisioningTokensResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/mirurobotics/python-platform-sdk#accessing-raw-response-data-eg-headers
+ """
+ return ProvisioningTokensResourceWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> ProvisioningTokensResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/mirurobotics/python-platform-sdk#with_streaming_response
+ """
+ return ProvisioningTokensResourceWithStreamingResponse(self)
+
+ def create(
+ self,
+ *,
+ # 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,
+ ) -> ProvisioningToken:
+ """Create a new provisioning token."""
+ return self._post(
+ "/provisioning_tokens",
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=ProvisioningToken,
+ )
+
+
+class AsyncProvisioningTokensResource(AsyncAPIResource):
+ @cached_property
+ def with_raw_response(self) -> AsyncProvisioningTokensResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/mirurobotics/python-platform-sdk#accessing-raw-response-data-eg-headers
+ """
+ return AsyncProvisioningTokensResourceWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> AsyncProvisioningTokensResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/mirurobotics/python-platform-sdk#with_streaming_response
+ """
+ return AsyncProvisioningTokensResourceWithStreamingResponse(self)
+
+ async def create(
+ self,
+ *,
+ # 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,
+ ) -> ProvisioningToken:
+ """Create a new provisioning token."""
+ return await self._post(
+ "/provisioning_tokens",
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=ProvisioningToken,
+ )
+
+
+class ProvisioningTokensResourceWithRawResponse:
+ def __init__(self, provisioning_tokens: ProvisioningTokensResource) -> None:
+ self._provisioning_tokens = provisioning_tokens
+
+ self.create = to_raw_response_wrapper(
+ provisioning_tokens.create,
+ )
+
+
+class AsyncProvisioningTokensResourceWithRawResponse:
+ def __init__(self, provisioning_tokens: AsyncProvisioningTokensResource) -> None:
+ self._provisioning_tokens = provisioning_tokens
+
+ self.create = async_to_raw_response_wrapper(
+ provisioning_tokens.create,
+ )
+
+
+class ProvisioningTokensResourceWithStreamingResponse:
+ def __init__(self, provisioning_tokens: ProvisioningTokensResource) -> None:
+ self._provisioning_tokens = provisioning_tokens
+
+ self.create = to_streamed_response_wrapper(
+ provisioning_tokens.create,
+ )
+
+
+class AsyncProvisioningTokensResourceWithStreamingResponse:
+ def __init__(self, provisioning_tokens: AsyncProvisioningTokensResource) -> None:
+ self._provisioning_tokens = provisioning_tokens
+
+ self.create = async_to_streamed_response_wrapper(
+ provisioning_tokens.create,
+ )
diff --git a/src/miru_platform_sdk/types/__init__.py b/src/miru_platform_sdk/types/__init__.py
index 2c52d49..6ca0508 100644
--- a/src/miru_platform_sdk/types/__init__.py
+++ b/src/miru_platform_sdk/types/__init__.py
@@ -2,6 +2,8 @@
from __future__ import annotations
+from . import device, deployment
+from .. import _compat
from .device import Device as Device
from .shared import PaginatedList as PaginatedList
from .release import Release as Release
@@ -21,6 +23,7 @@
from .config_schema_list import ConfigSchemaList as ConfigSchemaList
from .device_list_params import DeviceListParams as DeviceListParams
from .device_ping_params import DevicePingParams as DevicePingParams
+from .provisioning_token import ProvisioningToken as ProvisioningToken
from .release_list_params import ReleaseListParams as ReleaseListParams
from .device_create_params import DeviceCreateParams as DeviceCreateParams
from .device_ping_response import DevicePingResponse as DevicePingResponse
@@ -28,6 +31,7 @@
from .git_commit_ref_param import GitCommitRefParam as GitCommitRefParam
from .release_create_params import ReleaseCreateParams as ReleaseCreateParams
from .deployment_list_params import DeploymentListParams as DeploymentListParams
+from .device_retrieve_params import DeviceRetrieveParams as DeviceRetrieveParams
from .git_commit_list_params import GitCommitListParams as GitCommitListParams
from .instance_content_param import InstanceContentParam as InstanceContentParam
from .config_type_list_params import ConfigTypeListParams as ConfigTypeListParams
@@ -49,7 +53,14 @@
from .deployment_list_drifts_params import DeploymentListDriftsParams as DeploymentListDriftsParams
from .config_instance_download_params import ConfigInstanceDownloadParams as ConfigInstanceDownloadParams
from .config_instance_retrieve_params import ConfigInstanceRetrieveParams as ConfigInstanceRetrieveParams
-from .device_issue_activation_token_params import DeviceIssueActivationTokenParams as DeviceIssueActivationTokenParams
-from .device_issue_activation_token_response import (
- DeviceIssueActivationTokenResponse as DeviceIssueActivationTokenResponse,
-)
+
+# Rebuild cyclical models only after all modules are imported.
+# This ensures that, when building the deferred (due to cyclical references) model schema,
+# Pydantic can resolve the necessary references.
+# See: https://github.com/pydantic/pydantic/issues/11250 for more context.
+if _compat.PYDANTIC_V1:
+ deployment.Deployment.update_forward_refs() # type: ignore
+ device.Device.update_forward_refs() # type: ignore
+else:
+ deployment.Deployment.model_rebuild(_parent_namespace_depth=0)
+ device.Device.model_rebuild(_parent_namespace_depth=0)
diff --git a/src/miru_platform_sdk/types/config_instance.py b/src/miru_platform_sdk/types/config_instance.py
index c911de3..556394b 100644
--- a/src/miru_platform_sdk/types/config_instance.py
+++ b/src/miru_platform_sdk/types/config_instance.py
@@ -29,11 +29,7 @@ class ConfigInstance(BaseModel):
"""The timestamp of when the config instance was created."""
filepath: str
- """
- The file path to deploy the config instance relative to
- `/srv/miru/config_instances`. `v1/motion-control.json` would deploy to
- `/srv/miru/config_instances/v1/motion-control.json`.
- """
+ """The absolute file system path where this config instance is written."""
object: Literal["config_instance"]
"""The object type, which is always `config_instance`."""
diff --git a/src/miru_platform_sdk/types/config_schema.py b/src/miru_platform_sdk/types/config_schema.py
index 87fcd25..6fa2e34 100644
--- a/src/miru_platform_sdk/types/config_schema.py
+++ b/src/miru_platform_sdk/types/config_schema.py
@@ -32,9 +32,8 @@ class ConfigSchema(BaseModel):
instance_filepath: str
"""
- The file path to deploy the config instance relative to
- `/srv/miru/config_instances`. `v1/motion-control.json` would deploy to
- `/srv/miru/config_instances/v1/motion-control.json`.
+ The absolute file system path where config instances for this schema are
+ written.
"""
language: SchemaLanguage
diff --git a/src/miru_platform_sdk/types/config_schema_create_params.py b/src/miru_platform_sdk/types/config_schema_create_params.py
index b801670..e4938f8 100644
--- a/src/miru_platform_sdk/types/config_schema_create_params.py
+++ b/src/miru_platform_sdk/types/config_schema_create_params.py
@@ -34,7 +34,7 @@ class ConfigSchemaCreateParams(TypedDict, total=False):
"""The git commit to link to this config schema."""
instance_filepath: str
- """The file path for config instances created from this schema."""
+ """The absolute file system path config instances for this schema are written to."""
class ConfigTypeRef(TypedDict, total=False):
diff --git a/src/miru_platform_sdk/types/deployment.py b/src/miru_platform_sdk/types/deployment.py
index aca53d2..807913b 100644
--- a/src/miru_platform_sdk/types/deployment.py
+++ b/src/miru_platform_sdk/types/deployment.py
@@ -1,10 +1,11 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+from __future__ import annotations
+
from typing import List, Optional
from datetime import datetime
from typing_extensions import Literal
-from .device import Device
from .release import Release
from .._models import BaseModel
from .config_instance import ConfigInstance
@@ -16,18 +17,25 @@ class Deployment(BaseModel):
id: str
"""ID of the deployment."""
- activity_status: Literal["drifted", "staged", "queued", "deployed", "archived"]
+ activity_status: Literal["drifted", "staged", "queued", "deployed", "removing", "archived"]
"""Last known activity state of the deployment.
- - Drifted: device's configurations have drifted since this deployment was
- staged, and the deployment needs to be reviewed before it can be deployed
- - Staged: is ready to be deployed
- - Queued: the deployment's config instances are waiting to be received by the
- device; will be deployed as soon as the device is online
- - Deployed: the deployment's config instances are currently available for
- consumption on the device
- - Archived: the deployment is available for historical reference but cannot be
- deployed and is not active on the device
+ `drifted` means the device's configurations have drifted since this deployment
+ was staged, and the deployment needs to be reviewed before it can be deployed.
+
+ `staged` means the deployment is ready to be deployed.
+
+ `queued` means the deployment's config instances are waiting to be received by
+ the device and will be deployed as soon as the device is online.
+
+ `deployed` means the deployment's config instances are currently available for
+ consumption on the device.
+
+ `removing` means the deployment's config instances are being removed from the
+ device.
+
+ `archived` means the deployment is available for historical reference but cannot
+ be deployed and is not active on the device.
"""
created_at: datetime
@@ -42,11 +50,13 @@ class Deployment(BaseModel):
error_status: Literal["none", "failed", "retrying"]
"""Last known error state of the deployment.
- - None: no errors
- - Retrying: an error has been encountered and the agent is retrying to reach the
- target status
- - Failed: a fatal error has been encountered; the deployment is archived and (if
- deployed) removed from the device
+ `none` means there are no errors.
+
+ `retrying` means an error has been encountered and the agent is retrying to
+ reach the target status.
+
+ `failed` means a fatal error has been encountered; the deployment is archived
+ and, if deployed, removed from the device.
"""
object: Literal["deployment"]
@@ -55,7 +65,7 @@ class Deployment(BaseModel):
release_id: str
"""ID of the release."""
- status: Literal["drifted", "staged", "queued", "deployed", "archived", "failed", "retrying"]
+ status: Literal["drifted", "staged", "queued", "deployed", "removing", "archived", "failed", "retrying"]
"""
This status merges the 'activity_status' and 'error_status' fields, with error
states taking precedence over activity states when errors are present. For
@@ -67,11 +77,13 @@ class Deployment(BaseModel):
target_status: Literal["staged", "deployed", "archived"]
"""Desired state of the deployment.
- - Staged: is ready to be deployed
- - Deployed: all config instances part of the deployment are available for
- consumption on the device
- - Archived: the deployment is available for historical reference but cannot be
- deployed and is not active on the device
+ `staged` means the deployment is ready to be deployed.
+
+ `deployed` means all config instances in the deployment are available for
+ consumption on the device.
+
+ `archived` means the deployment is available for historical reference but cannot
+ be deployed and is not active on the device.
"""
updated_at: datetime
@@ -82,8 +94,11 @@ class Deployment(BaseModel):
Expand the config instances using 'expand=config_instances' in the query string.
"""
- device: Optional[Device] = None
+ device: Optional["Device"] = None
"""Expand the device using 'expand=device' in the query string."""
release: Optional[Release] = None
"""Expand the release using 'expand=release' in the query string."""
+
+
+from .device import Device
diff --git a/src/miru_platform_sdk/types/deployment_create_params.py b/src/miru_platform_sdk/types/deployment_create_params.py
index 33f0bf6..c7c6f0e 100644
--- a/src/miru_platform_sdk/types/deployment_create_params.py
+++ b/src/miru_platform_sdk/types/deployment_create_params.py
@@ -30,10 +30,11 @@ class DeploymentCreateParams(TypedDict, total=False):
target_status: Required[Literal["staged", "deployed"]]
"""Desired state of the deployment.
- - Staged: ready for deployment. Deployments can only be staged if their release
- is not the current release for the device.
- - Deployed: deployed to the device. Deployments can only be deployed if their
- release is the device's current release.
+ `staged` means the deployment is ready for deployment. Deployments can only be
+ staged if their release is not the device's current release.
+
+ `deployed` means the deployment should be deployed to the device. Deployments
+ can only be deployed if their release is the device's current release.
"""
expand: List[Literal["device", "release", "config_instances"]]
diff --git a/src/miru_platform_sdk/types/deployment_list.py b/src/miru_platform_sdk/types/deployment_list.py
index 7c976c3..792656a 100644
--- a/src/miru_platform_sdk/types/deployment_list.py
+++ b/src/miru_platform_sdk/types/deployment_list.py
@@ -1,13 +1,17 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+from __future__ import annotations
+
from typing import List
-from .deployment import Deployment
from .shared.paginated_list import PaginatedList
__all__ = ["DeploymentList"]
class DeploymentList(PaginatedList):
- data: List[Deployment]
+ data: List["Deployment"]
"""The list of deployments."""
+
+
+from .deployment import Deployment
diff --git a/src/miru_platform_sdk/types/deployment_list_params.py b/src/miru_platform_sdk/types/deployment_list_params.py
index 48cea5d..4df7034 100644
--- a/src/miru_platform_sdk/types/deployment_list_params.py
+++ b/src/miru_platform_sdk/types/deployment_list_params.py
@@ -14,7 +14,7 @@ class DeploymentListParams(TypedDict, total=False):
id: SequenceNotStr[str]
"""The deployment IDs to filter by."""
- activity_status: List[Literal["drifted", "staged", "queued", "deployed", "archived"]]
+ activity_status: List[Literal["drifted", "staged", "queued", "deployed", "removing", "archived"]]
"""The deployment activity statuses to filter by."""
device_id: SequenceNotStr[str]
diff --git a/src/miru_platform_sdk/types/device.py b/src/miru_platform_sdk/types/device.py
index a41be0f..62f6b32 100644
--- a/src/miru_platform_sdk/types/device.py
+++ b/src/miru_platform_sdk/types/device.py
@@ -1,9 +1,12 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+from __future__ import annotations
+
from typing import Optional
from datetime import datetime
from typing_extensions import Literal
+from .release import Release
from .._models import BaseModel
__all__ = ["Device"]
@@ -51,3 +54,12 @@ class Device(BaseModel):
updated_at: datetime
"""Timestamp of when the device was last updated."""
+
+ current_deployment: Optional["Deployment"] = None
+ """The current deployment for the device."""
+
+ current_release: Optional[Release] = None
+ """The current release for the device."""
+
+
+from .deployment import Deployment
diff --git a/src/miru_platform_sdk/types/device_create_params.py b/src/miru_platform_sdk/types/device_create_params.py
index 1f15b9a..76c72af 100644
--- a/src/miru_platform_sdk/types/device_create_params.py
+++ b/src/miru_platform_sdk/types/device_create_params.py
@@ -2,7 +2,8 @@
from __future__ import annotations
-from typing_extensions import Required, TypedDict
+from typing import List
+from typing_extensions import Literal, Required, TypedDict
__all__ = ["DeviceCreateParams"]
@@ -10,3 +11,6 @@
class DeviceCreateParams(TypedDict, total=False):
name: Required[str]
"""The name of the device."""
+
+ expand: List[Literal["current_deployment", "current_release"]]
+ """Fields to expand on the device resource."""
diff --git a/src/miru_platform_sdk/types/device_issue_activation_token_params.py b/src/miru_platform_sdk/types/device_issue_activation_token_params.py
deleted file mode 100644
index ae72909..0000000
--- a/src/miru_platform_sdk/types/device_issue_activation_token_params.py
+++ /dev/null
@@ -1,15 +0,0 @@
-# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-
-from __future__ import annotations
-
-from typing_extensions import TypedDict
-
-__all__ = ["DeviceIssueActivationTokenParams"]
-
-
-class DeviceIssueActivationTokenParams(TypedDict, total=False):
- allow_reactivation: bool
- """Whether this token can reactivate already activated devices.
-
- If false, the token is unable to activate devices which are already activated.
- """
diff --git a/src/miru_platform_sdk/types/device_list.py b/src/miru_platform_sdk/types/device_list.py
index 36ac9ed..a0f11ce 100644
--- a/src/miru_platform_sdk/types/device_list.py
+++ b/src/miru_platform_sdk/types/device_list.py
@@ -1,13 +1,17 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+from __future__ import annotations
+
from typing import List
-from .device import Device
from .shared.paginated_list import PaginatedList
__all__ = ["DeviceList"]
class DeviceList(PaginatedList):
- data: List[Device]
+ data: List["Device"]
"""The list of devices."""
+
+
+from .device import Device
diff --git a/src/miru_platform_sdk/types/device_list_params.py b/src/miru_platform_sdk/types/device_list_params.py
index e172859..fb31fdb 100644
--- a/src/miru_platform_sdk/types/device_list_params.py
+++ b/src/miru_platform_sdk/types/device_list_params.py
@@ -17,7 +17,10 @@ class DeviceListParams(TypedDict, total=False):
agent_version: SequenceNotStr[str]
"""The agent versions to filter by."""
- expand: List[Literal["total_count"]]
+ current_release_id: SequenceNotStr[str]
+ """The release IDs to filter devices by their current release."""
+
+ expand: List[Literal["total_count", "current_deployment", "current_release"]]
"""Fields to expand on each device in the list."""
limit: int
diff --git a/src/miru_platform_sdk/types/device_retrieve_params.py b/src/miru_platform_sdk/types/device_retrieve_params.py
new file mode 100644
index 0000000..da16691
--- /dev/null
+++ b/src/miru_platform_sdk/types/device_retrieve_params.py
@@ -0,0 +1,13 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import List
+from typing_extensions import Literal, TypedDict
+
+__all__ = ["DeviceRetrieveParams"]
+
+
+class DeviceRetrieveParams(TypedDict, total=False):
+ expand: List[Literal["current_deployment", "current_release"]]
+ """Fields to expand on the device resource."""
diff --git a/src/miru_platform_sdk/types/device_update_params.py b/src/miru_platform_sdk/types/device_update_params.py
index edc897d..d014024 100644
--- a/src/miru_platform_sdk/types/device_update_params.py
+++ b/src/miru_platform_sdk/types/device_update_params.py
@@ -2,12 +2,16 @@
from __future__ import annotations
-from typing_extensions import TypedDict
+from typing import List
+from typing_extensions import Literal, TypedDict
__all__ = ["DeviceUpdateParams"]
class DeviceUpdateParams(TypedDict, total=False):
+ expand: List[Literal["current_deployment", "current_release"]]
+ """Fields to expand on the device resource."""
+
name: str
"""The new name of the device.
diff --git a/src/miru_platform_sdk/types/instance_content.py b/src/miru_platform_sdk/types/instance_content.py
index ffc1b2d..30f38ae 100644
--- a/src/miru_platform_sdk/types/instance_content.py
+++ b/src/miru_platform_sdk/types/instance_content.py
@@ -11,4 +11,4 @@ class InstanceContent(BaseModel):
data: str
"""The configuration values associated with the config instance."""
- format: Literal["json"]
+ format: Literal["json", "yaml", "jsonc"]
diff --git a/src/miru_platform_sdk/types/instance_content_param.py b/src/miru_platform_sdk/types/instance_content_param.py
index aef148b..81259c8 100644
--- a/src/miru_platform_sdk/types/instance_content_param.py
+++ b/src/miru_platform_sdk/types/instance_content_param.py
@@ -11,4 +11,4 @@ class InstanceContentParam(TypedDict, total=False):
data: Required[str]
"""The configuration values associated with the config instance."""
- format: Required[Literal["json"]]
+ format: Required[Literal["json", "yaml", "jsonc"]]
diff --git a/src/miru_platform_sdk/types/device_issue_activation_token_response.py b/src/miru_platform_sdk/types/provisioning_token.py
similarity index 61%
rename from src/miru_platform_sdk/types/device_issue_activation_token_response.py
rename to src/miru_platform_sdk/types/provisioning_token.py
index b5cf47d..1593991 100644
--- a/src/miru_platform_sdk/types/device_issue_activation_token_response.py
+++ b/src/miru_platform_sdk/types/provisioning_token.py
@@ -4,12 +4,12 @@
from .._models import BaseModel
-__all__ = ["DeviceIssueActivationTokenResponse"]
+__all__ = ["ProvisioningToken"]
-class DeviceIssueActivationTokenResponse(BaseModel):
+class ProvisioningToken(BaseModel):
token: str
- """The token."""
+ """The provisioning token. This value is only returned when the token is created."""
expires_at: datetime
"""The expiration date and time of the token."""
diff --git a/tests/api_resources/test_config_schemas.py b/tests/api_resources/test_config_schemas.py
index e7e3916..795e78a 100644
--- a/tests/api_resources/test_config_schemas.py
+++ b/tests/api_resources/test_config_schemas.py
@@ -60,7 +60,7 @@ def test_method_create_with_all_params(self, client: Miru) -> None:
},
"schema_filepaths": ["path/to/config/schema/robot1.cue", "path/to/config/schema/robot2.cue"],
},
- instance_filepath="/v1/motion-control.json",
+ instance_filepath="/srv/miru/configs/v1/motion-control.json",
)
assert_matches_type(ConfigSchema, config_schema, path=["response"])
@@ -244,7 +244,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncMiru) -> N
},
"schema_filepaths": ["path/to/config/schema/robot1.cue", "path/to/config/schema/robot2.cue"],
},
- instance_filepath="/v1/motion-control.json",
+ instance_filepath="/srv/miru/configs/v1/motion-control.json",
)
assert_matches_type(ConfigSchema, config_schema, path=["response"])
diff --git a/tests/api_resources/test_devices.py b/tests/api_resources/test_devices.py
index 7b135d1..512eac8 100644
--- a/tests/api_resources/test_devices.py
+++ b/tests/api_resources/test_devices.py
@@ -13,7 +13,6 @@
Device,
DeviceList,
DevicePingResponse,
- DeviceIssueActivationTokenResponse,
)
base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
@@ -30,6 +29,15 @@ def test_method_create(self, client: Miru) -> None:
)
assert_matches_type(Device, device, path=["response"])
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_create_with_all_params(self, client: Miru) -> None:
+ device = client.devices.create(
+ name="Robot 1",
+ expand=["current_release"],
+ )
+ assert_matches_type(Device, device, path=["response"])
+
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
def test_raw_response_create(self, client: Miru) -> None:
@@ -60,7 +68,16 @@ def test_streaming_response_create(self, client: Miru) -> None:
@parametrize
def test_method_retrieve(self, client: Miru) -> None:
device = client.devices.retrieve(
- "dvc_123",
+ device_id="dvc_123",
+ )
+ assert_matches_type(Device, device, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_retrieve_with_all_params(self, client: Miru) -> None:
+ device = client.devices.retrieve(
+ device_id="dvc_123",
+ expand=["current_release"],
)
assert_matches_type(Device, device, path=["response"])
@@ -68,7 +85,7 @@ def test_method_retrieve(self, client: Miru) -> None:
@parametrize
def test_raw_response_retrieve(self, client: Miru) -> None:
response = client.devices.with_raw_response.retrieve(
- "dvc_123",
+ device_id="dvc_123",
)
assert response.is_closed is True
@@ -80,7 +97,7 @@ def test_raw_response_retrieve(self, client: Miru) -> None:
@parametrize
def test_streaming_response_retrieve(self, client: Miru) -> None:
with client.devices.with_streaming_response.retrieve(
- "dvc_123",
+ device_id="dvc_123",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -95,7 +112,7 @@ def test_streaming_response_retrieve(self, client: Miru) -> None:
def test_path_params_retrieve(self, client: Miru) -> None:
with pytest.raises(ValueError, match=r"Expected a non-empty value for `device_id` but received ''"):
client.devices.with_raw_response.retrieve(
- "",
+ device_id="",
)
@pytest.mark.skip(reason="Mock server tests are disabled")
@@ -111,6 +128,7 @@ def test_method_update(self, client: Miru) -> None:
def test_method_update_with_all_params(self, client: Miru) -> None:
device = client.devices.update(
device_id="dvc_123",
+ expand=["current_release"],
name="Robot 1",
)
assert_matches_type(Device, device, path=["response"])
@@ -161,7 +179,8 @@ def test_method_list_with_all_params(self, client: Miru) -> None:
device = client.devices.list(
id=["dev_123"],
agent_version=["v1.0.0"],
- expand=["total_count"],
+ current_release_id=["rls_123"],
+ expand=["current_release"],
limit=10,
name=["My Device"],
offset=0,
@@ -191,57 +210,6 @@ def test_streaming_response_list(self, client: Miru) -> None:
assert cast(Any, response.is_closed) is True
- @pytest.mark.skip(reason="Mock server tests are disabled")
- @parametrize
- def test_method_issue_activation_token(self, client: Miru) -> None:
- device = client.devices.issue_activation_token(
- device_id="dvc_123",
- )
- assert_matches_type(DeviceIssueActivationTokenResponse, device, path=["response"])
-
- @pytest.mark.skip(reason="Mock server tests are disabled")
- @parametrize
- def test_method_issue_activation_token_with_all_params(self, client: Miru) -> None:
- device = client.devices.issue_activation_token(
- device_id="dvc_123",
- allow_reactivation=False,
- )
- assert_matches_type(DeviceIssueActivationTokenResponse, device, path=["response"])
-
- @pytest.mark.skip(reason="Mock server tests are disabled")
- @parametrize
- def test_raw_response_issue_activation_token(self, client: Miru) -> None:
- response = client.devices.with_raw_response.issue_activation_token(
- device_id="dvc_123",
- )
-
- assert response.is_closed is True
- assert response.http_request.headers.get("X-Stainless-Lang") == "python"
- device = response.parse()
- assert_matches_type(DeviceIssueActivationTokenResponse, device, path=["response"])
-
- @pytest.mark.skip(reason="Mock server tests are disabled")
- @parametrize
- def test_streaming_response_issue_activation_token(self, client: Miru) -> None:
- with client.devices.with_streaming_response.issue_activation_token(
- device_id="dvc_123",
- ) as response:
- assert not response.is_closed
- assert response.http_request.headers.get("X-Stainless-Lang") == "python"
-
- device = response.parse()
- assert_matches_type(DeviceIssueActivationTokenResponse, device, path=["response"])
-
- assert cast(Any, response.is_closed) is True
-
- @pytest.mark.skip(reason="Mock server tests are disabled")
- @parametrize
- def test_path_params_issue_activation_token(self, client: Miru) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `device_id` but received ''"):
- client.devices.with_raw_response.issue_activation_token(
- device_id="",
- )
-
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
def test_method_ping(self, client: Miru) -> None:
@@ -302,6 +270,15 @@ async def test_method_create(self, async_client: AsyncMiru) -> None:
)
assert_matches_type(Device, device, path=["response"])
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_create_with_all_params(self, async_client: AsyncMiru) -> None:
+ device = await async_client.devices.create(
+ name="Robot 1",
+ expand=["current_release"],
+ )
+ assert_matches_type(Device, device, path=["response"])
+
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
async def test_raw_response_create(self, async_client: AsyncMiru) -> None:
@@ -332,7 +309,16 @@ async def test_streaming_response_create(self, async_client: AsyncMiru) -> None:
@parametrize
async def test_method_retrieve(self, async_client: AsyncMiru) -> None:
device = await async_client.devices.retrieve(
- "dvc_123",
+ device_id="dvc_123",
+ )
+ assert_matches_type(Device, device, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_retrieve_with_all_params(self, async_client: AsyncMiru) -> None:
+ device = await async_client.devices.retrieve(
+ device_id="dvc_123",
+ expand=["current_release"],
)
assert_matches_type(Device, device, path=["response"])
@@ -340,7 +326,7 @@ async def test_method_retrieve(self, async_client: AsyncMiru) -> None:
@parametrize
async def test_raw_response_retrieve(self, async_client: AsyncMiru) -> None:
response = await async_client.devices.with_raw_response.retrieve(
- "dvc_123",
+ device_id="dvc_123",
)
assert response.is_closed is True
@@ -352,7 +338,7 @@ async def test_raw_response_retrieve(self, async_client: AsyncMiru) -> None:
@parametrize
async def test_streaming_response_retrieve(self, async_client: AsyncMiru) -> None:
async with async_client.devices.with_streaming_response.retrieve(
- "dvc_123",
+ device_id="dvc_123",
) as response:
assert not response.is_closed
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
@@ -367,7 +353,7 @@ async def test_streaming_response_retrieve(self, async_client: AsyncMiru) -> Non
async def test_path_params_retrieve(self, async_client: AsyncMiru) -> None:
with pytest.raises(ValueError, match=r"Expected a non-empty value for `device_id` but received ''"):
await async_client.devices.with_raw_response.retrieve(
- "",
+ device_id="",
)
@pytest.mark.skip(reason="Mock server tests are disabled")
@@ -383,6 +369,7 @@ async def test_method_update(self, async_client: AsyncMiru) -> None:
async def test_method_update_with_all_params(self, async_client: AsyncMiru) -> None:
device = await async_client.devices.update(
device_id="dvc_123",
+ expand=["current_release"],
name="Robot 1",
)
assert_matches_type(Device, device, path=["response"])
@@ -433,7 +420,8 @@ async def test_method_list_with_all_params(self, async_client: AsyncMiru) -> Non
device = await async_client.devices.list(
id=["dev_123"],
agent_version=["v1.0.0"],
- expand=["total_count"],
+ current_release_id=["rls_123"],
+ expand=["current_release"],
limit=10,
name=["My Device"],
offset=0,
@@ -463,57 +451,6 @@ async def test_streaming_response_list(self, async_client: AsyncMiru) -> None:
assert cast(Any, response.is_closed) is True
- @pytest.mark.skip(reason="Mock server tests are disabled")
- @parametrize
- async def test_method_issue_activation_token(self, async_client: AsyncMiru) -> None:
- device = await async_client.devices.issue_activation_token(
- device_id="dvc_123",
- )
- assert_matches_type(DeviceIssueActivationTokenResponse, device, path=["response"])
-
- @pytest.mark.skip(reason="Mock server tests are disabled")
- @parametrize
- async def test_method_issue_activation_token_with_all_params(self, async_client: AsyncMiru) -> None:
- device = await async_client.devices.issue_activation_token(
- device_id="dvc_123",
- allow_reactivation=False,
- )
- assert_matches_type(DeviceIssueActivationTokenResponse, device, path=["response"])
-
- @pytest.mark.skip(reason="Mock server tests are disabled")
- @parametrize
- async def test_raw_response_issue_activation_token(self, async_client: AsyncMiru) -> None:
- response = await async_client.devices.with_raw_response.issue_activation_token(
- device_id="dvc_123",
- )
-
- assert response.is_closed is True
- assert response.http_request.headers.get("X-Stainless-Lang") == "python"
- device = await response.parse()
- assert_matches_type(DeviceIssueActivationTokenResponse, device, path=["response"])
-
- @pytest.mark.skip(reason="Mock server tests are disabled")
- @parametrize
- async def test_streaming_response_issue_activation_token(self, async_client: AsyncMiru) -> None:
- async with async_client.devices.with_streaming_response.issue_activation_token(
- device_id="dvc_123",
- ) as response:
- assert not response.is_closed
- assert response.http_request.headers.get("X-Stainless-Lang") == "python"
-
- device = await response.parse()
- assert_matches_type(DeviceIssueActivationTokenResponse, device, path=["response"])
-
- assert cast(Any, response.is_closed) is True
-
- @pytest.mark.skip(reason="Mock server tests are disabled")
- @parametrize
- async def test_path_params_issue_activation_token(self, async_client: AsyncMiru) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `device_id` but received ''"):
- await async_client.devices.with_raw_response.issue_activation_token(
- device_id="",
- )
-
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
async def test_method_ping(self, async_client: AsyncMiru) -> None:
diff --git a/tests/api_resources/test_provisioning_tokens.py b/tests/api_resources/test_provisioning_tokens.py
new file mode 100644
index 0000000..6d60ef0
--- /dev/null
+++ b/tests/api_resources/test_provisioning_tokens.py
@@ -0,0 +1,80 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+import os
+from typing import Any, cast
+
+import pytest
+
+from tests.utils import assert_matches_type
+from miru_platform_sdk import Miru, AsyncMiru
+from miru_platform_sdk.types import ProvisioningToken
+
+base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
+
+
+class TestProvisioningTokens:
+ parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_create(self, client: Miru) -> None:
+ provisioning_token = client.provisioning_tokens.create()
+ assert_matches_type(ProvisioningToken, provisioning_token, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_create(self, client: Miru) -> None:
+ response = client.provisioning_tokens.with_raw_response.create()
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ provisioning_token = response.parse()
+ assert_matches_type(ProvisioningToken, provisioning_token, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_create(self, client: Miru) -> None:
+ with client.provisioning_tokens.with_streaming_response.create() as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ provisioning_token = response.parse()
+ assert_matches_type(ProvisioningToken, provisioning_token, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+
+class TestAsyncProvisioningTokens:
+ parametrize = pytest.mark.parametrize(
+ "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"]
+ )
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_create(self, async_client: AsyncMiru) -> None:
+ provisioning_token = await async_client.provisioning_tokens.create()
+ assert_matches_type(ProvisioningToken, provisioning_token, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_create(self, async_client: AsyncMiru) -> None:
+ response = await async_client.provisioning_tokens.with_raw_response.create()
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ provisioning_token = await response.parse()
+ assert_matches_type(ProvisioningToken, provisioning_token, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_create(self, async_client: AsyncMiru) -> None:
+ async with async_client.provisioning_tokens.with_streaming_response.create() as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ provisioning_token = await response.parse()
+ assert_matches_type(ProvisioningToken, provisioning_token, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
From b21baec899aae9a318d65a0acacb76f76f946c07 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Sat, 9 May 2026 03:37:47 +0000
Subject: [PATCH 5/9] fix(client): add missing f-string prefix in file type
error message
---
src/miru_platform_sdk/_files.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/miru_platform_sdk/_files.py b/src/miru_platform_sdk/_files.py
index 0fdce17..76da9e0 100644
--- a/src/miru_platform_sdk/_files.py
+++ b/src/miru_platform_sdk/_files.py
@@ -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
From 1aadc67a16bd634bbf368f36c28334365e50c4eb Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 12 May 2026 03:31:05 +0000
Subject: [PATCH 6/9] feat(internal/types): support eagerly validating pydantic
iterators
---
src/miru_platform_sdk/_models.py | 80 ++++++++++++++++++++++++++++++++
tests/test_models.py | 60 ++++++++++++++++++++++--
2 files changed, 137 insertions(+), 3 deletions(-)
diff --git a/src/miru_platform_sdk/_models.py b/src/miru_platform_sdk/_models.py
index e3d6b9b..85670f4 100644
--- a/src/miru_platform_sdk/_models.py
+++ b/src/miru_platform_sdk/_models.py
@@ -25,7 +25,9 @@
ClassVar,
Protocol,
Required,
+ Annotated,
ParamSpec,
+ TypeAlias,
TypedDict,
TypeGuard,
final,
@@ -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"]
@@ -402,6 +412,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)
diff --git a/tests/test_models.py b/tests/test_models.py
index c506e2a..e94b7d5 100644
--- a/tests/test_models.py
+++ b/tests/test_models.py
@@ -1,7 +1,8 @@
import json
-from typing import TYPE_CHECKING, Any, Dict, List, Union, Optional, cast
+from typing import TYPE_CHECKING, Any, Dict, List, Union, Iterable, Optional, cast
from datetime import datetime, timezone
-from typing_extensions import Literal, Annotated, TypeAliasType
+from collections import deque
+from typing_extensions import Literal, Annotated, TypedDict, TypeAliasType
import pytest
import pydantic
@@ -9,7 +10,7 @@
from miru_platform_sdk._utils import PropertyInfo
from miru_platform_sdk._compat import PYDANTIC_V1, parse_obj, model_dump, model_json
-from miru_platform_sdk._models import DISCRIMINATOR_CACHE, BaseModel, construct_type
+from miru_platform_sdk._models import DISCRIMINATOR_CACHE, BaseModel, EagerIterable, construct_type
class BasicModel(BaseModel):
@@ -961,3 +962,56 @@ def __getattr__(self, attr: str) -> Item: ...
assert model.a.prop == 1
assert isinstance(model.a, Item)
assert model.other == "foo"
+
+
+# NOTE: Workaround for Pydantic Iterable behavior.
+# Iterable fields are replaced with a ValidatorIterator and may be consumed
+# during serialization, which can cause subsequent dumps to return empty data.
+# See: https://github.com/pydantic/pydantic/issues/9541
+@pytest.mark.parametrize(
+ "data, expected_validated",
+ [
+ ([1, 2, 3], [1, 2, 3]),
+ ((1, 2, 3), (1, 2, 3)),
+ (set([1, 2, 3]), set([1, 2, 3])),
+ (iter([1, 2, 3]), [1, 2, 3]),
+ ([], []),
+ ((x for x in [1, 2, 3]), [1, 2, 3]),
+ (map(lambda x: x, [1, 2, 3]), [1, 2, 3]),
+ (frozenset([1, 2, 3]), frozenset([1, 2, 3])),
+ (deque([1, 2, 3]), deque([1, 2, 3])),
+ ],
+ ids=["list", "tuple", "set", "iterator", "empty", "generator", "map", "frozenset", "deque"],
+)
+@pytest.mark.skipif(PYDANTIC_V1, reason="this is only supported in pydantic v2")
+def test_iterable_construction(data: Iterable[int], expected_validated: Iterable[int]) -> None:
+ class TypeWithIterable(TypedDict):
+ items: EagerIterable[int]
+
+ class Model(BaseModel):
+ data: TypeWithIterable
+
+ m = Model.model_validate({"data": {"items": data}})
+ assert m.data["items"] == expected_validated
+
+ # Verify repeated dumps don't lose data (the original bug)
+ assert m.model_dump()["data"]["items"] == list(expected_validated)
+ assert m.model_dump()["data"]["items"] == list(expected_validated)
+
+
+@pytest.mark.skipif(PYDANTIC_V1, reason="this is only supported in pydantic v2")
+def test_iterable_construction_str_falls_back_to_list() -> None:
+ # str is iterable (over chars), but str(list_of_chars) produces the list's repr
+ # rather than reconstructing a string from items. We special-case str to fall
+ # back to list instead of attempting reconstruction.
+ class TypeWithIterable(TypedDict):
+ items: EagerIterable[str]
+
+ class Model(BaseModel):
+ data: TypeWithIterable
+
+ m = Model.model_validate({"data": {"items": "hello"}})
+
+ # falls back to list of chars rather than calling str(["h", "e", "l", "l", "o"])
+ assert m.data["items"] == ["h", "e", "l", "l", "o"]
+ assert m.model_dump()["data"]["items"] == ["h", "e", "l", "l", "o"]
From a50302460958f372ffd4c836044a56da9704f186 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 12 May 2026 23:56:49 +0000
Subject: [PATCH 7/9] codegen metadata
---
.stats.yml | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index a0b90df..cb5abbc 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 30
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/miru-ml/miru-platform-ea08d7548c36e9295af05b1f7e60b6e8c15c8c8ceb558449a87f20976f1fc256.yml
-openapi_spec_hash: 6a803b3d8bc3d2030749d4f504392d98
-config_hash: df24eb8211667ea5c8651e1e5e66e441
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/miru-ml/miru-platform-8ef5c42a5e3e5f7c24cabc1a900a5f13ad5b6ff42734bd5dfefa48dec747f6b5.yml
+openapi_spec_hash: a5b8a1a51bddbe5a31445253a6effb7b
+config_hash: 8bf107cbb3d57e7a1704382e9991502c
From 284370341318c5f3c551a1db05cfa6522bf915d0 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 13 May 2026 02:51:50 +0000
Subject: [PATCH 8/9] ci: pin GitHub Actions to commit SHAs
Pin all GitHub Actions referenced in generated workflows (both
first-party `actions/*` and third-party) to immutable commit SHAs.
Updating pinned actions is now a deliberate codegen-side bump rather
than implicit on every workflow run.
---
.github/workflows/ci.yml | 14 +++++++-------
.github/workflows/publish-pypi.yml | 4 ++--
.github/workflows/release-doctor.yml | 2 +-
3 files changed, 10 insertions(+), 10 deletions(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index ff9ba82..189abbb 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -21,10 +21,10 @@ jobs:
runs-on: ${{ github.repository == 'stainless-sdks/miru-platform-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
if: (github.event_name == 'push' || github.event.pull_request.head.repo.fork) && (github.event_name != 'push' || github.event.head_commit.message != 'codegen metadata')
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Install uv
- uses: astral-sh/setup-uv@v5
+ uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2
with:
version: '0.10.2'
@@ -43,10 +43,10 @@ jobs:
id-token: write
runs-on: ${{ github.repository == 'stainless-sdks/miru-platform-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Install uv
- uses: astral-sh/setup-uv@v5
+ uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2
with:
version: '0.10.2'
@@ -61,7 +61,7 @@ jobs:
github.repository == 'stainless-sdks/miru-platform-python' &&
!startsWith(github.ref, 'refs/heads/stl/')
id: github-oidc
- uses: actions/github-script@v8
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
script: core.setOutput('github_token', await core.getIDToken());
@@ -81,10 +81,10 @@ jobs:
runs-on: ${{ github.repository == 'stainless-sdks/miru-platform-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
if: github.event_name == 'push' || github.event.pull_request.head.repo.fork
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Install uv
- uses: astral-sh/setup-uv@v5
+ uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2
with:
version: '0.10.2'
diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml
index 6729ce6..146229b 100644
--- a/.github/workflows/publish-pypi.yml
+++ b/.github/workflows/publish-pypi.yml
@@ -17,10 +17,10 @@ jobs:
id-token: write
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Install uv
- uses: astral-sh/setup-uv@v5
+ uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2
with:
version: '0.9.13'
diff --git a/.github/workflows/release-doctor.yml b/.github/workflows/release-doctor.yml
index c6f5c01..ce3154c 100644
--- a/.github/workflows/release-doctor.yml
+++ b/.github/workflows/release-doctor.yml
@@ -12,7 +12,7 @@ jobs:
if: github.repository == 'mirurobotics/python-platform-sdk' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || startsWith(github.head_ref, 'release-please') || github.head_ref == 'next')
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Check release environment
run: |
From 570388918ab7087789da185de3746a543e196b84 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 13 May 2026 03:52:59 +0000
Subject: [PATCH 9/9] release: 0.10.0
---
.release-please-manifest.json | 2 +-
CHANGELOG.md | 19 +++++++++++++++++++
pyproject.toml | 2 +-
src/miru_platform_sdk/_version.py | 2 +-
4 files changed, 22 insertions(+), 3 deletions(-)
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index 6d78745..091cfb1 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.9.0"
+ ".": "0.10.0"
}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 93d2bce..cc26f52 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,24 @@
# Changelog
+## 0.10.0 (2026-05-13)
+
+Full Changelog: [v0.9.0...v0.10.0](https://github.com/mirurobotics/python-platform-sdk/compare/v0.9.0...v0.10.0)
+
+### Features
+
+* **api:** upgrade to 2026-05-06.rainier-beta.3 ([2dbcc65](https://github.com/mirurobotics/python-platform-sdk/commit/2dbcc652ba6c201d997a87c6bbf9810cb8c5eabb))
+* **internal/types:** support eagerly validating pydantic iterators ([1aadc67](https://github.com/mirurobotics/python-platform-sdk/commit/1aadc67a16bd634bbf368f36c28334365e50c4eb))
+
+
+### Bug Fixes
+
+* **client:** add missing f-string prefix in file type error message ([b21baec](https://github.com/mirurobotics/python-platform-sdk/commit/b21baec899aae9a318d65a0acacb76f76f946c07))
+
+
+### Chores
+
+* **internal:** reformat pyproject.toml ([bd0e40e](https://github.com/mirurobotics/python-platform-sdk/commit/bd0e40e96fecdfa1a46e4cff6260aadcde3aaf0e))
+
## 0.9.0 (2026-04-28)
Full Changelog: [v0.8.2...v0.9.0](https://github.com/mirurobotics/python-platform-sdk/compare/v0.8.2...v0.9.0)
diff --git a/pyproject.toml b/pyproject.toml
index f9b2cc1..e154c05 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "miru-platform-sdk"
-version = "0.9.0"
+version = "0.10.0"
description = "The official Python library for the miru API"
dynamic = ["readme"]
license = "MIT"
diff --git a/src/miru_platform_sdk/_version.py b/src/miru_platform_sdk/_version.py
index 4b492c7..b4b9d6c 100644
--- a/src/miru_platform_sdk/_version.py
+++ b/src/miru_platform_sdk/_version.py
@@ -1,4 +1,4 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
__title__ = "miru_platform_sdk"
-__version__ = "0.9.0" # x-release-please-version
+__version__ = "0.10.0" # x-release-please-version