From 7f2543425b84bcabfdb7168c76f3887900cb39db Mon Sep 17 00:00:00 2001
From: "onepin-pipeline-bot[bot]"
<290953255+onepin-pipeline-bot[bot]@users.noreply.github.com>
Date: Fri, 18 Sep 2026 05:34:08 +0000
Subject: [PATCH 1/6] feat: sync SDK to OnePin API v0.41.135
---
.spec-sha | 2 +-
src/onepin/.fern/metadata.json | 6 +-
src/onepin/reference.md | 103 +++++++++++++++--
src/onepin/workflows/client.py | 73 ++++++++----
src/onepin/workflows/raw_client.py | 65 +++++++----
src/onepin/workflows/runs/client.py | 37 ++++--
src/onepin/workflows/runs/raw_client.py | 34 ++++--
src/onepin/workspaces/client.py | 91 +++++++++++++++
src/onepin/workspaces/raw_client.py | 147 ++++++++++++++++++++++++
9 files changed, 477 insertions(+), 81 deletions(-)
diff --git a/.spec-sha b/.spec-sha
index 4b93ca9..11ffc11 100644
--- a/.spec-sha
+++ b/.spec-sha
@@ -1 +1 @@
-33699d2e8a7c89bc778445e9ea3883ee985b20ad
+52e9ae2174a1de4a317c43929df1fe44a7c7697e
diff --git a/src/onepin/.fern/metadata.json b/src/onepin/.fern/metadata.json
index 8b4ef6f..1cc26f6 100644
--- a/src/onepin/.fern/metadata.json
+++ b/src/onepin/.fern/metadata.json
@@ -30,8 +30,8 @@
}
]
},
- "originGitCommit": "c4e8a48ae3bc09abd9ae19460bf56c49a1fe71c6",
- "originGitCommitIsDirty": true,
+ "originGitCommit": "403fef02d6d53ab8076d8023d3c42789add4b5ed",
+ "originGitCommitIsDirty": false,
"invokedBy": "ci",
- "ciProvider": "unknown"
+ "ciProvider": "github"
}
\ No newline at end of file
diff --git a/src/onepin/reference.md b/src/onepin/reference.md
index 8bde4f2..01630c0 100644
--- a/src/onepin/reference.md
+++ b/src/onepin/reference.md
@@ -5033,6 +5033,88 @@ client.workspaces.get_workspace_subscription(
+
+
+
+
+client.workspaces.get_workspace_credits(...) -> ApiResponseBalanceResponse
+
+-
+
+#### 📝 Description
+
+
+-
+
+
+-
+
+Return the credit balance governing this workspace — the source the Usage/credit card reads.
+
+Same shape as ``/users/me/credits``. For an **organization** workspace this is the organization's
+shared credit pool — the balance a member's runs debit; for a **personal** workspace it is the
+workspace owner's balance. Readable by any workspace member (the sensitive billing menu — payment
+methods and subscription lifecycle — is gated separately). ``balance``/``remaining`` are the
+spendable pool, ``used`` is consumption in the current billing period, ``plan_grant`` is the plan's
+monthly allowance, and ``period_end`` is the next expected reset boundary (or null when none is
+promised). For an annual subscriber this GET may perform idempotent period maintenance before
+returning; retries remain safe. Membership is existence-hidden (404); the org-admin fallback applies.
+
+
+
+
+
+#### 🔌 Usage
+
+
+-
+
+
+-
+
+```python
+from onepin import OnePinClient
+from onepin.environment import OnePinClientEnvironment
+
+client = OnePinClient(
+ token="",
+ environment=OnePinClientEnvironment.PROD,
+)
+
+client.workspaces.get_workspace_credits(
+ workspace_id="workspace_id",
+)
+
+```
+
+
+
+
+
+#### ⚙️ Parameters
+
+
+-
+
+
+-
+
+**workspace_id:** `str`
+
+
+
+
+
+-
+
+**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration.
+
+
+
+
+
+
+
@@ -7289,7 +7371,7 @@ lifecycle.
```python
-from onepin import OnePinClient, WorkflowRunStartIn
+from onepin import OnePinClient
from onepin.environment import OnePinClientEnvironment
client = OnePinClient(
@@ -7299,7 +7381,6 @@ client = OnePinClient(
client.workflows.estimate_workflow(
workflow_id="workflow_id",
- request=WorkflowRunStartIn(),
)
```
@@ -7324,7 +7405,7 @@ client.workflows.estimate_workflow(
-
-**workspace_id:** `typing.Optional[str]`
+**request:** `WorkflowRunStartIn`
@@ -7332,7 +7413,7 @@ client.workflows.estimate_workflow(
-
-**request:** `typing.Optional[WorkflowRunStartIn]`
+**workspace_id:** `typing.Optional[str]`
@@ -7388,7 +7469,7 @@ confirm the expected cost. Equivalent to `POST /estimate`.
```python
-from onepin import OnePinClient, WorkflowRunStartIn
+from onepin import OnePinClient
from onepin.environment import OnePinClientEnvironment
client = OnePinClient(
@@ -7398,7 +7479,6 @@ client = OnePinClient(
client.workflows.preview_run(
workflow_id="workflow_id",
- request=WorkflowRunStartIn(),
)
```
@@ -7423,7 +7503,7 @@ client.workflows.preview_run(
-
-**workspace_id:** `typing.Optional[str]`
+**request:** `WorkflowRunStartIn`
@@ -7431,7 +7511,7 @@ client.workflows.preview_run(
-
-**request:** `typing.Optional[WorkflowRunStartIn]`
+**workspace_id:** `typing.Optional[str]`
@@ -8951,7 +9031,7 @@ Requires at least `editor` role in the workspace; viewers cannot run workflows.
```python
-from onepin import OnePinClient, WorkflowRunStartIn
+from onepin import OnePinClient
from onepin.environment import OnePinClientEnvironment
client = OnePinClient(
@@ -8961,7 +9041,6 @@ client = OnePinClient(
client.workflows.runs.start(
workflow_id="workflow_id",
- request=WorkflowRunStartIn(),
)
```
@@ -8986,7 +9065,7 @@ client.workflows.runs.start(
-
-**workspace_id:** `typing.Optional[str]`
+**request:** `WorkflowRunStartIn`
@@ -8994,7 +9073,7 @@ client.workflows.runs.start(
-
-**request:** `typing.Optional[WorkflowRunStartIn]`
+**workspace_id:** `typing.Optional[str]`
diff --git a/src/onepin/workflows/client.py b/src/onepin/workflows/client.py
index 55dae99..78da03d 100644
--- a/src/onepin/workflows/client.py
+++ b/src/onepin/workflows/client.py
@@ -24,7 +24,6 @@
from ..types.workflow_definition_input import WorkflowDefinitionInput
from ..types.workflow_list_status import WorkflowListStatus
from ..types.workflow_run_data_response import WorkflowRunDataResponse
-from ..types.workflow_run_start_in import WorkflowRunStartIn
from .raw_client import AsyncRawWorkflowsClient, RawWorkflowsClient
from .types.list_workflows_request_order_item import ListWorkflowsRequestOrderItem
from .types.list_workflows_request_sort_item import ListWorkflowsRequestSortItem
@@ -627,7 +626,8 @@ def estimate_workflow(
workflow_id: str,
*,
workspace_id: typing.Optional[str] = None,
- request: typing.Optional[WorkflowRunStartIn] = None,
+ script_text: typing.Optional[str] = OMIT,
+ source_language: typing.Optional[str] = OMIT,
request_options: typing.Optional[RequestOptions] = None,
) -> ApiResponseEstimateResponse:
"""
@@ -648,7 +648,11 @@ def estimate_workflow(
workspace_id : typing.Optional[str]
- request : typing.Optional[WorkflowRunStartIn]
+ script_text : typing.Optional[str]
+ Run this workflow with this script text instead of the text saved in the workflow's source_script node. Applied to the run's definition snapshot only.
+
+ source_language : typing.Optional[str]
+ BCP-47 language of script_text (e.g. en-us). Optional; when omitted the saved source_language (or automatic detection) applies.
request_options : typing.Optional[RequestOptions]
Request-specific configuration.
@@ -660,18 +664,21 @@ def estimate_workflow(
Examples
--------
- from onepin import OnePinClient, WorkflowRunStartIn
+ from onepin import OnePinClient
client = OnePinClient(
token="YOUR_TOKEN",
)
client.workflows.estimate_workflow(
workflow_id="workflow_id",
- request=WorkflowRunStartIn(),
)
"""
_response = self._raw_client.estimate_workflow(
- workflow_id, workspace_id=workspace_id, request=request, request_options=request_options
+ workflow_id,
+ workspace_id=workspace_id,
+ script_text=script_text,
+ source_language=source_language,
+ request_options=request_options,
)
return _response.data
@@ -680,7 +687,8 @@ def preview_run(
workflow_id: str,
*,
workspace_id: typing.Optional[str] = None,
- request: typing.Optional[WorkflowRunStartIn] = None,
+ script_text: typing.Optional[str] = OMIT,
+ source_language: typing.Optional[str] = OMIT,
request_options: typing.Optional[RequestOptions] = None,
) -> ApiResponseEstimateResponse:
"""
@@ -701,7 +709,11 @@ def preview_run(
workspace_id : typing.Optional[str]
- request : typing.Optional[WorkflowRunStartIn]
+ script_text : typing.Optional[str]
+ Run this workflow with this script text instead of the text saved in the workflow's source_script node. Applied to the run's definition snapshot only.
+
+ source_language : typing.Optional[str]
+ BCP-47 language of script_text (e.g. en-us). Optional; when omitted the saved source_language (or automatic detection) applies.
request_options : typing.Optional[RequestOptions]
Request-specific configuration.
@@ -713,18 +725,21 @@ def preview_run(
Examples
--------
- from onepin import OnePinClient, WorkflowRunStartIn
+ from onepin import OnePinClient
client = OnePinClient(
token="YOUR_TOKEN",
)
client.workflows.preview_run(
workflow_id="workflow_id",
- request=WorkflowRunStartIn(),
)
"""
_response = self._raw_client.preview_run(
- workflow_id, workspace_id=workspace_id, request=request, request_options=request_options
+ workflow_id,
+ workspace_id=workspace_id,
+ script_text=script_text,
+ source_language=source_language,
+ request_options=request_options,
)
return _response.data
@@ -2143,7 +2158,8 @@ async def estimate_workflow(
workflow_id: str,
*,
workspace_id: typing.Optional[str] = None,
- request: typing.Optional[WorkflowRunStartIn] = None,
+ script_text: typing.Optional[str] = OMIT,
+ source_language: typing.Optional[str] = OMIT,
request_options: typing.Optional[RequestOptions] = None,
) -> ApiResponseEstimateResponse:
"""
@@ -2164,7 +2180,11 @@ async def estimate_workflow(
workspace_id : typing.Optional[str]
- request : typing.Optional[WorkflowRunStartIn]
+ script_text : typing.Optional[str]
+ Run this workflow with this script text instead of the text saved in the workflow's source_script node. Applied to the run's definition snapshot only.
+
+ source_language : typing.Optional[str]
+ BCP-47 language of script_text (e.g. en-us). Optional; when omitted the saved source_language (or automatic detection) applies.
request_options : typing.Optional[RequestOptions]
Request-specific configuration.
@@ -2178,7 +2198,7 @@ async def estimate_workflow(
--------
import asyncio
- from onepin import AsyncOnePinClient, WorkflowRunStartIn
+ from onepin import AsyncOnePinClient
client = AsyncOnePinClient(
token="YOUR_TOKEN",
@@ -2188,14 +2208,17 @@ async def estimate_workflow(
async def main() -> None:
await client.workflows.estimate_workflow(
workflow_id="workflow_id",
- request=WorkflowRunStartIn(),
)
asyncio.run(main())
"""
_response = await self._raw_client.estimate_workflow(
- workflow_id, workspace_id=workspace_id, request=request, request_options=request_options
+ workflow_id,
+ workspace_id=workspace_id,
+ script_text=script_text,
+ source_language=source_language,
+ request_options=request_options,
)
return _response.data
@@ -2204,7 +2227,8 @@ async def preview_run(
workflow_id: str,
*,
workspace_id: typing.Optional[str] = None,
- request: typing.Optional[WorkflowRunStartIn] = None,
+ script_text: typing.Optional[str] = OMIT,
+ source_language: typing.Optional[str] = OMIT,
request_options: typing.Optional[RequestOptions] = None,
) -> ApiResponseEstimateResponse:
"""
@@ -2225,7 +2249,11 @@ async def preview_run(
workspace_id : typing.Optional[str]
- request : typing.Optional[WorkflowRunStartIn]
+ script_text : typing.Optional[str]
+ Run this workflow with this script text instead of the text saved in the workflow's source_script node. Applied to the run's definition snapshot only.
+
+ source_language : typing.Optional[str]
+ BCP-47 language of script_text (e.g. en-us). Optional; when omitted the saved source_language (or automatic detection) applies.
request_options : typing.Optional[RequestOptions]
Request-specific configuration.
@@ -2239,7 +2267,7 @@ async def preview_run(
--------
import asyncio
- from onepin import AsyncOnePinClient, WorkflowRunStartIn
+ from onepin import AsyncOnePinClient
client = AsyncOnePinClient(
token="YOUR_TOKEN",
@@ -2249,14 +2277,17 @@ async def preview_run(
async def main() -> None:
await client.workflows.preview_run(
workflow_id="workflow_id",
- request=WorkflowRunStartIn(),
)
asyncio.run(main())
"""
_response = await self._raw_client.preview_run(
- workflow_id, workspace_id=workspace_id, request=request, request_options=request_options
+ workflow_id,
+ workspace_id=workspace_id,
+ script_text=script_text,
+ source_language=source_language,
+ request_options=request_options,
)
return _response.data
diff --git a/src/onepin/workflows/raw_client.py b/src/onepin/workflows/raw_client.py
index d532b11..e7bd5b6 100644
--- a/src/onepin/workflows/raw_client.py
+++ b/src/onepin/workflows/raw_client.py
@@ -33,7 +33,6 @@
from ..types.workflow_definition_input import WorkflowDefinitionInput
from ..types.workflow_list_status import WorkflowListStatus
from ..types.workflow_run_data_response import WorkflowRunDataResponse
-from ..types.workflow_run_start_in import WorkflowRunStartIn
from .types.list_workflows_request_order_item import ListWorkflowsRequestOrderItem
from .types.list_workflows_request_sort_item import ListWorkflowsRequestSortItem
from pydantic import ValidationError
@@ -845,7 +844,8 @@ def estimate_workflow(
workflow_id: str,
*,
workspace_id: typing.Optional[str] = None,
- request: typing.Optional[WorkflowRunStartIn] = None,
+ script_text: typing.Optional[str] = OMIT,
+ source_language: typing.Optional[str] = OMIT,
request_options: typing.Optional[RequestOptions] = None,
) -> HttpResponse[ApiResponseEstimateResponse]:
"""
@@ -866,7 +866,11 @@ def estimate_workflow(
workspace_id : typing.Optional[str]
- request : typing.Optional[WorkflowRunStartIn]
+ script_text : typing.Optional[str]
+ Run this workflow with this script text instead of the text saved in the workflow's source_script node. Applied to the run's definition snapshot only.
+
+ source_language : typing.Optional[str]
+ BCP-47 language of script_text (e.g. en-us). Optional; when omitted the saved source_language (or automatic detection) applies.
request_options : typing.Optional[RequestOptions]
Request-specific configuration.
@@ -879,9 +883,10 @@ def estimate_workflow(
_response = self._client_wrapper.httpx_client.request(
f"api/v1/workflows/{encode_path_param(workflow_id)}/estimate",
method="POST",
- json=convert_and_respect_annotation_metadata(
- object_=request, annotation=typing.Optional[WorkflowRunStartIn], direction="write"
- ),
+ json={
+ "script_text": script_text,
+ "source_language": source_language,
+ },
headers={
"content-type": "application/json",
"X-Workspace-Id": str(workspace_id) if workspace_id is not None else None,
@@ -924,7 +929,8 @@ def preview_run(
workflow_id: str,
*,
workspace_id: typing.Optional[str] = None,
- request: typing.Optional[WorkflowRunStartIn] = None,
+ script_text: typing.Optional[str] = OMIT,
+ source_language: typing.Optional[str] = OMIT,
request_options: typing.Optional[RequestOptions] = None,
) -> HttpResponse[ApiResponseEstimateResponse]:
"""
@@ -945,7 +951,11 @@ def preview_run(
workspace_id : typing.Optional[str]
- request : typing.Optional[WorkflowRunStartIn]
+ script_text : typing.Optional[str]
+ Run this workflow with this script text instead of the text saved in the workflow's source_script node. Applied to the run's definition snapshot only.
+
+ source_language : typing.Optional[str]
+ BCP-47 language of script_text (e.g. en-us). Optional; when omitted the saved source_language (or automatic detection) applies.
request_options : typing.Optional[RequestOptions]
Request-specific configuration.
@@ -958,9 +968,10 @@ def preview_run(
_response = self._client_wrapper.httpx_client.request(
f"api/v1/workflows/{encode_path_param(workflow_id)}/runs/preview",
method="POST",
- json=convert_and_respect_annotation_metadata(
- object_=request, annotation=typing.Optional[WorkflowRunStartIn], direction="write"
- ),
+ json={
+ "script_text": script_text,
+ "source_language": source_language,
+ },
headers={
"content-type": "application/json",
"X-Workspace-Id": str(workspace_id) if workspace_id is not None else None,
@@ -2862,7 +2873,8 @@ async def estimate_workflow(
workflow_id: str,
*,
workspace_id: typing.Optional[str] = None,
- request: typing.Optional[WorkflowRunStartIn] = None,
+ script_text: typing.Optional[str] = OMIT,
+ source_language: typing.Optional[str] = OMIT,
request_options: typing.Optional[RequestOptions] = None,
) -> AsyncHttpResponse[ApiResponseEstimateResponse]:
"""
@@ -2883,7 +2895,11 @@ async def estimate_workflow(
workspace_id : typing.Optional[str]
- request : typing.Optional[WorkflowRunStartIn]
+ script_text : typing.Optional[str]
+ Run this workflow with this script text instead of the text saved in the workflow's source_script node. Applied to the run's definition snapshot only.
+
+ source_language : typing.Optional[str]
+ BCP-47 language of script_text (e.g. en-us). Optional; when omitted the saved source_language (or automatic detection) applies.
request_options : typing.Optional[RequestOptions]
Request-specific configuration.
@@ -2896,9 +2912,10 @@ async def estimate_workflow(
_response = await self._client_wrapper.httpx_client.request(
f"api/v1/workflows/{encode_path_param(workflow_id)}/estimate",
method="POST",
- json=convert_and_respect_annotation_metadata(
- object_=request, annotation=typing.Optional[WorkflowRunStartIn], direction="write"
- ),
+ json={
+ "script_text": script_text,
+ "source_language": source_language,
+ },
headers={
"content-type": "application/json",
"X-Workspace-Id": str(workspace_id) if workspace_id is not None else None,
@@ -2941,7 +2958,8 @@ async def preview_run(
workflow_id: str,
*,
workspace_id: typing.Optional[str] = None,
- request: typing.Optional[WorkflowRunStartIn] = None,
+ script_text: typing.Optional[str] = OMIT,
+ source_language: typing.Optional[str] = OMIT,
request_options: typing.Optional[RequestOptions] = None,
) -> AsyncHttpResponse[ApiResponseEstimateResponse]:
"""
@@ -2962,7 +2980,11 @@ async def preview_run(
workspace_id : typing.Optional[str]
- request : typing.Optional[WorkflowRunStartIn]
+ script_text : typing.Optional[str]
+ Run this workflow with this script text instead of the text saved in the workflow's source_script node. Applied to the run's definition snapshot only.
+
+ source_language : typing.Optional[str]
+ BCP-47 language of script_text (e.g. en-us). Optional; when omitted the saved source_language (or automatic detection) applies.
request_options : typing.Optional[RequestOptions]
Request-specific configuration.
@@ -2975,9 +2997,10 @@ async def preview_run(
_response = await self._client_wrapper.httpx_client.request(
f"api/v1/workflows/{encode_path_param(workflow_id)}/runs/preview",
method="POST",
- json=convert_and_respect_annotation_metadata(
- object_=request, annotation=typing.Optional[WorkflowRunStartIn], direction="write"
- ),
+ json={
+ "script_text": script_text,
+ "source_language": source_language,
+ },
headers={
"content-type": "application/json",
"X-Workspace-Id": str(workspace_id) if workspace_id is not None else None,
diff --git a/src/onepin/workflows/runs/client.py b/src/onepin/workflows/runs/client.py
index 58c7d08..8f6a24b 100644
--- a/src/onepin/workflows/runs/client.py
+++ b/src/onepin/workflows/runs/client.py
@@ -10,7 +10,6 @@
from ...types.api_response_workflow_run_out import ApiResponseWorkflowRunOut
from ...types.api_response_workflow_run_status_out import ApiResponseWorkflowRunStatusOut
from ...types.node_type import NodeType
-from ...types.workflow_run_start_in import WorkflowRunStartIn
from .raw_client import AsyncRawRunsClient, RawRunsClient
from .types.list_runs_request_order import ListRunsRequestOrder
from .types.list_runs_request_sort import ListRunsRequestSort
@@ -125,7 +124,8 @@ def start(
workflow_id: str,
*,
workspace_id: typing.Optional[str] = None,
- request: typing.Optional[WorkflowRunStartIn] = None,
+ script_text: typing.Optional[str] = OMIT,
+ source_language: typing.Optional[str] = OMIT,
request_options: typing.Optional[RequestOptions] = None,
) -> ApiResponseWorkflowRunOut:
"""
@@ -158,7 +158,11 @@ def start(
workspace_id : typing.Optional[str]
- request : typing.Optional[WorkflowRunStartIn]
+ script_text : typing.Optional[str]
+ Run this workflow with this script text instead of the text saved in the workflow's source_script node. Applied to the run's definition snapshot only.
+
+ source_language : typing.Optional[str]
+ BCP-47 language of script_text (e.g. en-us). Optional; when omitted the saved source_language (or automatic detection) applies.
request_options : typing.Optional[RequestOptions]
Request-specific configuration.
@@ -170,18 +174,21 @@ def start(
Examples
--------
- from onepin import OnePinClient, WorkflowRunStartIn
+ from onepin import OnePinClient
client = OnePinClient(
token="YOUR_TOKEN",
)
client.workflows.runs.start(
workflow_id="workflow_id",
- request=WorkflowRunStartIn(),
)
"""
_response = self._raw_client.start(
- workflow_id, workspace_id=workspace_id, request=request, request_options=request_options
+ workflow_id,
+ workspace_id=workspace_id,
+ script_text=script_text,
+ source_language=source_language,
+ request_options=request_options,
)
return _response.data
@@ -553,7 +560,8 @@ async def start(
workflow_id: str,
*,
workspace_id: typing.Optional[str] = None,
- request: typing.Optional[WorkflowRunStartIn] = None,
+ script_text: typing.Optional[str] = OMIT,
+ source_language: typing.Optional[str] = OMIT,
request_options: typing.Optional[RequestOptions] = None,
) -> ApiResponseWorkflowRunOut:
"""
@@ -586,7 +594,11 @@ async def start(
workspace_id : typing.Optional[str]
- request : typing.Optional[WorkflowRunStartIn]
+ script_text : typing.Optional[str]
+ Run this workflow with this script text instead of the text saved in the workflow's source_script node. Applied to the run's definition snapshot only.
+
+ source_language : typing.Optional[str]
+ BCP-47 language of script_text (e.g. en-us). Optional; when omitted the saved source_language (or automatic detection) applies.
request_options : typing.Optional[RequestOptions]
Request-specific configuration.
@@ -600,7 +612,7 @@ async def start(
--------
import asyncio
- from onepin import AsyncOnePinClient, WorkflowRunStartIn
+ from onepin import AsyncOnePinClient
client = AsyncOnePinClient(
token="YOUR_TOKEN",
@@ -610,14 +622,17 @@ async def start(
async def main() -> None:
await client.workflows.runs.start(
workflow_id="workflow_id",
- request=WorkflowRunStartIn(),
)
asyncio.run(main())
"""
_response = await self._raw_client.start(
- workflow_id, workspace_id=workspace_id, request=request, request_options=request_options
+ workflow_id,
+ workspace_id=workspace_id,
+ script_text=script_text,
+ source_language=source_language,
+ request_options=request_options,
)
return _response.data
diff --git a/src/onepin/workflows/runs/raw_client.py b/src/onepin/workflows/runs/raw_client.py
index 9c57899..0599dae 100644
--- a/src/onepin/workflows/runs/raw_client.py
+++ b/src/onepin/workflows/runs/raw_client.py
@@ -10,7 +10,6 @@
from ...core.parse_error import ParsingError
from ...core.pydantic_utilities import parse_obj_as
from ...core.request_options import RequestOptions
-from ...core.serialization import convert_and_respect_annotation_metadata
from ...errors.unprocessable_entity_error import UnprocessableEntityError
from ...types.api_counted_list_response_workflow_run_list_item import ApiCountedListResponseWorkflowRunListItem
from ...types.api_response_list_workflow_run_step_out import ApiResponseListWorkflowRunStepOut
@@ -18,7 +17,6 @@
from ...types.api_response_workflow_run_out import ApiResponseWorkflowRunOut
from ...types.api_response_workflow_run_status_out import ApiResponseWorkflowRunStatusOut
from ...types.node_type import NodeType
-from ...types.workflow_run_start_in import WorkflowRunStartIn
from .types.list_runs_request_order import ListRunsRequestOrder
from .types.list_runs_request_sort import ListRunsRequestSort
from pydantic import ValidationError
@@ -144,7 +142,8 @@ def start(
workflow_id: str,
*,
workspace_id: typing.Optional[str] = None,
- request: typing.Optional[WorkflowRunStartIn] = None,
+ script_text: typing.Optional[str] = OMIT,
+ source_language: typing.Optional[str] = OMIT,
request_options: typing.Optional[RequestOptions] = None,
) -> HttpResponse[ApiResponseWorkflowRunOut]:
"""
@@ -177,7 +176,11 @@ def start(
workspace_id : typing.Optional[str]
- request : typing.Optional[WorkflowRunStartIn]
+ script_text : typing.Optional[str]
+ Run this workflow with this script text instead of the text saved in the workflow's source_script node. Applied to the run's definition snapshot only.
+
+ source_language : typing.Optional[str]
+ BCP-47 language of script_text (e.g. en-us). Optional; when omitted the saved source_language (or automatic detection) applies.
request_options : typing.Optional[RequestOptions]
Request-specific configuration.
@@ -190,9 +193,10 @@ def start(
_response = self._client_wrapper.httpx_client.request(
f"api/v1/workflows/{encode_path_param(workflow_id)}/runs",
method="POST",
- json=convert_and_respect_annotation_metadata(
- object_=request, annotation=typing.Optional[WorkflowRunStartIn], direction="write"
- ),
+ json={
+ "script_text": script_text,
+ "source_language": source_language,
+ },
headers={
"content-type": "application/json",
"X-Workspace-Id": str(workspace_id) if workspace_id is not None else None,
@@ -684,7 +688,8 @@ async def start(
workflow_id: str,
*,
workspace_id: typing.Optional[str] = None,
- request: typing.Optional[WorkflowRunStartIn] = None,
+ script_text: typing.Optional[str] = OMIT,
+ source_language: typing.Optional[str] = OMIT,
request_options: typing.Optional[RequestOptions] = None,
) -> AsyncHttpResponse[ApiResponseWorkflowRunOut]:
"""
@@ -717,7 +722,11 @@ async def start(
workspace_id : typing.Optional[str]
- request : typing.Optional[WorkflowRunStartIn]
+ script_text : typing.Optional[str]
+ Run this workflow with this script text instead of the text saved in the workflow's source_script node. Applied to the run's definition snapshot only.
+
+ source_language : typing.Optional[str]
+ BCP-47 language of script_text (e.g. en-us). Optional; when omitted the saved source_language (or automatic detection) applies.
request_options : typing.Optional[RequestOptions]
Request-specific configuration.
@@ -730,9 +739,10 @@ async def start(
_response = await self._client_wrapper.httpx_client.request(
f"api/v1/workflows/{encode_path_param(workflow_id)}/runs",
method="POST",
- json=convert_and_respect_annotation_metadata(
- object_=request, annotation=typing.Optional[WorkflowRunStartIn], direction="write"
- ),
+ json={
+ "script_text": script_text,
+ "source_language": source_language,
+ },
headers={
"content-type": "application/json",
"X-Workspace-Id": str(workspace_id) if workspace_id is not None else None,
diff --git a/src/onepin/workspaces/client.py b/src/onepin/workspaces/client.py
index c4fdcbb..367f245 100644
--- a/src/onepin/workspaces/client.py
+++ b/src/onepin/workspaces/client.py
@@ -5,6 +5,7 @@
from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper
from ..core.request_options import RequestOptions
from ..types.api_list_response_workspace_out import ApiListResponseWorkspaceOut
+from ..types.api_response_balance_response import ApiResponseBalanceResponse
from ..types.api_response_checkout_response import ApiResponseCheckoutResponse
from ..types.api_response_dict import ApiResponseDict
from ..types.api_response_plan_limits import ApiResponsePlanLimits
@@ -422,6 +423,47 @@ def get_workspace_subscription(
_response = self._raw_client.get_workspace_subscription(workspace_id, request_options=request_options)
return _response.data
+ def get_workspace_credits(
+ self, workspace_id: str, *, request_options: typing.Optional[RequestOptions] = None
+ ) -> ApiResponseBalanceResponse:
+ """
+ Return the credit balance governing this workspace — the source the Usage/credit card reads.
+
+ Same shape as ``/users/me/credits``. For an **organization** workspace this is the organization's
+ shared credit pool — the balance a member's runs debit; for a **personal** workspace it is the
+ workspace owner's balance. Readable by any workspace member (the sensitive billing menu — payment
+ methods and subscription lifecycle — is gated separately). ``balance``/``remaining`` are the
+ spendable pool, ``used`` is consumption in the current billing period, ``plan_grant`` is the plan's
+ monthly allowance, and ``period_end`` is the next expected reset boundary (or null when none is
+ promised). For an annual subscriber this GET may perform idempotent period maintenance before
+ returning; retries remain safe. Membership is existence-hidden (404); the org-admin fallback applies.
+
+ Parameters
+ ----------
+ workspace_id : str
+
+ request_options : typing.Optional[RequestOptions]
+ Request-specific configuration.
+
+ Returns
+ -------
+ ApiResponseBalanceResponse
+ Successful Response
+
+ Examples
+ --------
+ from onepin import OnePinClient
+
+ client = OnePinClient(
+ token="YOUR_TOKEN",
+ )
+ client.workspaces.get_workspace_credits(
+ workspace_id="workspace_id",
+ )
+ """
+ _response = self._raw_client.get_workspace_credits(workspace_id, request_options=request_options)
+ return _response.data
+
def create_workspace_org_checkout(
self,
workspace_id: str,
@@ -949,6 +991,55 @@ async def main() -> None:
_response = await self._raw_client.get_workspace_subscription(workspace_id, request_options=request_options)
return _response.data
+ async def get_workspace_credits(
+ self, workspace_id: str, *, request_options: typing.Optional[RequestOptions] = None
+ ) -> ApiResponseBalanceResponse:
+ """
+ Return the credit balance governing this workspace — the source the Usage/credit card reads.
+
+ Same shape as ``/users/me/credits``. For an **organization** workspace this is the organization's
+ shared credit pool — the balance a member's runs debit; for a **personal** workspace it is the
+ workspace owner's balance. Readable by any workspace member (the sensitive billing menu — payment
+ methods and subscription lifecycle — is gated separately). ``balance``/``remaining`` are the
+ spendable pool, ``used`` is consumption in the current billing period, ``plan_grant`` is the plan's
+ monthly allowance, and ``period_end`` is the next expected reset boundary (or null when none is
+ promised). For an annual subscriber this GET may perform idempotent period maintenance before
+ returning; retries remain safe. Membership is existence-hidden (404); the org-admin fallback applies.
+
+ Parameters
+ ----------
+ workspace_id : str
+
+ request_options : typing.Optional[RequestOptions]
+ Request-specific configuration.
+
+ Returns
+ -------
+ ApiResponseBalanceResponse
+ Successful Response
+
+ Examples
+ --------
+ import asyncio
+
+ from onepin import AsyncOnePinClient
+
+ client = AsyncOnePinClient(
+ token="YOUR_TOKEN",
+ )
+
+
+ async def main() -> None:
+ await client.workspaces.get_workspace_credits(
+ workspace_id="workspace_id",
+ )
+
+
+ asyncio.run(main())
+ """
+ _response = await self._raw_client.get_workspace_credits(workspace_id, request_options=request_options)
+ return _response.data
+
async def create_workspace_org_checkout(
self,
workspace_id: str,
diff --git a/src/onepin/workspaces/raw_client.py b/src/onepin/workspaces/raw_client.py
index 2db6f1b..5855970 100644
--- a/src/onepin/workspaces/raw_client.py
+++ b/src/onepin/workspaces/raw_client.py
@@ -17,6 +17,7 @@
from ..errors.unprocessable_entity_error import UnprocessableEntityError
from ..types.api_error_response import ApiErrorResponse
from ..types.api_list_response_workspace_out import ApiListResponseWorkspaceOut
+from ..types.api_response_balance_response import ApiResponseBalanceResponse
from ..types.api_response_checkout_response import ApiResponseCheckoutResponse
from ..types.api_response_dict import ApiResponseDict
from ..types.api_response_plan_limits import ApiResponsePlanLimits
@@ -666,6 +667,79 @@ def get_workspace_subscription(
)
raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json)
+ def get_workspace_credits(
+ self, workspace_id: str, *, request_options: typing.Optional[RequestOptions] = None
+ ) -> HttpResponse[ApiResponseBalanceResponse]:
+ """
+ Return the credit balance governing this workspace — the source the Usage/credit card reads.
+
+ Same shape as ``/users/me/credits``. For an **organization** workspace this is the organization's
+ shared credit pool — the balance a member's runs debit; for a **personal** workspace it is the
+ workspace owner's balance. Readable by any workspace member (the sensitive billing menu — payment
+ methods and subscription lifecycle — is gated separately). ``balance``/``remaining`` are the
+ spendable pool, ``used`` is consumption in the current billing period, ``plan_grant`` is the plan's
+ monthly allowance, and ``period_end`` is the next expected reset boundary (or null when none is
+ promised). For an annual subscriber this GET may perform idempotent period maintenance before
+ returning; retries remain safe. Membership is existence-hidden (404); the org-admin fallback applies.
+
+ Parameters
+ ----------
+ workspace_id : str
+
+ request_options : typing.Optional[RequestOptions]
+ Request-specific configuration.
+
+ Returns
+ -------
+ HttpResponse[ApiResponseBalanceResponse]
+ Successful Response
+ """
+ _response = self._client_wrapper.httpx_client.request(
+ f"api/v1/workspaces/{encode_path_param(workspace_id)}/credits",
+ method="GET",
+ request_options=request_options,
+ )
+ try:
+ if 200 <= _response.status_code < 300:
+ _data = typing.cast(
+ ApiResponseBalanceResponse,
+ parse_obj_as(
+ type_=ApiResponseBalanceResponse, # type: ignore
+ object_=_response.json(),
+ ),
+ )
+ return HttpResponse(response=_response, data=_data)
+ if _response.status_code == 404:
+ raise NotFoundError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ typing.Any,
+ parse_obj_as(
+ type_=typing.Any, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 422:
+ raise UnprocessableEntityError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ typing.Any,
+ parse_obj_as(
+ type_=typing.Any, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ _response_json = _response.json()
+ except JSONDecodeError:
+ raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text)
+ except ValidationError as e:
+ raise ParsingError(
+ status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e
+ )
+ raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json)
+
def create_workspace_org_checkout(
self,
workspace_id: str,
@@ -1427,6 +1501,79 @@ async def get_workspace_subscription(
)
raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json)
+ async def get_workspace_credits(
+ self, workspace_id: str, *, request_options: typing.Optional[RequestOptions] = None
+ ) -> AsyncHttpResponse[ApiResponseBalanceResponse]:
+ """
+ Return the credit balance governing this workspace — the source the Usage/credit card reads.
+
+ Same shape as ``/users/me/credits``. For an **organization** workspace this is the organization's
+ shared credit pool — the balance a member's runs debit; for a **personal** workspace it is the
+ workspace owner's balance. Readable by any workspace member (the sensitive billing menu — payment
+ methods and subscription lifecycle — is gated separately). ``balance``/``remaining`` are the
+ spendable pool, ``used`` is consumption in the current billing period, ``plan_grant`` is the plan's
+ monthly allowance, and ``period_end`` is the next expected reset boundary (or null when none is
+ promised). For an annual subscriber this GET may perform idempotent period maintenance before
+ returning; retries remain safe. Membership is existence-hidden (404); the org-admin fallback applies.
+
+ Parameters
+ ----------
+ workspace_id : str
+
+ request_options : typing.Optional[RequestOptions]
+ Request-specific configuration.
+
+ Returns
+ -------
+ AsyncHttpResponse[ApiResponseBalanceResponse]
+ Successful Response
+ """
+ _response = await self._client_wrapper.httpx_client.request(
+ f"api/v1/workspaces/{encode_path_param(workspace_id)}/credits",
+ method="GET",
+ request_options=request_options,
+ )
+ try:
+ if 200 <= _response.status_code < 300:
+ _data = typing.cast(
+ ApiResponseBalanceResponse,
+ parse_obj_as(
+ type_=ApiResponseBalanceResponse, # type: ignore
+ object_=_response.json(),
+ ),
+ )
+ return AsyncHttpResponse(response=_response, data=_data)
+ if _response.status_code == 404:
+ raise NotFoundError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ typing.Any,
+ parse_obj_as(
+ type_=typing.Any, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 422:
+ raise UnprocessableEntityError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ typing.Any,
+ parse_obj_as(
+ type_=typing.Any, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ _response_json = _response.json()
+ except JSONDecodeError:
+ raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text)
+ except ValidationError as e:
+ raise ParsingError(
+ status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e
+ )
+ raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json)
+
async def create_workspace_org_checkout(
self,
workspace_id: str,
From e39f42e5c67b6d4ceae71638ed6a119bea4a278e Mon Sep 17 00:00:00 2001
From: kj-podonos
Date: Mon, 21 Sep 2026 13:47:41 +0900
Subject: [PATCH 2/6] test(cli): expect empty json body for override-free
preview run
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The run-input body was inlined upstream (script_text/source_language are now
OMIT-defaulted kwargs on start/preview/estimate instead of a wrapped request
object). An override-free call therefore serializes to `{}` rather than no body
at all. The endpoint's Body(default_factory=WorkflowRunStartIn) treats `{}` and
an absent body identically, so this is wire-compatible — update the assertion
(and the test name) to match the regenerated client.
Co-Authored-By: Claude Opus 4.8 (1M context)
---
tests/cli/test_cli_composites.py | 8 ++++++--
1 file changed, 6 insertions(+), 2 deletions(-)
diff --git a/tests/cli/test_cli_composites.py b/tests/cli/test_cli_composites.py
index 999207f..a27b978 100644
--- a/tests/cli/test_cli_composites.py
+++ b/tests/cli/test_cli_composites.py
@@ -217,13 +217,17 @@ def test_body_matches_the_run_byte_for_byte(self, tmp_home) -> None:
assert preview.calls[0].request.content == run.calls[0].request.content
@respx.mock
- def test_no_overrides_sends_no_body(self, tmp_home) -> None:
+ def test_no_overrides_sends_empty_body(self, tmp_home) -> None:
route = respx.post("https://api.onepin.ai/api/v1/workflows/wf-1/runs/preview").mock(
return_value=httpx.Response(200, json={"data": _ESTIMATE, "meta": _META_JSON})
)
result = runner.invoke(app, ["--api-key", "op_live_x", "workflows", "preview-run", "wf-1", "--json"])
assert result.exit_code == 0, result.output
- assert route.calls[0].request.content == b""
+ # With the run-input body inlined (script_text/source_language as OMIT-defaulted
+ # kwargs), an override-free call serializes to an empty JSON object rather than no
+ # body at all. The endpoint's `Body(default_factory=WorkflowRunStartIn)` treats `{}`
+ # and an absent body identically (both mean "no run-scoped overrides").
+ assert route.calls[0].request.content == b"{}"
@respx.mock
def test_text_output_renders_the_credits(self, tmp_home) -> None:
From 5b6952c36d759601e5b82ea7d3a983219b4e5589 Mon Sep 17 00:00:00 2001
From: kj-podonos
Date: Mon, 21 Sep 2026 15:37:03 +0900
Subject: [PATCH 3/6] fix(cli): align run overrides with flattened SDK
---
.spec-sha | 2 +-
src/onepin/_cli/commands/composites.py | 22 +++++-------
src/onepin/reference.md | 36 +++++++++++++++----
tests/build/test_sdk_contract.py | 50 +++++++++++++++-----------
tests/cli/test_cli_composites.py | 20 ++++++-----
5 files changed, 79 insertions(+), 51 deletions(-)
diff --git a/.spec-sha b/.spec-sha
index 11ffc11..3d6a10f 100644
--- a/.spec-sha
+++ b/.spec-sha
@@ -1 +1 @@
-52e9ae2174a1de4a317c43929df1fe44a7c7697e
+5415372bbf4091e12103b7ed203402afeca64df6
diff --git a/src/onepin/_cli/commands/composites.py b/src/onepin/_cli/commands/composites.py
index 4b4f55a..8c2eec0 100644
--- a/src/onepin/_cli/commands/composites.py
+++ b/src/onepin/_cli/commands/composites.py
@@ -47,9 +47,7 @@ def workflow_run(
with api_errors(json_on):
client = get_client()
kwargs = _maybe_workspace(client.workflows.runs.start)
- overrides = _run_scoped_body(script, source_language)
- if overrides is not None:
- kwargs["request_options"] = overrides
+ kwargs.update(_run_scoped_kwargs(script, source_language))
started = client.workflows.runs.start(workflow_id, **kwargs)
run = to_jsonable(getattr(started, "data", started))
run_id = run.get("id") if isinstance(run, dict) else None
@@ -142,9 +140,7 @@ def workflow_preview_run(
with api_errors(json_on):
client = get_client()
kwargs = _maybe_workspace(client.workflows.preview_run)
- overrides = _run_scoped_body(script, source_language)
- if overrides is not None:
- kwargs["request_options"] = overrides
+ kwargs.update(_run_scoped_kwargs(script, source_language))
resp = client.workflows.preview_run(workflow_id, **kwargs)
payload = to_jsonable(getattr(resp, "data", resp))
if json_on:
@@ -870,24 +866,22 @@ def definition_schema(
# === Shared helpers ======================================================================
-def _run_scoped_body(script: Optional[str], source_language: Optional[str]) -> Optional[dict[str, Any]]:
- """Build ``request_options`` carrying the run-scoped overrides, or ``None`` for neither.
+def _run_scoped_kwargs(script: Optional[str], source_language: Optional[str]) -> dict[str, str]:
+ """Typed run-scoped overrides for ``runs.start`` / ``preview_run``, omitting what wasn't passed.
``workflows run`` and ``workflows preview-run`` share this so an estimate prices the exact
body the run will send — two builders could drift, and a cost quoted from a different body
than the one charged is precisely the failure these overrides exist to prevent.
- Sent as additional body parameters rather than a ``WorkflowRunStartIn``: the generated model
- defaults both fields to ``None``, so Fern serializes the unset one as an explicit
- ``"source_language": null`` instead of omitting it. Building the dict keeps only the keys the
- user actually passed, which is the wire format this endpoint has always been called with.
+ The SDK now accepts ``script_text`` / ``source_language`` as ``OMIT``-defaulted keyword args,
+ so passing only the keys the user supplied yields the same wire body without the explicit
+ ``"source_language": null`` the old wrapped-model path would have serialized.
"""
- overrides = {
+ return {
key: value
for key, value in (("script_text", script), ("source_language", source_language))
if value is not None
}
- return {"additional_body_parameters": overrides} if overrides else None
def _maybe_workspace(method: Any) -> dict[str, Any]:
diff --git a/src/onepin/reference.md b/src/onepin/reference.md
index 01630c0..3299f91 100644
--- a/src/onepin/reference.md
+++ b/src/onepin/reference.md
@@ -7405,7 +7405,7 @@ client.workflows.estimate_workflow(
-
-**request:** `WorkflowRunStartIn`
+**workspace_id:** `typing.Optional[str]`
@@ -7413,7 +7413,15 @@ client.workflows.estimate_workflow(
-
-**workspace_id:** `typing.Optional[str]`
+**script_text:** `typing.Optional[str]` — Run this workflow with this script text instead of the text saved in the workflow's source_script node. Applied to the run's definition snapshot only.
+
+
+
+
+
+-
+
+**source_language:** `typing.Optional[str]` — BCP-47 language of script_text (e.g. en-us). Optional; when omitted the saved source_language (or automatic detection) applies.
@@ -7503,7 +7511,7 @@ client.workflows.preview_run(
-
-**request:** `WorkflowRunStartIn`
+**workspace_id:** `typing.Optional[str]`
@@ -7511,7 +7519,15 @@ client.workflows.preview_run(
-
-**workspace_id:** `typing.Optional[str]`
+**script_text:** `typing.Optional[str]` — Run this workflow with this script text instead of the text saved in the workflow's source_script node. Applied to the run's definition snapshot only.
+
+
+
+
+
+-
+
+**source_language:** `typing.Optional[str]` — BCP-47 language of script_text (e.g. en-us). Optional; when omitted the saved source_language (or automatic detection) applies.
@@ -9065,7 +9081,7 @@ client.workflows.runs.start(
-
-**request:** `WorkflowRunStartIn`
+**workspace_id:** `typing.Optional[str]`
@@ -9073,7 +9089,15 @@ client.workflows.runs.start(
-
-**workspace_id:** `typing.Optional[str]`
+**script_text:** `typing.Optional[str]` — Run this workflow with this script text instead of the text saved in the workflow's source_script node. Applied to the run's definition snapshot only.
+
+
+
+
+
+-
+
+**source_language:** `typing.Optional[str]` — BCP-47 language of script_text (e.g. en-us). Optional; when omitted the saved source_language (or automatic detection) applies.
diff --git a/tests/build/test_sdk_contract.py b/tests/build/test_sdk_contract.py
index 4cc65f7..829c9d1 100644
--- a/tests/build/test_sdk_contract.py
+++ b/tests/build/test_sdk_contract.py
@@ -29,6 +29,8 @@
from __future__ import annotations
import inspect
+import re
+from pathlib import Path
from typing import get_args
import pytest
@@ -272,27 +274,15 @@ def test_allowlist_has_no_stale_entries() -> None:
"workflows.duplicate_workflow": frozenset({"workflow_id", "workspace_id"}),
"workflows.get": frozenset({"workflow_id", "workspace_id"}),
"workflows.patch_workflow": frozenset({"workflow_id", "workspace_id", "name", "definition"}),
- "workflows.preview_run": frozenset({"workflow_id", "workspace_id", "request_options"}),
- "workflows.runs.start": frozenset({"workflow_id", "workspace_id", "request_options"}),
+ "workflows.preview_run": frozenset({"workflow_id", "workspace_id", "script_text", "source_language"}),
+ "workflows.runs.start": frozenset({"workflow_id", "workspace_id", "script_text", "source_language"}),
"workflows.runs.status": frozenset({"workflow_id", "run_id", "workspace_id"}),
}
-# Same contract as _INTENTIONALLY_UNEXPOSED, keyed by dotted SDK path.
-_COMPOSITE_UNEXPOSED: dict[str, dict[str, str]] = {
- "workflows.runs.start": {
- "request": (
- "Run-scoped overrides ride as request_options.additional_body_parameters instead: "
- "the generated WorkflowRunStartIn defaults both fields to None, so Fern would "
- "serialize the unset one as an explicit null (see composites._run_scoped_body)."
- ),
- },
- "workflows.preview_run": {
- "request": (
- "Built by the same composites._run_scoped_body as workflows.runs.start, so the "
- "estimate prices the byte-identical body the run will send."
- ),
- },
-}
+# Same contract as _INTENTIONALLY_UNEXPOSED, keyed by dotted SDK path. The run-input body
+# is now inlined (script_text/source_language are OMIT-defaulted kwargs), so both composites
+# pass them directly via composites._run_scoped_kwargs — nothing is left unexposed here.
+_COMPOSITE_UNEXPOSED: dict[str, dict[str, str]] = {}
def _resolve_composite_method(dotted: str):
@@ -371,9 +361,6 @@ def test_composite_calls_covers_every_sdk_call_site() -> None:
call sites out of the source keeps a newly added composite from slipping past the three
checks above the same way `duplicate`/`preview-run` slipped past the TABLE ones.
"""
- import re
- from pathlib import Path
-
import onepin._cli.commands.composites as composites_module
source = Path(composites_module.__file__).read_text(encoding="utf-8")
@@ -390,3 +377,24 @@ def test_composite_calls_covers_every_sdk_call_site() -> None:
unused = set(_COMPOSITE_CALLS) - called
assert not unused, f"_COMPOSITE_CALLS lists {sorted(unused)}, which composites.py no longer calls — drop it"
+
+
+@pytest.mark.parametrize(
+ "method_path",
+ ("workflows.estimate_workflow", "workflows.preview_run", "workflows.runs.start"),
+)
+def test_run_input_reference_matches_flattened_signatures(method_path: str) -> None:
+ """The generated reference must expose the same flattened run-input kwargs as the clients."""
+ reference = (Path(__file__).parents[2] / "src" / "onepin" / "reference.md").read_text(encoding="utf-8")
+ method_name = method_path.rsplit(".", 1)[-1]
+ owner = method_path.removesuffix(f".{method_name}")
+ marker = rf"client\.{re.escape(owner)}(?:\.]+>)?{method_name}\(\.\.\.\)"
+ match = re.search(marker, reference)
+ assert match is not None, f"reference.md has no section for {method_path}"
+ section = reference[match.start() : reference.index("
", match.start())]
+
+ assert "**script_text:**" in section, f"{method_path} reference omits script_text"
+ assert "**source_language:**" in section, f"{method_path} reference omits source_language"
+ assert "**request:** `WorkflowRunStartIn`" not in section, (
+ f"{method_path} reference still documents the stale wrapped request model"
+ )
diff --git a/tests/cli/test_cli_composites.py b/tests/cli/test_cli_composites.py
index a27b978..5eed6c0 100644
--- a/tests/cli/test_cli_composites.py
+++ b/tests/cli/test_cli_composites.py
@@ -133,7 +133,7 @@ def test_watch_timeout_exits_1(self, patch_client, monkeypatch, tmp_home) -> Non
assert result.exit_code == 1
assert "Timed out" in result.output
- def test_script_flags_ride_as_body_params(self, patch_client, tmp_home) -> None:
+ def test_script_flags_are_forwarded_as_typed_kwargs(self, patch_client, tmp_home) -> None:
client = _WfClient(["running"])
patch_client(client)
result = runner.invoke(
@@ -151,24 +151,26 @@ def test_script_flags_ride_as_body_params(self, patch_client, tmp_home) -> None:
],
)
assert result.exit_code == 0, result.output
- assert client.workflows.runs.start_kwargs["request_options"] == {
- "additional_body_parameters": {"script_text": "Hello world!", "source_language": "en-us"}
- }
+ assert client.workflows.runs.start_kwargs["script_text"] == "Hello world!"
+ assert client.workflows.runs.start_kwargs["source_language"] == "en-us"
+ assert "request_options" not in client.workflows.runs.start_kwargs
def test_script_flag_alone_sends_only_script_text(self, patch_client, tmp_home) -> None:
client = _WfClient(["running"])
patch_client(client)
result = runner.invoke(app, ["--api-key", "op_live_x", "workflows", "run", "wf-1", "--script", "Hi"])
assert result.exit_code == 0, result.output
- assert client.workflows.runs.start_kwargs["request_options"] == {
- "additional_body_parameters": {"script_text": "Hi"}
- }
+ assert client.workflows.runs.start_kwargs["script_text"] == "Hi"
+ assert "source_language" not in client.workflows.runs.start_kwargs
+ assert "request_options" not in client.workflows.runs.start_kwargs
- def test_no_script_flags_sends_no_request_options(self, patch_client, tmp_home) -> None:
+ def test_no_script_flags_forwards_no_typed_overrides(self, patch_client, tmp_home) -> None:
client = _WfClient(["running"])
patch_client(client)
result = runner.invoke(app, ["--api-key", "op_live_x", "workflows", "run", "wf-1"])
assert result.exit_code == 0, result.output
+ assert "script_text" not in client.workflows.runs.start_kwargs
+ assert "source_language" not in client.workflows.runs.start_kwargs
assert "request_options" not in client.workflows.runs.start_kwargs
@@ -187,7 +189,7 @@ def test_no_script_flags_sends_no_request_options(self, patch_client, tmp_home)
class TestWorkflowPreviewRun:
- """The estimate must price the body the run will actually send (see _run_scoped_body)."""
+ """The estimate must price the body the run will actually send (see _run_scoped_kwargs)."""
@respx.mock
def test_script_rides_in_the_body(self, tmp_home) -> None:
From 3e5555d0a9562711b3ad86ae71e8704ce632f8df Mon Sep 17 00:00:00 2001
From: kj-podonos
Date: Mon, 21 Sep 2026 20:45:53 +0900
Subject: [PATCH 4/6] feat: sync SDK to OnePin API v0.41.137
---
.spec-sha | 2 +-
src/onepin/.fern/metadata.json | 7 ++-
src/onepin/README.md | 30 +++++++++--
src/onepin/reference.md | 37 +++-----------
src/onepin/templates/client.py | 31 ++++++++----
src/onepin/templates/raw_client.py | 47 ++++++++++++++---
src/onepin/voices/client.py | 31 ++++++++----
src/onepin/voices/raw_client.py | 67 ++++++++++++++++++++++---
src/onepin/workflows/client.py | 31 ++++++++----
src/onepin/workflows/raw_client.py | 57 ++++++++++++++++++---
src/onepin/workflows/runs/client.py | 31 ++++++++----
src/onepin/workflows/runs/raw_client.py | 51 ++++++++++++++++---
12 files changed, 311 insertions(+), 111 deletions(-)
diff --git a/.spec-sha b/.spec-sha
index 3d6a10f..d984310 100644
--- a/.spec-sha
+++ b/.spec-sha
@@ -1 +1 @@
-5415372bbf4091e12103b7ed203402afeca64df6
+6e8f07d1484ca4c4b976609f61c1d88b8288b3a3
diff --git a/src/onepin/.fern/metadata.json b/src/onepin/.fern/metadata.json
index 1cc26f6..0dbb2dc 100644
--- a/src/onepin/.fern/metadata.json
+++ b/src/onepin/.fern/metadata.json
@@ -30,8 +30,7 @@
}
]
},
- "originGitCommit": "403fef02d6d53ab8076d8023d3c42789add4b5ed",
- "originGitCommitIsDirty": false,
- "invokedBy": "ci",
- "ciProvider": "github"
+ "originGitCommit": "3d712ffd87d0eab81a87aa8c0e603081a11ee5f1",
+ "originGitCommitIsDirty": true,
+ "invokedBy": "manual"
}
\ No newline at end of file
diff --git a/src/onepin/README.md b/src/onepin/README.md
index 8e57350..040f91a 100644
--- a/src/onepin/README.md
+++ b/src/onepin/README.md
@@ -1,9 +1,9 @@
-# OnePin Python Library
+# Onepin Python Library
-[](https://buildwithfern.com?utm_source=github&utm_medium=github&utm_campaign=readme&utm_source=OnePin%2FPython)
+[](https://buildwithfern.com?utm_source=github&utm_medium=github&utm_campaign=readme&utm_source=Onepin%2FPython)
[](https://pypi.python.org/pypi/onepin)
-The OnePin Python library provides convenient access to the OnePin APIs from Python.
+The Onepin Python library provides convenient access to the Onepin APIs from Python.
## Table of Contents
@@ -13,6 +13,7 @@ The OnePin Python library provides convenient access to the OnePin APIs from Pyt
- [Environments](#environments)
- [Async Client](#async-client)
- [Exception Handling](#exception-handling)
+- [Pagination](#pagination)
- [Advanced](#advanced)
- [Access Raw Response Data](#access-raw-response-data)
- [Retries](#retries)
@@ -101,6 +102,29 @@ except ApiError as e:
print(e.body)
```
+## Pagination
+
+Paginated requests will return a `SyncPager` or `AsyncPager`, which can be used as generators for the underlying object.
+
+```python
+from onepin import OnePinClient
+
+client = OnePinClient(
+ token="",
+)
+
+client.templates.list()
+```
+
+```python
+# You can also iterate through pages and access the typed response per page
+pager = client.templates.list(...)
+for page in pager.iter_pages():
+ print(page.response) # access the typed response for each page
+ for item in page:
+ print(item)
+```
+
## Advanced
### Access Raw Response Data
diff --git a/src/onepin/reference.md b/src/onepin/reference.md
index 3299f91..8b09392 100644
--- a/src/onepin/reference.md
+++ b/src/onepin/reference.md
@@ -7405,7 +7405,7 @@ client.workflows.estimate_workflow(
-
-**workspace_id:** `typing.Optional[str]`
+**request:** `WorkflowRunStartIn`
@@ -7413,15 +7413,7 @@ client.workflows.estimate_workflow(
-
-**script_text:** `typing.Optional[str]` — Run this workflow with this script text instead of the text saved in the workflow's source_script node. Applied to the run's definition snapshot only.
-
-
-
-
-
--
-
-**source_language:** `typing.Optional[str]` — BCP-47 language of script_text (e.g. en-us). Optional; when omitted the saved source_language (or automatic detection) applies.
+**workspace_id:** `typing.Optional[str]`
@@ -7511,7 +7503,7 @@ client.workflows.preview_run(
-
-**workspace_id:** `typing.Optional[str]`
+**request:** `WorkflowRunStartIn`
@@ -7519,15 +7511,7 @@ client.workflows.preview_run(
-
-**script_text:** `typing.Optional[str]` — Run this workflow with this script text instead of the text saved in the workflow's source_script node. Applied to the run's definition snapshot only.
-
-
-
-
-
--
-
-**source_language:** `typing.Optional[str]` — BCP-47 language of script_text (e.g. en-us). Optional; when omitted the saved source_language (or automatic detection) applies.
+**workspace_id:** `typing.Optional[str]`
@@ -9081,15 +9065,7 @@ client.workflows.runs.start(
-
-**workspace_id:** `typing.Optional[str]`
-
-
-
-
-
--
-
-**script_text:** `typing.Optional[str]` — Run this workflow with this script text instead of the text saved in the workflow's source_script node. Applied to the run's definition snapshot only.
+**request:** `WorkflowRunStartIn`
@@ -9097,7 +9073,7 @@ client.workflows.runs.start(
-
-**source_language:** `typing.Optional[str]` — BCP-47 language of script_text (e.g. en-us). Optional; when omitted the saved source_language (or automatic detection) applies.
+**workspace_id:** `typing.Optional[str]`
@@ -9559,4 +9535,3 @@ client.workflows.runs.cancel(
-
diff --git a/src/onepin/templates/client.py b/src/onepin/templates/client.py
index 877e5e1..1e25d50 100644
--- a/src/onepin/templates/client.py
+++ b/src/onepin/templates/client.py
@@ -3,6 +3,7 @@
import typing
from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper
+from ..core.pagination import AsyncPager, SyncPager
from ..core.request_options import RequestOptions
from ..types.api_list_response_template_out import ApiListResponseTemplateOut
from ..types.api_response_dict import ApiResponseDict
@@ -10,6 +11,7 @@
from ..types.api_response_template_out import ApiResponseTemplateOut
from ..types.api_response_workflow_out import ApiResponseWorkflowOut
from ..types.template_category import TemplateCategory
+from ..types.template_out import TemplateOut
from ..types.workflow_definition_input import WorkflowDefinitionInput
from .raw_client import AsyncRawTemplatesClient, RawTemplatesClient
from .types.list_templates_request_sort import ListTemplatesRequestSort
@@ -43,7 +45,7 @@ def list(
limit: typing.Optional[int] = None,
favorites_only: typing.Optional[bool] = None,
request_options: typing.Optional[RequestOptions] = None,
- ) -> ApiListResponseTemplateOut:
+ ) -> SyncPager[TemplateOut, ApiListResponseTemplateOut]:
"""
Browse the public template gallery across all workspaces.
@@ -82,7 +84,7 @@ def list(
Returns
-------
- ApiListResponseTemplateOut
+ SyncPager[TemplateOut, ApiListResponseTemplateOut]
Successful Response
Examples
@@ -92,9 +94,14 @@ def list(
client = OnePinClient(
token="YOUR_TOKEN",
)
- client.templates.list()
+ response = client.templates.list()
+ for item in response:
+ yield item
+ # alternatively, you can paginate page-by-page
+ for page in response.iter_pages():
+ yield page
"""
- _response = self._raw_client.list(
+ return self._raw_client.list(
category=category,
search=search,
sort=sort,
@@ -103,7 +110,6 @@ def list(
favorites_only=favorites_only,
request_options=request_options,
)
- return _response.data
def create_template(
self,
@@ -580,7 +586,7 @@ async def list(
limit: typing.Optional[int] = None,
favorites_only: typing.Optional[bool] = None,
request_options: typing.Optional[RequestOptions] = None,
- ) -> ApiListResponseTemplateOut:
+ ) -> AsyncPager[TemplateOut, ApiListResponseTemplateOut]:
"""
Browse the public template gallery across all workspaces.
@@ -619,7 +625,7 @@ async def list(
Returns
-------
- ApiListResponseTemplateOut
+ AsyncPager[TemplateOut, ApiListResponseTemplateOut]
Successful Response
Examples
@@ -634,12 +640,18 @@ async def list(
async def main() -> None:
- await client.templates.list()
+ response = await client.templates.list()
+ async for item in response:
+ yield item
+
+ # alternatively, you can paginate page-by-page
+ async for page in response.iter_pages():
+ yield page
asyncio.run(main())
"""
- _response = await self._raw_client.list(
+ return await self._raw_client.list(
category=category,
search=search,
sort=sort,
@@ -648,7 +660,6 @@ async def main() -> None:
favorites_only=favorites_only,
request_options=request_options,
)
- return _response.data
async def create_template(
self,
diff --git a/src/onepin/templates/raw_client.py b/src/onepin/templates/raw_client.py
index c417687..44f3b74 100644
--- a/src/onepin/templates/raw_client.py
+++ b/src/onepin/templates/raw_client.py
@@ -7,6 +7,7 @@
from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper
from ..core.http_response import AsyncHttpResponse, HttpResponse
from ..core.jsonable_encoder import encode_path_param
+from ..core.pagination import AsyncPager, SyncPager
from ..core.parse_error import ParsingError
from ..core.pydantic_utilities import parse_obj_as
from ..core.request_options import RequestOptions
@@ -18,6 +19,7 @@
from ..types.api_response_template_out import ApiResponseTemplateOut
from ..types.api_response_workflow_out import ApiResponseWorkflowOut
from ..types.template_category import TemplateCategory
+from ..types.template_out import TemplateOut
from ..types.workflow_definition_input import WorkflowDefinitionInput
from .types.list_templates_request_sort import ListTemplatesRequestSort
from pydantic import ValidationError
@@ -40,7 +42,7 @@ def list(
limit: typing.Optional[int] = None,
favorites_only: typing.Optional[bool] = None,
request_options: typing.Optional[RequestOptions] = None,
- ) -> HttpResponse[ApiListResponseTemplateOut]:
+ ) -> SyncPager[TemplateOut, ApiListResponseTemplateOut]:
"""
Browse the public template gallery across all workspaces.
@@ -79,9 +81,11 @@ def list(
Returns
-------
- HttpResponse[ApiListResponseTemplateOut]
+ SyncPager[TemplateOut, ApiListResponseTemplateOut]
Successful Response
"""
+ offset = offset if offset is not None else 0
+
_response = self._client_wrapper.httpx_client.request(
"api/v1/templates",
method="GET",
@@ -97,14 +101,25 @@ def list(
)
try:
if 200 <= _response.status_code < 300:
- _data = typing.cast(
+ _parsed_response = typing.cast(
ApiListResponseTemplateOut,
parse_obj_as(
type_=ApiListResponseTemplateOut, # type: ignore
object_=_response.json(),
),
)
- return HttpResponse(response=_response, data=_data)
+ _items = _parsed_response.data
+ _has_next = len(_items or []) > 0
+ _get_next = lambda: self.list(
+ category=category,
+ search=search,
+ sort=sort,
+ offset=offset + len(_items or []),
+ limit=limit,
+ favorites_only=favorites_only,
+ request_options=request_options,
+ )
+ return SyncPager(has_next=_has_next, items=_items, get_next=_get_next, response=_parsed_response)
if _response.status_code == 422:
raise UnprocessableEntityError(
headers=dict(_response.headers),
@@ -779,7 +794,7 @@ async def list(
limit: typing.Optional[int] = None,
favorites_only: typing.Optional[bool] = None,
request_options: typing.Optional[RequestOptions] = None,
- ) -> AsyncHttpResponse[ApiListResponseTemplateOut]:
+ ) -> AsyncPager[TemplateOut, ApiListResponseTemplateOut]:
"""
Browse the public template gallery across all workspaces.
@@ -818,9 +833,11 @@ async def list(
Returns
-------
- AsyncHttpResponse[ApiListResponseTemplateOut]
+ AsyncPager[TemplateOut, ApiListResponseTemplateOut]
Successful Response
"""
+ offset = offset if offset is not None else 0
+
_response = await self._client_wrapper.httpx_client.request(
"api/v1/templates",
method="GET",
@@ -836,14 +853,28 @@ async def list(
)
try:
if 200 <= _response.status_code < 300:
- _data = typing.cast(
+ _parsed_response = typing.cast(
ApiListResponseTemplateOut,
parse_obj_as(
type_=ApiListResponseTemplateOut, # type: ignore
object_=_response.json(),
),
)
- return AsyncHttpResponse(response=_response, data=_data)
+ _items = _parsed_response.data
+ _has_next = len(_items or []) > 0
+
+ async def _get_next():
+ return await self.list(
+ category=category,
+ search=search,
+ sort=sort,
+ offset=offset + len(_items or []),
+ limit=limit,
+ favorites_only=favorites_only,
+ request_options=request_options,
+ )
+
+ return AsyncPager(has_next=_has_next, items=_items, get_next=_get_next, response=_parsed_response)
if _response.status_code == 422:
raise UnprocessableEntityError(
headers=dict(_response.headers),
diff --git a/src/onepin/voices/client.py b/src/onepin/voices/client.py
index d1cd555..154da2b 100644
--- a/src/onepin/voices/client.py
+++ b/src/onepin/voices/client.py
@@ -3,6 +3,7 @@
import typing
from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper
+from ..core.pagination import AsyncPager, SyncPager
from ..core.request_options import RequestOptions
from ..types.api_counted_list_response_voice_out import ApiCountedListResponseVoiceOut
from ..types.api_list_response_voice_similar_out import ApiListResponseVoiceSimilarOut
@@ -14,6 +15,7 @@
from ..types.voice_age import VoiceAge
from ..types.voice_category import VoiceCategory
from ..types.voice_gender import VoiceGender
+from ..types.voice_out import VoiceOut
from .raw_client import AsyncRawVoicesClient, RawVoicesClient
from .types.get_voice_facets_api_v1voices_facets_get_request_source_item import (
GetVoiceFacetsApiV1VoicesFacetsGetRequestSourceItem,
@@ -60,7 +62,7 @@ def list(
language: typing.Optional[typing.Sequence[ListVoicesRequestLanguageItem]] = None,
workspace_id: typing.Optional[str] = None,
request_options: typing.Optional[RequestOptions] = None,
- ) -> ApiCountedListResponseVoiceOut:
+ ) -> SyncPager[VoiceOut, ApiCountedListResponseVoiceOut]:
"""
List TTS voices available to the current workspace.
@@ -193,7 +195,7 @@ def list(
Returns
-------
- ApiCountedListResponseVoiceOut
+ SyncPager[VoiceOut, ApiCountedListResponseVoiceOut]
Successful Response
Examples
@@ -203,9 +205,14 @@ def list(
client = OnePinClient(
token="YOUR_TOKEN",
)
- client.voices.list()
+ response = client.voices.list()
+ for item in response:
+ yield item
+ # alternatively, you can paginate page-by-page
+ for page in response.iter_pages():
+ yield page
"""
- _response = self._raw_client.list(
+ return self._raw_client.list(
offset=offset,
limit=limit,
favorites_only=favorites_only,
@@ -224,7 +231,6 @@ def list(
workspace_id=workspace_id,
request_options=request_options,
)
- return _response.data
def get_voice_facets(
self,
@@ -689,7 +695,7 @@ async def list(
language: typing.Optional[typing.Sequence[ListVoicesRequestLanguageItem]] = None,
workspace_id: typing.Optional[str] = None,
request_options: typing.Optional[RequestOptions] = None,
- ) -> ApiCountedListResponseVoiceOut:
+ ) -> AsyncPager[VoiceOut, ApiCountedListResponseVoiceOut]:
"""
List TTS voices available to the current workspace.
@@ -822,7 +828,7 @@ async def list(
Returns
-------
- ApiCountedListResponseVoiceOut
+ AsyncPager[VoiceOut, ApiCountedListResponseVoiceOut]
Successful Response
Examples
@@ -837,12 +843,18 @@ async def list(
async def main() -> None:
- await client.voices.list()
+ response = await client.voices.list()
+ async for item in response:
+ yield item
+
+ # alternatively, you can paginate page-by-page
+ async for page in response.iter_pages():
+ yield page
asyncio.run(main())
"""
- _response = await self._raw_client.list(
+ return await self._raw_client.list(
offset=offset,
limit=limit,
favorites_only=favorites_only,
@@ -861,7 +873,6 @@ async def main() -> None:
workspace_id=workspace_id,
request_options=request_options,
)
- return _response.data
async def get_voice_facets(
self,
diff --git a/src/onepin/voices/raw_client.py b/src/onepin/voices/raw_client.py
index b1cffe1..8e34fc2 100644
--- a/src/onepin/voices/raw_client.py
+++ b/src/onepin/voices/raw_client.py
@@ -7,6 +7,7 @@
from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper
from ..core.http_response import AsyncHttpResponse, HttpResponse
from ..core.jsonable_encoder import encode_path_param
+from ..core.pagination import AsyncPager, SyncPager
from ..core.parse_error import ParsingError
from ..core.pydantic_utilities import parse_obj_as
from ..core.request_options import RequestOptions
@@ -21,6 +22,7 @@
from ..types.voice_age import VoiceAge
from ..types.voice_category import VoiceCategory
from ..types.voice_gender import VoiceGender
+from ..types.voice_out import VoiceOut
from .types.get_voice_facets_api_v1voices_facets_get_request_source_item import (
GetVoiceFacetsApiV1VoicesFacetsGetRequestSourceItem,
)
@@ -56,7 +58,7 @@ def list(
language: typing.Optional[typing.Sequence[ListVoicesRequestLanguageItem]] = None,
workspace_id: typing.Optional[str] = None,
request_options: typing.Optional[RequestOptions] = None,
- ) -> HttpResponse[ApiCountedListResponseVoiceOut]:
+ ) -> SyncPager[VoiceOut, ApiCountedListResponseVoiceOut]:
"""
List TTS voices available to the current workspace.
@@ -189,9 +191,11 @@ def list(
Returns
-------
- HttpResponse[ApiCountedListResponseVoiceOut]
+ SyncPager[VoiceOut, ApiCountedListResponseVoiceOut]
Successful Response
"""
+ offset = offset if offset is not None else 0
+
_response = self._client_wrapper.httpx_client.request(
"api/v1/voices",
method="GET",
@@ -219,14 +223,35 @@ def list(
)
try:
if 200 <= _response.status_code < 300:
- _data = typing.cast(
+ _parsed_response = typing.cast(
ApiCountedListResponseVoiceOut,
parse_obj_as(
type_=ApiCountedListResponseVoiceOut, # type: ignore
object_=_response.json(),
),
)
- return HttpResponse(response=_response, data=_data)
+ _items = _parsed_response.data
+ _has_next = len(_items or []) > 0
+ _get_next = lambda: self.list(
+ offset=offset + len(_items or []),
+ limit=limit,
+ favorites_only=favorites_only,
+ source=source,
+ gender=gender,
+ age=age,
+ category=category,
+ accent=accent,
+ search=search,
+ sort=sort,
+ order=order,
+ buildable=buildable,
+ provider=provider,
+ model=model,
+ language=language,
+ workspace_id=workspace_id,
+ request_options=request_options,
+ )
+ return SyncPager(has_next=_has_next, items=_items, get_next=_get_next, response=_parsed_response)
if _response.status_code == 422:
raise UnprocessableEntityError(
headers=dict(_response.headers),
@@ -843,7 +868,7 @@ async def list(
language: typing.Optional[typing.Sequence[ListVoicesRequestLanguageItem]] = None,
workspace_id: typing.Optional[str] = None,
request_options: typing.Optional[RequestOptions] = None,
- ) -> AsyncHttpResponse[ApiCountedListResponseVoiceOut]:
+ ) -> AsyncPager[VoiceOut, ApiCountedListResponseVoiceOut]:
"""
List TTS voices available to the current workspace.
@@ -976,9 +1001,11 @@ async def list(
Returns
-------
- AsyncHttpResponse[ApiCountedListResponseVoiceOut]
+ AsyncPager[VoiceOut, ApiCountedListResponseVoiceOut]
Successful Response
"""
+ offset = offset if offset is not None else 0
+
_response = await self._client_wrapper.httpx_client.request(
"api/v1/voices",
method="GET",
@@ -1006,14 +1033,38 @@ async def list(
)
try:
if 200 <= _response.status_code < 300:
- _data = typing.cast(
+ _parsed_response = typing.cast(
ApiCountedListResponseVoiceOut,
parse_obj_as(
type_=ApiCountedListResponseVoiceOut, # type: ignore
object_=_response.json(),
),
)
- return AsyncHttpResponse(response=_response, data=_data)
+ _items = _parsed_response.data
+ _has_next = len(_items or []) > 0
+
+ async def _get_next():
+ return await self.list(
+ offset=offset + len(_items or []),
+ limit=limit,
+ favorites_only=favorites_only,
+ source=source,
+ gender=gender,
+ age=age,
+ category=category,
+ accent=accent,
+ search=search,
+ sort=sort,
+ order=order,
+ buildable=buildable,
+ provider=provider,
+ model=model,
+ language=language,
+ workspace_id=workspace_id,
+ request_options=request_options,
+ )
+
+ return AsyncPager(has_next=_has_next, items=_items, get_next=_get_next, response=_parsed_response)
if _response.status_code == 422:
raise UnprocessableEntityError(
headers=dict(_response.headers),
diff --git a/src/onepin/workflows/client.py b/src/onepin/workflows/client.py
index 78da03d..db6cb44 100644
--- a/src/onepin/workflows/client.py
+++ b/src/onepin/workflows/client.py
@@ -6,6 +6,7 @@
import typing
from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper
+from ..core.pagination import AsyncPager, SyncPager
from ..core.request_options import RequestOptions
from ..types.api_counted_list_response_workflow_list_item import ApiCountedListResponseWorkflowListItem
from ..types.api_list_response_upload_out import ApiListResponseUploadOut
@@ -22,6 +23,7 @@
from ..types.api_response_workflow_run_overview_out import ApiResponseWorkflowRunOverviewOut
from ..types.api_response_workflow_validate_out import ApiResponseWorkflowValidateOut
from ..types.workflow_definition_input import WorkflowDefinitionInput
+from ..types.workflow_list_item import WorkflowListItem
from ..types.workflow_list_status import WorkflowListStatus
from ..types.workflow_run_data_response import WorkflowRunDataResponse
from .raw_client import AsyncRawWorkflowsClient, RawWorkflowsClient
@@ -66,7 +68,7 @@ def list(
limit: typing.Optional[int] = None,
workspace_id: typing.Optional[str] = None,
request_options: typing.Optional[RequestOptions] = None,
- ) -> ApiCountedListResponseWorkflowListItem:
+ ) -> SyncPager[WorkflowListItem, ApiCountedListResponseWorkflowListItem]:
"""
List workflows in the current workspace.
@@ -144,7 +146,7 @@ def list(
Returns
-------
- ApiCountedListResponseWorkflowListItem
+ SyncPager[WorkflowListItem, ApiCountedListResponseWorkflowListItem]
Successful Response
Examples
@@ -154,9 +156,14 @@ def list(
client = OnePinClient(
token="YOUR_TOKEN",
)
- client.workflows.list()
+ response = client.workflows.list()
+ for item in response:
+ yield item
+ # alternatively, you can paginate page-by-page
+ for page in response.iter_pages():
+ yield page
"""
- _response = self._raw_client.list(
+ return self._raw_client.list(
status=status,
search=search,
sort=sort,
@@ -170,7 +177,6 @@ def list(
workspace_id=workspace_id,
request_options=request_options,
)
- return _response.data
def create_workflow(
self,
@@ -1526,7 +1532,7 @@ async def list(
limit: typing.Optional[int] = None,
workspace_id: typing.Optional[str] = None,
request_options: typing.Optional[RequestOptions] = None,
- ) -> ApiCountedListResponseWorkflowListItem:
+ ) -> AsyncPager[WorkflowListItem, ApiCountedListResponseWorkflowListItem]:
"""
List workflows in the current workspace.
@@ -1604,7 +1610,7 @@ async def list(
Returns
-------
- ApiCountedListResponseWorkflowListItem
+ AsyncPager[WorkflowListItem, ApiCountedListResponseWorkflowListItem]
Successful Response
Examples
@@ -1619,12 +1625,18 @@ async def list(
async def main() -> None:
- await client.workflows.list()
+ response = await client.workflows.list()
+ async for item in response:
+ yield item
+
+ # alternatively, you can paginate page-by-page
+ async for page in response.iter_pages():
+ yield page
asyncio.run(main())
"""
- _response = await self._raw_client.list(
+ return await self._raw_client.list(
status=status,
search=search,
sort=sort,
@@ -1638,7 +1650,6 @@ async def main() -> None:
workspace_id=workspace_id,
request_options=request_options,
)
- return _response.data
async def create_workflow(
self,
diff --git a/src/onepin/workflows/raw_client.py b/src/onepin/workflows/raw_client.py
index e7bd5b6..11a9bcc 100644
--- a/src/onepin/workflows/raw_client.py
+++ b/src/onepin/workflows/raw_client.py
@@ -9,6 +9,7 @@
from ..core.datetime_utils import serialize_datetime
from ..core.http_response import AsyncHttpResponse, HttpResponse
from ..core.jsonable_encoder import encode_path_param
+from ..core.pagination import AsyncPager, SyncPager
from ..core.parse_error import ParsingError
from ..core.pydantic_utilities import parse_obj_as
from ..core.request_options import RequestOptions
@@ -31,6 +32,7 @@
from ..types.api_response_workflow_run_overview_out import ApiResponseWorkflowRunOverviewOut
from ..types.api_response_workflow_validate_out import ApiResponseWorkflowValidateOut
from ..types.workflow_definition_input import WorkflowDefinitionInput
+from ..types.workflow_list_item import WorkflowListItem
from ..types.workflow_list_status import WorkflowListStatus
from ..types.workflow_run_data_response import WorkflowRunDataResponse
from .types.list_workflows_request_order_item import ListWorkflowsRequestOrderItem
@@ -60,7 +62,7 @@ def list(
limit: typing.Optional[int] = None,
workspace_id: typing.Optional[str] = None,
request_options: typing.Optional[RequestOptions] = None,
- ) -> HttpResponse[ApiCountedListResponseWorkflowListItem]:
+ ) -> SyncPager[WorkflowListItem, ApiCountedListResponseWorkflowListItem]:
"""
List workflows in the current workspace.
@@ -138,9 +140,11 @@ def list(
Returns
-------
- HttpResponse[ApiCountedListResponseWorkflowListItem]
+ SyncPager[WorkflowListItem, ApiCountedListResponseWorkflowListItem]
Successful Response
"""
+ offset = offset if offset is not None else 0
+
_response = self._client_wrapper.httpx_client.request(
"api/v1/workflows",
method="GET",
@@ -163,14 +167,30 @@ def list(
)
try:
if 200 <= _response.status_code < 300:
- _data = typing.cast(
+ _parsed_response = typing.cast(
ApiCountedListResponseWorkflowListItem,
parse_obj_as(
type_=ApiCountedListResponseWorkflowListItem, # type: ignore
object_=_response.json(),
),
)
- return HttpResponse(response=_response, data=_data)
+ _items = _parsed_response.data
+ _has_next = len(_items or []) > 0
+ _get_next = lambda: self.list(
+ status=status,
+ search=search,
+ sort=sort,
+ order=order,
+ last_run_after=last_run_after,
+ last_run_before=last_run_before,
+ has_failed_run=has_failed_run,
+ include_definition=include_definition,
+ offset=offset + len(_items or []),
+ limit=limit,
+ workspace_id=workspace_id,
+ request_options=request_options,
+ )
+ return SyncPager(has_next=_has_next, items=_items, get_next=_get_next, response=_parsed_response)
if _response.status_code == 422:
raise UnprocessableEntityError(
headers=dict(_response.headers),
@@ -2089,7 +2109,7 @@ async def list(
limit: typing.Optional[int] = None,
workspace_id: typing.Optional[str] = None,
request_options: typing.Optional[RequestOptions] = None,
- ) -> AsyncHttpResponse[ApiCountedListResponseWorkflowListItem]:
+ ) -> AsyncPager[WorkflowListItem, ApiCountedListResponseWorkflowListItem]:
"""
List workflows in the current workspace.
@@ -2167,9 +2187,11 @@ async def list(
Returns
-------
- AsyncHttpResponse[ApiCountedListResponseWorkflowListItem]
+ AsyncPager[WorkflowListItem, ApiCountedListResponseWorkflowListItem]
Successful Response
"""
+ offset = offset if offset is not None else 0
+
_response = await self._client_wrapper.httpx_client.request(
"api/v1/workflows",
method="GET",
@@ -2192,14 +2214,33 @@ async def list(
)
try:
if 200 <= _response.status_code < 300:
- _data = typing.cast(
+ _parsed_response = typing.cast(
ApiCountedListResponseWorkflowListItem,
parse_obj_as(
type_=ApiCountedListResponseWorkflowListItem, # type: ignore
object_=_response.json(),
),
)
- return AsyncHttpResponse(response=_response, data=_data)
+ _items = _parsed_response.data
+ _has_next = len(_items or []) > 0
+
+ async def _get_next():
+ return await self.list(
+ status=status,
+ search=search,
+ sort=sort,
+ order=order,
+ last_run_after=last_run_after,
+ last_run_before=last_run_before,
+ has_failed_run=has_failed_run,
+ include_definition=include_definition,
+ offset=offset + len(_items or []),
+ limit=limit,
+ workspace_id=workspace_id,
+ request_options=request_options,
+ )
+
+ return AsyncPager(has_next=_has_next, items=_items, get_next=_get_next, response=_parsed_response)
if _response.status_code == 422:
raise UnprocessableEntityError(
headers=dict(_response.headers),
diff --git a/src/onepin/workflows/runs/client.py b/src/onepin/workflows/runs/client.py
index 8f6a24b..d0ccc51 100644
--- a/src/onepin/workflows/runs/client.py
+++ b/src/onepin/workflows/runs/client.py
@@ -3,6 +3,7 @@
import typing
from ...core.client_wrapper import AsyncClientWrapper, SyncClientWrapper
+from ...core.pagination import AsyncPager, SyncPager
from ...core.request_options import RequestOptions
from ...types.api_counted_list_response_workflow_run_list_item import ApiCountedListResponseWorkflowRunListItem
from ...types.api_response_list_workflow_run_step_out import ApiResponseListWorkflowRunStepOut
@@ -10,6 +11,7 @@
from ...types.api_response_workflow_run_out import ApiResponseWorkflowRunOut
from ...types.api_response_workflow_run_status_out import ApiResponseWorkflowRunStatusOut
from ...types.node_type import NodeType
+from ...types.workflow_run_list_item import WorkflowRunListItem
from .raw_client import AsyncRawRunsClient, RawRunsClient
from .types.list_runs_request_order import ListRunsRequestOrder
from .types.list_runs_request_sort import ListRunsRequestSort
@@ -45,7 +47,7 @@ def list(
order: typing.Optional[ListRunsRequestOrder] = None,
workspace_id: typing.Optional[str] = None,
request_options: typing.Optional[RequestOptions] = None,
- ) -> ApiCountedListResponseWorkflowRunListItem:
+ ) -> SyncPager[WorkflowRunListItem, ApiCountedListResponseWorkflowRunListItem]:
"""
List runs for a workflow, newest first by default.
@@ -92,7 +94,7 @@ def list(
Returns
-------
- ApiCountedListResponseWorkflowRunListItem
+ SyncPager[WorkflowRunListItem, ApiCountedListResponseWorkflowRunListItem]
Successful Response
Examples
@@ -102,11 +104,16 @@ def list(
client = OnePinClient(
token="YOUR_TOKEN",
)
- client.workflows.runs.list(
+ response = client.workflows.runs.list(
workflow_id="workflow_id",
)
+ for item in response:
+ yield item
+ # alternatively, you can paginate page-by-page
+ for page in response.iter_pages():
+ yield page
"""
- _response = self._raw_client.list(
+ return self._raw_client.list(
workflow_id,
offset=offset,
limit=limit,
@@ -117,7 +124,6 @@ def list(
workspace_id=workspace_id,
request_options=request_options,
)
- return _response.data
def start(
self,
@@ -473,7 +479,7 @@ async def list(
order: typing.Optional[ListRunsRequestOrder] = None,
workspace_id: typing.Optional[str] = None,
request_options: typing.Optional[RequestOptions] = None,
- ) -> ApiCountedListResponseWorkflowRunListItem:
+ ) -> AsyncPager[WorkflowRunListItem, ApiCountedListResponseWorkflowRunListItem]:
"""
List runs for a workflow, newest first by default.
@@ -520,7 +526,7 @@ async def list(
Returns
-------
- ApiCountedListResponseWorkflowRunListItem
+ AsyncPager[WorkflowRunListItem, ApiCountedListResponseWorkflowRunListItem]
Successful Response
Examples
@@ -535,14 +541,20 @@ async def list(
async def main() -> None:
- await client.workflows.runs.list(
+ response = await client.workflows.runs.list(
workflow_id="workflow_id",
)
+ async for item in response:
+ yield item
+
+ # alternatively, you can paginate page-by-page
+ async for page in response.iter_pages():
+ yield page
asyncio.run(main())
"""
- _response = await self._raw_client.list(
+ return await self._raw_client.list(
workflow_id,
offset=offset,
limit=limit,
@@ -553,7 +565,6 @@ async def main() -> None:
workspace_id=workspace_id,
request_options=request_options,
)
- return _response.data
async def start(
self,
diff --git a/src/onepin/workflows/runs/raw_client.py b/src/onepin/workflows/runs/raw_client.py
index 0599dae..b77f883 100644
--- a/src/onepin/workflows/runs/raw_client.py
+++ b/src/onepin/workflows/runs/raw_client.py
@@ -7,6 +7,7 @@
from ...core.client_wrapper import AsyncClientWrapper, SyncClientWrapper
from ...core.http_response import AsyncHttpResponse, HttpResponse
from ...core.jsonable_encoder import encode_path_param
+from ...core.pagination import AsyncPager, SyncPager
from ...core.parse_error import ParsingError
from ...core.pydantic_utilities import parse_obj_as
from ...core.request_options import RequestOptions
@@ -17,6 +18,7 @@
from ...types.api_response_workflow_run_out import ApiResponseWorkflowRunOut
from ...types.api_response_workflow_run_status_out import ApiResponseWorkflowRunStatusOut
from ...types.node_type import NodeType
+from ...types.workflow_run_list_item import WorkflowRunListItem
from .types.list_runs_request_order import ListRunsRequestOrder
from .types.list_runs_request_sort import ListRunsRequestSort
from pydantic import ValidationError
@@ -41,7 +43,7 @@ def list(
order: typing.Optional[ListRunsRequestOrder] = None,
workspace_id: typing.Optional[str] = None,
request_options: typing.Optional[RequestOptions] = None,
- ) -> HttpResponse[ApiCountedListResponseWorkflowRunListItem]:
+ ) -> SyncPager[WorkflowRunListItem, ApiCountedListResponseWorkflowRunListItem]:
"""
List runs for a workflow, newest first by default.
@@ -88,9 +90,11 @@ def list(
Returns
-------
- HttpResponse[ApiCountedListResponseWorkflowRunListItem]
+ SyncPager[WorkflowRunListItem, ApiCountedListResponseWorkflowRunListItem]
Successful Response
"""
+ offset = offset if offset is not None else 0
+
_response = self._client_wrapper.httpx_client.request(
f"api/v1/workflows/{encode_path_param(workflow_id)}/runs",
method="GET",
@@ -109,14 +113,27 @@ def list(
)
try:
if 200 <= _response.status_code < 300:
- _data = typing.cast(
+ _parsed_response = typing.cast(
ApiCountedListResponseWorkflowRunListItem,
parse_obj_as(
type_=ApiCountedListResponseWorkflowRunListItem, # type: ignore
object_=_response.json(),
),
)
- return HttpResponse(response=_response, data=_data)
+ _items = _parsed_response.data
+ _has_next = len(_items or []) > 0
+ _get_next = lambda: self.list(
+ workflow_id,
+ offset=offset + len(_items or []),
+ limit=limit,
+ status=status,
+ search=search,
+ sort=sort,
+ order=order,
+ workspace_id=workspace_id,
+ request_options=request_options,
+ )
+ return SyncPager(has_next=_has_next, items=_items, get_next=_get_next, response=_parsed_response)
if _response.status_code == 422:
raise UnprocessableEntityError(
headers=dict(_response.headers),
@@ -587,7 +604,7 @@ async def list(
order: typing.Optional[ListRunsRequestOrder] = None,
workspace_id: typing.Optional[str] = None,
request_options: typing.Optional[RequestOptions] = None,
- ) -> AsyncHttpResponse[ApiCountedListResponseWorkflowRunListItem]:
+ ) -> AsyncPager[WorkflowRunListItem, ApiCountedListResponseWorkflowRunListItem]:
"""
List runs for a workflow, newest first by default.
@@ -634,9 +651,11 @@ async def list(
Returns
-------
- AsyncHttpResponse[ApiCountedListResponseWorkflowRunListItem]
+ AsyncPager[WorkflowRunListItem, ApiCountedListResponseWorkflowRunListItem]
Successful Response
"""
+ offset = offset if offset is not None else 0
+
_response = await self._client_wrapper.httpx_client.request(
f"api/v1/workflows/{encode_path_param(workflow_id)}/runs",
method="GET",
@@ -655,14 +674,30 @@ async def list(
)
try:
if 200 <= _response.status_code < 300:
- _data = typing.cast(
+ _parsed_response = typing.cast(
ApiCountedListResponseWorkflowRunListItem,
parse_obj_as(
type_=ApiCountedListResponseWorkflowRunListItem, # type: ignore
object_=_response.json(),
),
)
- return AsyncHttpResponse(response=_response, data=_data)
+ _items = _parsed_response.data
+ _has_next = len(_items or []) > 0
+
+ async def _get_next():
+ return await self.list(
+ workflow_id,
+ offset=offset + len(_items or []),
+ limit=limit,
+ status=status,
+ search=search,
+ sort=sort,
+ order=order,
+ workspace_id=workspace_id,
+ request_options=request_options,
+ )
+
+ return AsyncPager(has_next=_has_next, items=_items, get_next=_get_next, response=_parsed_response)
if _response.status_code == 422:
raise UnprocessableEntityError(
headers=dict(_response.headers),
From 4fa0199a08d3621c13223059b15c5578dd1b2849 Mon Sep 17 00:00:00 2001
From: kj-podonos
Date: Mon, 21 Sep 2026 20:45:53 +0900
Subject: [PATCH 5/6] fix(cli): keep SDK pagers to the requested page
---
src/onepin/_cli/_dispatch.py | 14 ++++++++------
tests/cli/test_cli_dispatch.py | 20 ++++++++++++++++++++
2 files changed, 28 insertions(+), 6 deletions(-)
diff --git a/src/onepin/_cli/_dispatch.py b/src/onepin/_cli/_dispatch.py
index eca0c5c..fdc59c6 100644
--- a/src/onepin/_cli/_dispatch.py
+++ b/src/onepin/_cli/_dispatch.py
@@ -320,11 +320,12 @@ def _emit(cmd: Cmd, resp: Any, bound: dict[str, Any], json_on: bool, *, limit: i
def _emit_pager(cmd: Cmd, pager: Any, json_on: bool, *, limit: int, offset: int = 0) -> int:
- # The SDK returns a SyncPager when generated with pagination enabled, and a
- # list-envelope model (items under .data) otherwise. Iterating a pydantic
- # envelope directly would yield (field, value) tuples — unwrap .data first.
- # SyncPager has no .data attribute, so this is a no-op for real pagers.
- items = list(islice(iter(getattr(pager, "data", pager)), limit))
+ # A generated SyncPager iterates across every API page. The CLI exposes explicit
+ # --offset paging, so render only the current page's .items; otherwise a single
+ # command silently fans out into repeated requests. Older SDK envelopes keep rows
+ # under .data, and iterating the envelope itself would yield (field, value) tuples.
+ source = (pager.items or []) if hasattr(pager, "items") else getattr(pager, "data", pager)
+ items = list(islice(iter(source), limit))
rows = [to_jsonable(item) for item in items]
if json_on:
render_json(rows)
@@ -354,7 +355,8 @@ def _echo_pager_footer(pager: Any, shown: int, *, limit: int, offset: int = 0) -
Human output only. The ``--json`` payload stays a bare array because that shape is the
agent contract pinned by the manifest snapshot; agents read the count by paging.
"""
- total = getattr(getattr(pager, "pagination", None), "total", None)
+ envelope = getattr(pager, "response", pager)
+ total = getattr(getattr(envelope, "pagination", None), "total", None)
if isinstance(total, int):
remaining = max(total - (offset + shown), 0)
more = f" {remaining} more — page with --offset." if remaining > 0 else ""
diff --git a/tests/cli/test_cli_dispatch.py b/tests/cli/test_cli_dispatch.py
index c37cfc4..33023df 100644
--- a/tests/cli/test_cli_dispatch.py
+++ b/tests/cli/test_cli_dispatch.py
@@ -211,6 +211,26 @@ def test_json(self, fake_client: FakeClient, tmp_home) -> None:
assert result.exit_code == 0, result.output
assert '"id": "wf-1"' in result.output
+ def test_sdk_pager_renders_only_the_requested_page(self, fake_client: FakeClient, tmp_home) -> None:
+ item = _workflow_item()
+ response = _counted(42, [item])
+
+ def unexpected_next_page():
+ raise AssertionError("the CLI must not auto-fetch the next SDK page")
+
+ fake_client.workflows.list = lambda **_kw: SyncPager(
+ get_next=unexpected_next_page,
+ has_next=True,
+ items=[item],
+ response=response,
+ )
+
+ result = _invoke(["--no-color", "workflows", "list"])
+
+ assert result.exit_code == 0, result.output
+ assert "Alpha" in result.output
+ assert "Showing 1 of 42." in result.output
+
class TestDataMode:
def test_happy(self, fake_client: FakeClient, tmp_home) -> None:
From bd56051f3a27e3576a2b805811f91f977e872056 Mon Sep 17 00:00:00 2001
From: kj-podonos
Date: Mon, 21 Sep 2026 20:47:04 +0900
Subject: [PATCH 6/6] chore: normalize generated reference whitespace
---
src/onepin/reference.md | 12 ++++++------
1 file changed, 6 insertions(+), 6 deletions(-)
diff --git a/src/onepin/reference.md b/src/onepin/reference.md
index 8b09392..e766f4b 100644
--- a/src/onepin/reference.md
+++ b/src/onepin/reference.md
@@ -4651,8 +4651,8 @@ client.workspaces.get_workspace(
-
-**workspace_id:** `str`
-
+**workspace_id:** `str`
+
@@ -4660,7 +4660,7 @@ client.workspaces.get_workspace(
**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration.
-
+
@@ -5099,8 +5099,8 @@ client.workspaces.get_workspace_credits(
-
-**workspace_id:** `str`
-
+**workspace_id:** `str`
+
@@ -5108,7 +5108,7 @@ client.workspaces.get_workspace_credits(
**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration.
-
+