diff --git a/.spec-sha b/.spec-sha
index bf9bed9..46df6d3 100644
--- a/.spec-sha
+++ b/.spec-sha
@@ -1 +1 @@
-fc36985f78cde936ebe11de2da4310102b7a6c80
+0b38b5c071f4b97f634faed780611d52969ea716
diff --git a/src/onepin/.fern/metadata.json b/src/onepin/.fern/metadata.json
index 53334d1..9d73ce4 100644
--- a/src/onepin/.fern/metadata.json
+++ b/src/onepin/.fern/metadata.json
@@ -14,10 +14,23 @@
"make_async_client",
"OnePinUpgradeRequiredError"
]
+ },
+ {
+ "from": "_client",
+ "imports": [
+ "OnepinClient",
+ "AsyncOnepinClient"
+ ]
+ },
+ {
+ "from": "_auth_resolve",
+ "imports": [
+ "OnepinAuthError"
+ ]
}
]
},
- "originGitCommit": "76a0b71893b02defdab9fb41fea3c42d842d4578",
+ "originGitCommit": "41215c0e6ac788a5ef9da91a94fdcaa669837c82",
"originGitCommitIsDirty": false,
"invokedBy": "ci",
"ciProvider": "github"
diff --git a/src/onepin/README.md b/src/onepin/README.md
index b16310d..72079d0 100644
--- a/src/onepin/README.md
+++ b/src/onepin/README.md
@@ -183,7 +183,7 @@ client = OnePinClient(..., timeout=20.0)
# Override timeout for a specific method
client.dictionary.create_dictionary_entry(..., request_options={
- "timeout_in_seconds": 1
+ "timeout": 1
})
```
diff --git a/src/onepin/__init__.py b/src/onepin/__init__.py
index a6a29e8..34f18e6 100644
--- a/src/onepin/__init__.py
+++ b/src/onepin/__init__.py
@@ -49,6 +49,7 @@
ApiResponseUsageSummaryOut,
ApiResponseVoiceFacetsOut,
ApiResponseVoiceOut,
+ ApiResponseWorkflowNameAvailabilityOut,
ApiResponseWorkflowOut,
ApiResponseWorkflowRunDetailOut,
ApiResponseWorkflowRunOut,
@@ -100,6 +101,7 @@
PortOut,
PronunciationSuggestion,
ProviderGroupOut,
+ RunStatusCounts,
RunsSummaryOut,
SlugAvailabilityOut,
SlugAvailabilityOutReason,
@@ -148,6 +150,7 @@
WorkflowDefinitionOutput,
WorkflowListItem,
WorkflowListStatus,
+ WorkflowNameAvailabilityOut,
WorkflowOut,
WorkflowRunDataAudioOut,
WorkflowRunDataAudioOutStatus,
@@ -182,6 +185,7 @@
WorkflowRunOverviewValidatorOut,
WorkflowRunOverviewValidatorOutStatus,
WorkflowRunOverviewWorkflowOut,
+ WorkflowRunStartIn,
WorkflowRunStatusOut,
WorkflowRunStepOut,
WorkspaceInviteOut,
@@ -207,9 +211,9 @@
workspace_members,
workspaces,
)
- from ._default_clients import DefaultAioHttpClient, DefaultAsyncHttpxClient
from ._auth_resolve import OnepinAuthError
from ._client import AsyncOnepinClient, OnepinClient
+ from ._default_clients import DefaultAioHttpClient, DefaultAsyncHttpxClient
from ._version_gate import OnePinUpgradeRequiredError, make_async_client, make_client
from .client import AsyncOnePinClient, OnePinClient
from .dictionary import (
@@ -235,7 +239,6 @@
GetVoiceFacetsApiV1VoicesFacetsGetRequestSourceItem,
ListVoicesRequestLanguageItem,
ListVoicesRequestOrderItem,
- ListVoicesRequestProviderItem,
ListVoicesRequestSortItem,
ListVoicesRequestSourceItem,
)
@@ -283,6 +286,7 @@
"ApiResponseUsageSummaryOut": ".types",
"ApiResponseVoiceFacetsOut": ".types",
"ApiResponseVoiceOut": ".types",
+ "ApiResponseWorkflowNameAvailabilityOut": ".types",
"ApiResponseWorkflowOut": ".types",
"ApiResponseWorkflowRunDetailOut": ".types",
"ApiResponseWorkflowRunOut": ".types",
@@ -292,6 +296,7 @@
"ApiResponseWorkspaceOut": ".types",
"ApiResponseWorkspaceSettingsOut": ".types",
"AsyncOnePinClient": ".client",
+ "AsyncOnepinClient": "._client",
"AuthWhoamiOut": ".types",
"AuthWhoamiOutAuthKind": ".types",
"BadRequestError": ".errors",
@@ -326,7 +331,6 @@
"ListTemplatesRequestSort": ".templates",
"ListVoicesRequestLanguageItem": ".voices",
"ListVoicesRequestOrderItem": ".voices",
- "ListVoicesRequestProviderItem": ".voices",
"ListVoicesRequestSortItem": ".voices",
"ListVoicesRequestSourceItem": ".voices",
"ListWorkflowsRequestOrderItem": ".workflows",
@@ -353,13 +357,13 @@
"OnePinUpgradeRequiredError": "._version_gate",
"OnepinAuthError": "._auth_resolve",
"OnepinClient": "._client",
- "AsyncOnepinClient": "._client",
"PaginationMeta": ".types",
"PlanLimits": ".types",
"PlanTier": ".types",
"PortOut": ".types",
"PronunciationSuggestion": ".types",
"ProviderGroupOut": ".types",
+ "RunStatusCounts": ".types",
"RunsSummaryOut": ".types",
"SearchDictionaryEntriesApiV1DictionarySearchGetRequestLanguageItem": ".dictionary",
"SearchDictionaryEntriesApiV1DictionarySearchGetRequestOrder": ".dictionary",
@@ -419,6 +423,7 @@
"WorkflowDefinitionOutput": ".types",
"WorkflowListItem": ".types",
"WorkflowListStatus": ".types",
+ "WorkflowNameAvailabilityOut": ".types",
"WorkflowOut": ".types",
"WorkflowRunDataAudioOut": ".types",
"WorkflowRunDataAudioOutStatus": ".types",
@@ -453,6 +458,7 @@
"WorkflowRunOverviewValidatorOut": ".types",
"WorkflowRunOverviewValidatorOutStatus": ".types",
"WorkflowRunOverviewWorkflowOut": ".types",
+ "WorkflowRunStartIn": ".types",
"WorkflowRunStatusOut": ".types",
"WorkflowRunStepOut": ".types",
"WorkspaceInviteOut": ".types",
@@ -543,6 +549,7 @@ def __dir__():
"ApiResponseUsageSummaryOut",
"ApiResponseVoiceFacetsOut",
"ApiResponseVoiceOut",
+ "ApiResponseWorkflowNameAvailabilityOut",
"ApiResponseWorkflowOut",
"ApiResponseWorkflowRunDetailOut",
"ApiResponseWorkflowRunOut",
@@ -552,6 +559,7 @@ def __dir__():
"ApiResponseWorkspaceOut",
"ApiResponseWorkspaceSettingsOut",
"AsyncOnePinClient",
+ "AsyncOnepinClient",
"AuthWhoamiOut",
"AuthWhoamiOutAuthKind",
"BadRequestError",
@@ -586,7 +594,6 @@ def __dir__():
"ListTemplatesRequestSort",
"ListVoicesRequestLanguageItem",
"ListVoicesRequestOrderItem",
- "ListVoicesRequestProviderItem",
"ListVoicesRequestSortItem",
"ListVoicesRequestSourceItem",
"ListWorkflowsRequestOrderItem",
@@ -613,13 +620,13 @@ def __dir__():
"OnePinUpgradeRequiredError",
"OnepinAuthError",
"OnepinClient",
- "AsyncOnepinClient",
"PaginationMeta",
"PlanLimits",
"PlanTier",
"PortOut",
"PronunciationSuggestion",
"ProviderGroupOut",
+ "RunStatusCounts",
"RunsSummaryOut",
"SearchDictionaryEntriesApiV1DictionarySearchGetRequestLanguageItem",
"SearchDictionaryEntriesApiV1DictionarySearchGetRequestOrder",
@@ -679,6 +686,7 @@ def __dir__():
"WorkflowDefinitionOutput",
"WorkflowListItem",
"WorkflowListStatus",
+ "WorkflowNameAvailabilityOut",
"WorkflowOut",
"WorkflowRunDataAudioOut",
"WorkflowRunDataAudioOutStatus",
@@ -713,6 +721,7 @@ def __dir__():
"WorkflowRunOverviewValidatorOut",
"WorkflowRunOverviewValidatorOutStatus",
"WorkflowRunOverviewWorkflowOut",
+ "WorkflowRunStartIn",
"WorkflowRunStatusOut",
"WorkflowRunStepOut",
"WorkspaceInviteOut",
diff --git a/src/onepin/core/client_wrapper.py b/src/onepin/core/client_wrapper.py
index f88ecaa..d2a0f90 100644
--- a/src/onepin/core/client_wrapper.py
+++ b/src/onepin/core/client_wrapper.py
@@ -33,7 +33,7 @@ def get_headers(self) -> typing.Dict[str, str]:
import platform
headers: typing.Dict[str, str] = {
- "User-Agent": "onepin/0.7.2",
+ "User-Agent": "onepin/0.9.1",
"X-Fern-Language": "Python",
"X-Fern-Runtime": f"python/{platform.python_version()}",
"X-Fern-Platform": f"{platform.system().lower()}/{platform.release()}",
diff --git a/src/onepin/core/http_client.py b/src/onepin/core/http_client.py
index aae91a2..124dce2 100644
--- a/src/onepin/core/http_client.py
+++ b/src/onepin/core/http_client.py
@@ -313,7 +313,9 @@ def request(
) -> httpx.Response:
base_url = self.get_base_url(base_url)
_timeout = (
- request_options.get("timeout_in_seconds")
+ request_options.get("timeout")
+ if request_options is not None and request_options.get("timeout") is not None
+ else request_options.get("timeout_in_seconds")
if request_options is not None and request_options.get("timeout_in_seconds") is not None
else self.base_timeout()
)
@@ -474,7 +476,9 @@ def stream(
) -> typing.Iterator[httpx.Response]:
base_url = self.get_base_url(base_url)
_timeout = (
- request_options.get("timeout_in_seconds")
+ request_options.get("timeout")
+ if request_options is not None and request_options.get("timeout") is not None
+ else request_options.get("timeout_in_seconds")
if request_options is not None and request_options.get("timeout_in_seconds") is not None
else self.base_timeout()
)
@@ -604,7 +608,9 @@ async def request(
) -> httpx.Response:
base_url = self.get_base_url(base_url)
_timeout = (
- request_options.get("timeout_in_seconds")
+ request_options.get("timeout")
+ if request_options is not None and request_options.get("timeout") is not None
+ else request_options.get("timeout_in_seconds")
if request_options is not None and request_options.get("timeout_in_seconds") is not None
else self.base_timeout()
)
@@ -768,7 +774,9 @@ async def stream(
) -> typing.AsyncIterator[httpx.Response]:
base_url = self.get_base_url(base_url)
_timeout = (
- request_options.get("timeout_in_seconds")
+ request_options.get("timeout")
+ if request_options is not None and request_options.get("timeout") is not None
+ else request_options.get("timeout_in_seconds")
if request_options is not None and request_options.get("timeout_in_seconds") is not None
else self.base_timeout()
)
diff --git a/src/onepin/core/http_sse/_api.py b/src/onepin/core/http_sse/_api.py
index 192ec0d..9ca5602 100644
--- a/src/onepin/core/http_sse/_api.py
+++ b/src/onepin/core/http_sse/_api.py
@@ -2,9 +2,20 @@
import codecs
import re
+import time
from contextlib import asynccontextmanager, contextmanager
-from typing import Any, AsyncGenerator, AsyncIterator, Iterator, Optional
-
+from typing import (
+ Any,
+ AsyncContextManager,
+ AsyncGenerator,
+ AsyncIterator,
+ Callable,
+ ContextManager,
+ Iterator,
+ Optional,
+)
+
+import anyio
import httpx
from ._decoders import SSEDecoder
from ._exceptions import SSEError
@@ -12,7 +23,16 @@
MAX_LINE_SIZE: int = 1_048_576 # 1 MiB
+# Reconnection defaults, mirroring the TypeScript SDK's Stream implementation.
+DEFAULT_MAX_RECONNECTION_ATTEMPTS: int = 5
+DEFAULT_RECONNECT_DELAY_MS: int = 1_000
+MAX_RECONNECT_DELAY_MS: int = 30_000
+
+# A reconnect callback re-issues the original request (with a ``Last-Event-ID``
+# header set to the supplied event id) and returns a *context manager* yielding
+# a fresh streaming ``httpx.Response``. Sync clients supply a sync context
+# manager; async clients supply an async one.
class EventSource:
def __init__(
self,
@@ -21,22 +41,42 @@ def __init__(
resumable: bool = False,
stream_reconnection_enabled: bool = True,
max_stream_reconnection_attempts: Optional[int] = None,
+ stream_terminator: Optional[str] = None,
+ reconnect: Optional[Callable[[str], Any]] = None,
) -> None:
self._response = response
self._resumable = resumable
self._stream_reconnection_enabled = stream_reconnection_enabled
self._max_stream_reconnection_attempts = max_stream_reconnection_attempts
+ self._stream_terminator = stream_terminator
+ self._reconnect = reconnect
+
+ @staticmethod
+ def _is_event_stream(response: httpx.Response) -> bool:
+ content_type = response.headers.get("content-type", "").partition(";")[0]
+ return "text/event-stream" in content_type
def _check_content_type(self) -> None:
- content_type = self._response.headers.get("content-type", "").partition(";")[0]
- if "text/event-stream" not in content_type:
+ if not self._is_event_stream(self._response):
+ content_type = self._response.headers.get("content-type", "").partition(";")[0]
raise SSEError(
f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}"
)
- def _get_charset(self) -> str:
+ def _is_reconnect_response_usable(self, response: httpx.Response) -> bool:
+ """Whether a reconnected response can be resumed as an SSE stream.
+
+ ``httpx.stream`` does not raise on non-success status, so a resume that
+ returns an error page (e.g. ``200 text/html`` or a ``500`` body) would
+ otherwise be parsed as SSE and yield garbage/zero events. Such a
+ response is treated as a failed attempt (back off and retry) instead.
+ """
+ return response.status_code < 400 and self._is_event_stream(response)
+
+ def _get_charset(self, response: Optional[httpx.Response] = None) -> str:
"""Extract charset from Content-Type header, fallback to UTF-8."""
- content_type = self._response.headers.get("content-type", "")
+ resolved = response if response is not None else self._response
+ content_type = resolved.headers.get("content-type", "")
# Parse charset parameter using regex
charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE)
@@ -70,14 +110,83 @@ def _normalize_sse_line_endings(buf: str) -> str:
return buf[:-1].replace("\r", "\n") + "\r"
return buf.replace("\r", "\n")
- def iter_sse(self) -> Iterator[ServerSentEvent]:
- self._check_content_type()
- decoder = SSEDecoder()
- charset = self._get_charset()
- text_decoder = codecs.getincrementaldecoder(charset)(errors="replace")
-
+ def _new_text_decoder(self, response: Optional[httpx.Response] = None) -> "codecs.IncrementalDecoder":
+ return codecs.getincrementaldecoder(self._get_charset(response))(errors="replace")
+
+ def _reconnect_applicable(self) -> bool:
+ """Whether reconnection is configured for this stream at all.
+
+ This is the terminator-gating half of the reconnect decision, kept
+ separate from :meth:`_should_reconnect` (which additionally requires a
+ last *dispatched* id and an unexhausted attempt budget). The split lets
+ a mid-stream transport error terminate consistently:
+ - a stream that can never reconnect (non-resumable, no terminator,
+ disabled, or no callback) must re-raise the error to the caller, so a
+ truncated stream is not mistaken for a clean completion;
+ - a resumable stream that has merely run out of attempts (or has no id
+ to resume from) ends cleanly — the same way an exhausted empty/error
+ -body resume already does, matching the TypeScript ``return``.
+ """
+ return (
+ self._resumable
+ and self._stream_terminator is not None
+ and self._stream_reconnection_enabled
+ and self._reconnect is not None
+ )
+
+ def _should_reconnect(self, last_dispatched_id: Optional[str], reconnect_attempts: int) -> bool:
+ """Decide whether a prematurely-ended stream should be reconnected.
+
+ Mirrors the TypeScript ``shouldReconnect`` gating:
+ - only resumable SSE endpoints with a configured terminator, reconnect
+ enabled, and a reconnect callback are eligible (see
+ :meth:`_reconnect_applicable`);
+ - a last *dispatched* event id must exist to resume from;
+ - the consecutive-failed-attempt cap must not be exceeded.
+ """
+ if not self._reconnect_applicable():
+ return False
+ if not last_dispatched_id:
+ return False
+ max_attempts = (
+ self._max_stream_reconnection_attempts
+ if self._max_stream_reconnection_attempts is not None
+ else DEFAULT_MAX_RECONNECTION_ATTEMPTS
+ )
+ if reconnect_attempts >= max_attempts:
+ return False
+ return True
+
+ def _reconnect_delay_seconds(self, last_retry: Optional[int]) -> float:
+ """Backoff before a reconnect.
+
+ Uses the server's most recent ``retry:`` directive (milliseconds) when
+ present, otherwise a default of ``DEFAULT_RECONNECT_DELAY_MS``, clamped
+ to ``MAX_RECONNECT_DELAY_MS``.
+ """
+ base_ms = last_retry if (last_retry is not None and last_retry > 0) else DEFAULT_RECONNECT_DELAY_MS
+ return min(base_ms, MAX_RECONNECT_DELAY_MS) / 1000.0
+
+ def _sleep_before_reconnect(self, last_retry: Optional[int]) -> None:
+ # ``time.sleep`` blocks the calling thread but remains interruptible by
+ # signals (e.g. ``KeyboardInterrupt``), which propagate out and abort
+ # the reconnect without issuing another request.
+ time.sleep(self._reconnect_delay_seconds(last_retry))
+
+ async def _asleep_before_reconnect(self, last_retry: Optional[int]) -> None:
+ # ``anyio.sleep`` is cancellation-aware: if the consumer cancels the task
+ # or closes the async generator mid-delay, this raises (and no further
+ # request is issued) instead of blocking for the whole interval.
+ await anyio.sleep(self._reconnect_delay_seconds(last_retry))
+
+ def _decode_response(
+ self,
+ response: httpx.Response,
+ decoder: SSEDecoder,
+ text_decoder: "codecs.IncrementalDecoder",
+ ) -> Iterator[ServerSentEvent]:
buf = ""
- for chunk in self._response.iter_bytes():
+ for chunk in response.iter_bytes():
buf += text_decoder.decode(chunk)
buf = self._normalize_sse_line_endings(buf)
@@ -92,34 +201,16 @@ def iter_sse(self) -> Iterator[ServerSentEvent]:
f"SSE line exceeded maximum size of {MAX_LINE_SIZE} characters without encountering a newline"
)
- # Flush any remaining bytes from the incremental decoder
- buf += text_decoder.decode(b"", final=True)
- buf = buf.replace("\r\n", "\n").replace("\r", "\n")
-
- if len(buf) > MAX_LINE_SIZE:
- raise SSEError(
- f"SSE line exceeded maximum size of {MAX_LINE_SIZE} characters without encountering a newline"
- )
-
- while "\n" in buf:
- line, buf = buf.split("\n", 1)
- sse = decoder.decode(line)
- if sse is not None:
- yield sse
-
- if buf.strip():
- sse = decoder.decode(buf)
- if sse is not None:
- yield sse
-
- async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]:
- self._check_content_type()
- decoder = SSEDecoder()
- charset = self._get_charset()
- text_decoder = codecs.getincrementaldecoder(charset)(errors="replace")
+ yield from self._flush_decoder(buf, decoder, text_decoder)
+ async def _adecode_response(
+ self,
+ response: httpx.Response,
+ decoder: SSEDecoder,
+ text_decoder: "codecs.IncrementalDecoder",
+ ) -> AsyncGenerator[ServerSentEvent, None]:
buf = ""
- async for chunk in self._response.aiter_bytes():
+ async for chunk in response.aiter_bytes():
buf += text_decoder.decode(chunk)
buf = self._normalize_sse_line_endings(buf)
@@ -134,6 +225,15 @@ async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]:
f"SSE line exceeded maximum size of {MAX_LINE_SIZE} characters without encountering a newline"
)
+ for sse in self._flush_decoder(buf, decoder, text_decoder):
+ yield sse
+
+ def _flush_decoder(
+ self,
+ buf: str,
+ decoder: SSEDecoder,
+ text_decoder: "codecs.IncrementalDecoder",
+ ) -> Iterator[ServerSentEvent]:
# Flush any remaining bytes from the incremental decoder
buf += text_decoder.decode(b"", final=True)
buf = buf.replace("\r\n", "\n").replace("\r", "\n")
@@ -154,6 +254,181 @@ async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]:
if sse is not None:
yield sse
+ def iter_sse(self) -> Iterator[ServerSentEvent]:
+ self._check_content_type()
+ decoder = SSEDecoder()
+ text_decoder = self._new_text_decoder()
+
+ last_dispatched_id: Optional[str] = None
+ last_retry: Optional[int] = None
+ # Consecutive failed reconnection attempts. Reset to 0 whenever an event
+ # is successfully dispatched (reset-on-progress) — matching browser
+ # `EventSource` semantics: a server that emits >=1 event then drops on
+ # every connection can reconnect indefinitely.
+ reconnect_attempts = 0
+
+ # ``None`` means there is no live stream to read this iteration (e.g. a
+ # failed reconnect); the loop then re-evaluates the reconnect decision
+ # without re-reading an exhausted response.
+ response: Optional[httpx.Response] = self._response
+ # Context manager for a response we opened ourselves and must close.
+ # The initial response is owned by the caller, so it starts as None.
+ owned_cm: Optional[ContextManager[httpx.Response]] = None
+ try:
+ while True:
+ if response is not None:
+ events = self._decode_response(response, decoder, text_decoder)
+ while True:
+ try:
+ sse = next(events)
+ except StopIteration:
+ break
+ except SSEError:
+ # A protocol violation (e.g. an oversized line) is a
+ # genuine error, not a dropped connection; propagate it.
+ # Listed first because ``SSEError`` subclasses
+ # ``httpx.TransportError``.
+ raise
+ except httpx.TransportError:
+ # A transport error mid-stream (e.g. the server dropped
+ # the connection: ``ReadError``/``RemoteProtocolError``)
+ # is a premature end. Only swallow it when reconnection
+ # is configured for this stream; otherwise re-raise so a
+ # non-resumable stream still surfaces the error to the
+ # caller instead of looking like a clean completion.
+ # When reconnection is applicable but the attempt budget
+ # is exhausted, we ``break`` and end cleanly below — the
+ # same way an exhausted empty/error-body resume does, so
+ # give-up is consistent regardless of failure shape.
+ # ``next`` is used rather than ``for`` so this cannot
+ # swallow a ``GeneratorExit`` raised at a ``yield``.
+ if not self._reconnect_applicable():
+ raise
+ break
+ yield sse
+ if sse.id:
+ last_dispatched_id = sse.id
+ if sse.retry is not None:
+ last_retry = sse.retry
+ reconnect_attempts = 0
+
+ if not self._should_reconnect(last_dispatched_id, reconnect_attempts):
+ return
+ reconnect_attempts += 1
+
+ self._sleep_before_reconnect(last_retry)
+
+ # Close the previously-opened reconnect response before opening
+ # a new one so we never hold more than one extra connection.
+ if owned_cm is not None:
+ owned_cm.__exit__(None, None, None)
+ owned_cm = None
+
+ assert self._reconnect is not None # guaranteed by _should_reconnect
+ try:
+ cm: ContextManager[httpx.Response] = self._reconnect(last_dispatched_id or "")
+ new_response = cm.__enter__()
+ except Exception:
+ # A failed reconnect consumes an attempt; back off and retry.
+ response = None
+ continue
+ owned_cm = cm
+ if new_response is None or not self._is_reconnect_response_usable(new_response):
+ # Null/empty body or a non-SSE/error response (e.g. 204/304,
+ # a 500, or an HTML error page): treat as a failed attempt.
+ response = None
+ continue
+
+ response = new_response
+ # Drop any partial event left over from the dropped stream, but
+ # keep the last event id (per the SSE spec) and start a fresh
+ # incremental text decoder for the new connection.
+ decoder.reset_in_progress_event()
+ text_decoder = self._new_text_decoder(new_response)
+ finally:
+ if owned_cm is not None:
+ owned_cm.__exit__(None, None, None)
+
+ async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]:
+ self._check_content_type()
+ decoder = SSEDecoder()
+ text_decoder = self._new_text_decoder()
+
+ last_dispatched_id: Optional[str] = None
+ last_retry: Optional[int] = None
+ reconnect_attempts = 0
+
+ response: Optional[httpx.Response] = self._response
+ owned_cm: Optional[AsyncContextManager[httpx.Response]] = None
+ try:
+ while True:
+ if response is not None:
+ events = self._adecode_response(response, decoder, text_decoder)
+ while True:
+ try:
+ sse = await events.__anext__()
+ except StopAsyncIteration:
+ break
+ except SSEError:
+ # A protocol violation (e.g. an oversized line) is a
+ # genuine error, not a dropped connection; propagate it.
+ # Listed first because ``SSEError`` subclasses
+ # ``httpx.TransportError``.
+ raise
+ except httpx.TransportError:
+ # A transport error mid-stream (e.g. the server dropped
+ # the connection: ``ReadError``/``RemoteProtocolError``)
+ # is a premature end. Only swallow it when reconnection
+ # is configured for this stream; otherwise re-raise so a
+ # non-resumable stream still surfaces the error to the
+ # caller instead of looking like a clean completion.
+ # When reconnection is applicable but the attempt budget
+ # is exhausted, we ``break`` and end cleanly below — the
+ # same way an exhausted empty/error-body resume does, so
+ # give-up is consistent regardless of failure shape.
+ if not self._reconnect_applicable():
+ raise
+ break
+ yield sse
+ if sse.id:
+ last_dispatched_id = sse.id
+ if sse.retry is not None:
+ last_retry = sse.retry
+ reconnect_attempts = 0
+
+ if not self._should_reconnect(last_dispatched_id, reconnect_attempts):
+ return
+ reconnect_attempts += 1
+
+ await self._asleep_before_reconnect(last_retry)
+
+ if owned_cm is not None:
+ await owned_cm.__aexit__(None, None, None)
+ owned_cm = None
+
+ assert self._reconnect is not None # guaranteed by _should_reconnect
+ try:
+ cm: AsyncContextManager[httpx.Response] = self._reconnect(last_dispatched_id or "")
+ new_response = await cm.__aenter__()
+ except Exception:
+ response = None
+ continue
+ owned_cm = cm
+ if new_response is None or not self._is_reconnect_response_usable(new_response):
+ response = None
+ continue
+
+ response = new_response
+ decoder.reset_in_progress_event()
+ text_decoder = self._new_text_decoder(new_response)
+ finally:
+ if owned_cm is not None:
+ # Shield the close so a cancellation delivered while reading a
+ # reconnected response still fully tears the connection down
+ # instead of leaking it until the client is closed.
+ with anyio.CancelScope(shield=True):
+ await owned_cm.__aexit__(None, None, None)
+
@contextmanager
def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]:
diff --git a/src/onepin/core/http_sse/_decoders.py b/src/onepin/core/http_sse/_decoders.py
index 339b089..1f6b35e 100644
--- a/src/onepin/core/http_sse/_decoders.py
+++ b/src/onepin/core/http_sse/_decoders.py
@@ -12,6 +12,19 @@ def __init__(self) -> None:
self._last_event_id = ""
self._retry: Optional[int] = None
+ def reset_in_progress_event(self) -> None:
+ """Discard any partially-parsed (undispatched) event.
+
+ Used when a stream ends mid-event before reconnecting: the buffered
+ ``event``/``data``/``retry`` fields of the never-dispatched event must
+ be dropped so they do not corrupt the first event of the reconnected
+ stream. Per the SSE spec the last event id is *not* reset here — it
+ persists across connections.
+ """
+ self._event = ""
+ self._data = []
+ self._retry = None
+
def decode(self, line: str) -> Optional[ServerSentEvent]:
# See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501
diff --git a/src/onepin/core/request_options.py b/src/onepin/core/request_options.py
index ebf17bc..caa6f66 100644
--- a/src/onepin/core/request_options.py
+++ b/src/onepin/core/request_options.py
@@ -14,7 +14,9 @@ class RequestOptions(typing.TypedDict, total=False):
This is used primarily as an optional final parameter for service functions.
Attributes:
- - timeout_in_seconds: int. The number of seconds to await an API call before timing out.
+ - timeout: float. The number of seconds to await an API call before timing out.
+
+ - timeout_in_seconds: int. Deprecated alias for `timeout`; both are in seconds. Prefer `timeout`.
- max_retries: int. The max number of retries to attempt if the API call fails.
@@ -27,6 +29,7 @@ class RequestOptions(typing.TypedDict, total=False):
- chunk_size: int. The size, in bytes, to process each chunk of data being streamed back within the response. This equates to leveraging `chunk_size` within `requests` or `httpx`, and is only leveraged for file downloads.
"""
+ timeout: NotRequired[float]
timeout_in_seconds: NotRequired[int]
max_retries: NotRequired[int]
additional_headers: NotRequired[typing.Dict[str, typing.Any]]
diff --git a/src/onepin/providers/client.py b/src/onepin/providers/client.py
index 6d1c547..0d886a6 100644
--- a/src/onepin/providers/client.py
+++ b/src/onepin/providers/client.py
@@ -33,7 +33,7 @@ def list_catalog_providers(
"""
List all available speech synthesis providers in the catalog.
- Returns the full set of processing providers — each with its display name,
+ Returns the full set of speech synthesis providers — each with its display name,
number of available models, and a HATEOAS `models` link to
`GET /providers/{provider}/models`. The response contains only
customer-facing metadata; cost, credentials, and base URLs are never included.
@@ -300,7 +300,7 @@ async def list_catalog_providers(
"""
List all available speech synthesis providers in the catalog.
- Returns the full set of processing providers — each with its display name,
+ Returns the full set of speech synthesis providers — each with its display name,
number of available models, and a HATEOAS `models` link to
`GET /providers/{provider}/models`. The response contains only
customer-facing metadata; cost, credentials, and base URLs are never included.
diff --git a/src/onepin/providers/raw_client.py b/src/onepin/providers/raw_client.py
index 14bcd24..f790717 100644
--- a/src/onepin/providers/raw_client.py
+++ b/src/onepin/providers/raw_client.py
@@ -30,7 +30,7 @@ def list_catalog_providers(
"""
List all available speech synthesis providers in the catalog.
- Returns the full set of processing providers — each with its display name,
+ Returns the full set of speech synthesis providers — each with its display name,
number of available models, and a HATEOAS `models` link to
`GET /providers/{provider}/models`. The response contains only
customer-facing metadata; cost, credentials, and base URLs are never included.
@@ -446,7 +446,7 @@ async def list_catalog_providers(
"""
List all available speech synthesis providers in the catalog.
- Returns the full set of processing providers — each with its display name,
+ Returns the full set of speech synthesis providers — each with its display name,
number of available models, and a HATEOAS `models` link to
`GET /providers/{provider}/models`. The response contains only
customer-facing metadata; cost, credentials, and base URLs are never included.
diff --git a/src/onepin/reference.md b/src/onepin/reference.md
index 86832b8..7b2f385 100644
--- a/src/onepin/reference.md
+++ b/src/onepin/reference.md
@@ -1172,7 +1172,7 @@ client.nodes.get_node_detail_v2(
List all available speech synthesis providers in the catalog.
-Returns the full set of processing providers — each with its display name,
+Returns the full set of speech synthesis providers — each with its display name,
number of available models, and a HATEOAS `models` link to
`GET /providers/{provider}/models`. The response contains only
customer-facing metadata; cost, credentials, and base URLs are never included.
@@ -2705,7 +2705,7 @@ client.voices.list()
-
-**provider:** `typing.Optional[typing.List[ListVoicesRequestProviderItem]]` — Repeat for OR, e.g. ?provider=elevenlabs&provider=rime
+**provider:** `typing.Optional[typing.List[str]]` — Repeat for OR, e.g. ?provider=elevenlabs&provider=rime
@@ -3485,8 +3485,6 @@ per workspace is allowed at a time; re-inviting the same address while a
pending invite exists returns 409. Inviting an address that already
belongs to an active member also returns 409.
-The total number of active members plus pending invites is counted against
-the workspace owner's plan seat limit. Exceeding the limit returns 402.
The invitee's role can be updated before acceptance via
`PATCH /workspaces/{ws_id}/invites/{invite_id}`, or the invite can be
cancelled via `DELETE /workspaces/{ws_id}/invites/{invite_id}`.
@@ -3977,10 +3975,6 @@ Error cases (all return 410 Gone):
- Invite already accepted.
- Invite was revoked by an admin.
- Invite has expired (14-day TTL from creation).
-
-The workspace owner's plan seat limit is re-checked at accept time in case
-the plan was downgraded after the invite was sent; exceeding the limit
-returns 402.
@@ -4803,7 +4797,7 @@ client.uploads.confirm(
-
-**context_type:** `UploadConfirmRequestContextType` — Type of resource this upload is being attached to. Currently only `workflow` is supported.
+**context_type:** `UploadConfirmRequestContextType` — Type of resource this upload is being attached to: `workflow` or `playground`.
@@ -5382,10 +5376,9 @@ Return the plan limits that govern the caller's current tier.
Includes numeric quotas (`monthly_credits`, `concurrent_runs_per_user`,
`storage_bytes_per_workspace`, `workspaces_per_owner`) and feature flags
-(`byok_enabled`, `auto_fix_enabled`, `auto_edit_enabled`). `null` on list
-fields such as `tts_models_allowlist` or `supported_languages` means all
-available options are permitted. Use this endpoint to gate feature access in
-your application rather than hardcoding tier names, which may change.
+(`byok_enabled`, `auto_fix_enabled`). Use this endpoint to gate feature
+access in your application rather than hardcoding tier names, which may
+change.
@@ -5728,7 +5721,10 @@ List workflows in the current workspace.
Returns a counted, paginated list of workflows scoped to the `X-Workspace-Id`
header. Each item includes aggregate stats (`runs_count`, `last_run_at`,
-`last_run_status`) computed over all runs for that workflow.
+`last_run_status`, `run_status_counts`) computed over all runs for that
+workflow. `run_status_counts` is a per-raw-`RunStatus` map whose values sum to
+`runs_count` and are NOT affected by the `status` filter below, so a collapsed
+row can render an accurate per-tab total without a separate runs query.
**Status filter:** `status` narrows by the UI-derived state of the workflow's
most recent run. `completed` matches only workflows whose latest run succeeded
@@ -5989,6 +5985,102 @@ client.workflows.create_workflow(
+
+
+
+
+client.workflows.check_workflow_name_availability(...) -> ApiResponseWorkflowNameAvailabilityOut
+
+-
+
+#### 📝 Description
+
+
+-
+
+
+-
+
+Check whether a workflow name is free within the current workspace.
+
+Workflow names are unique per workspace among live (non-deleted) workflows,
+so this lets a client validate a name before create or rename. The `name` is
+trimmed and validated with the same policy as create — an invalid name
+returns 422. The check is case-sensitive and ignores soft-deleted workflows,
+mirroring the underlying uniqueness constraint. Pass `exclude_id` when
+renaming so the workflow's current name is not reported as taken by itself.
+
+
+
+
+
+#### 🔌 Usage
+
+
+-
+
+
+-
+
+```python
+from onepin import OnePinClient
+from onepin.environment import OnePinClientEnvironment
+
+client = OnePinClient(
+ token="",
+ environment=OnePinClientEnvironment.PROD,
+)
+
+client.workflows.check_workflow_name_availability(
+ name="name",
+)
+
+```
+
+
+
+
+
+#### ⚙️ Parameters
+
+
+-
+
+
+-
+
+**name:** `str` — Workflow name to check (trimmed before comparison).
+
+
+
+
+
+-
+
+**exclude_id:** `typing.Optional[str]` — Workflow to exclude from the check — its own name then reads as available. Pass the workflow's id when validating a rename.
+
+
+
+
+
+-
+
+**workspace_id:** `typing.Optional[str]`
+
+
+
+
+
+-
+
+**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration.
+
+
+
+
+
+
+
@@ -6802,8 +6894,10 @@ values (valid for 15 minutes) so callers can stream audio directly without
a separate download step.
`node_display_name` is resolved from the run's definition snapshot, so it
-reflects the name the node had when the run executed. Nodes that were
-retried appear as multiple steps with incrementing `iteration` values.
+reflects the name the node had when the run executed. When multiple language
+branches reach one output node, repeated sink steps receive locale suffixes
+based on the language introduced by each iteration. Other retried nodes keep
+their snapshot display name and incrementing `iteration` value.
For a higher-level view with aggregated metrics (pass rates, audio duration
by language), use `GET /runs/{run_id}/overview`. For paginated, grouped
@@ -6904,6 +6998,10 @@ pass rates) grouped by display section, along with per-language audio
breakdowns and per-validator scoring summaries. Also includes a
`workflow_snapshot` with the graph definition and per-node completion states.
+Repeated states for one shared output node use the same locale suffixes as
+`GET /runs/{run_id}/steps`, based on the language introduced by each sink
+iteration.
+
This endpoint is best suited for a summary/results view after a run
completes. It differs from the other run sub-resources as follows:
@@ -7255,7 +7353,7 @@ results rather than the full run archive.
`node_id` must identify an output-category node in the run's definition
snapshot. Passing a node ID that belongs to a non-output node type (e.g.
-a processing or validation node) returns 404. Returns 404 if the node
+an operator or validation node) returns 404. Returns 404 if the node
produced no audio files, and 409 if the run has not yet completed.
The URL is valid for 15 minutes. To download all output nodes in a single
@@ -7796,6 +7894,12 @@ its nodes in the background; poll `GET /runs/{run_id}/status` for
lightweight progress updates, or `GET /runs/{run_id}` once to load the
immutable definition snapshot.
+The optional request body supplies run-scoped inputs: `script_text`
+(and optionally `source_language`) replaces the source_script text for
+THIS run's snapshot only — the stored workflow definition is not
+modified, so concurrent runs with different scripts cannot race.
+Requires exactly one source_script node (422 otherwise).
+
Use `POST /runs/preview` or `POST /estimate` to compute the credit cost
before committing to an actual run — those endpoints are read-only and
incur no charges.
@@ -7816,7 +7920,7 @@ run for this workflow is already active.
```python
-from onepin import OnePinClient
+from onepin import OnePinClient, WorkflowRunStartIn
from onepin.environment import OnePinClientEnvironment
client = OnePinClient(
@@ -7826,6 +7930,7 @@ client = OnePinClient(
client.workflows.runs.start(
workflow_id="workflow_id",
+ request=WorkflowRunStartIn(),
)
```
@@ -7858,6 +7963,14 @@ client.workflows.runs.start(
-
+**request:** `typing.Optional[WorkflowRunStartIn]`
+
+
+
+
+
+-
+
**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration.
diff --git a/src/onepin/types/__init__.py b/src/onepin/types/__init__.py
index bdee59a..50f0fd6 100644
--- a/src/onepin/types/__init__.py
+++ b/src/onepin/types/__init__.py
@@ -48,6 +48,7 @@
from .api_response_usage_summary_out import ApiResponseUsageSummaryOut
from .api_response_voice_facets_out import ApiResponseVoiceFacetsOut
from .api_response_voice_out import ApiResponseVoiceOut
+ from .api_response_workflow_name_availability_out import ApiResponseWorkflowNameAvailabilityOut
from .api_response_workflow_out import ApiResponseWorkflowOut
from .api_response_workflow_run_detail_out import ApiResponseWorkflowRunDetailOut
from .api_response_workflow_run_out import ApiResponseWorkflowRunOut
@@ -101,6 +102,7 @@
from .port_out import PortOut
from .pronunciation_suggestion import PronunciationSuggestion
from .provider_group_out import ProviderGroupOut
+ from .run_status_counts import RunStatusCounts
from .runs_summary_out import RunsSummaryOut
from .slug_availability_out import SlugAvailabilityOut
from .slug_availability_out_reason import SlugAvailabilityOutReason
@@ -149,6 +151,7 @@
from .workflow_definition_output import WorkflowDefinitionOutput
from .workflow_list_item import WorkflowListItem
from .workflow_list_status import WorkflowListStatus
+ from .workflow_name_availability_out import WorkflowNameAvailabilityOut
from .workflow_out import WorkflowOut
from .workflow_run_data_audio_out import WorkflowRunDataAudioOut
from .workflow_run_data_audio_out_status import WorkflowRunDataAudioOutStatus
@@ -183,6 +186,7 @@
from .workflow_run_overview_validator_out import WorkflowRunOverviewValidatorOut
from .workflow_run_overview_validator_out_status import WorkflowRunOverviewValidatorOutStatus
from .workflow_run_overview_workflow_out import WorkflowRunOverviewWorkflowOut
+ from .workflow_run_start_in import WorkflowRunStartIn
from .workflow_run_status_out import WorkflowRunStatusOut
from .workflow_run_step_out import WorkflowRunStepOut
from .workspace_invite_out import WorkspaceInviteOut
@@ -234,6 +238,7 @@
"ApiResponseUsageSummaryOut": ".api_response_usage_summary_out",
"ApiResponseVoiceFacetsOut": ".api_response_voice_facets_out",
"ApiResponseVoiceOut": ".api_response_voice_out",
+ "ApiResponseWorkflowNameAvailabilityOut": ".api_response_workflow_name_availability_out",
"ApiResponseWorkflowOut": ".api_response_workflow_out",
"ApiResponseWorkflowRunDetailOut": ".api_response_workflow_run_detail_out",
"ApiResponseWorkflowRunOut": ".api_response_workflow_run_out",
@@ -285,6 +290,7 @@
"PortOut": ".port_out",
"PronunciationSuggestion": ".pronunciation_suggestion",
"ProviderGroupOut": ".provider_group_out",
+ "RunStatusCounts": ".run_status_counts",
"RunsSummaryOut": ".runs_summary_out",
"SlugAvailabilityOut": ".slug_availability_out",
"SlugAvailabilityOutReason": ".slug_availability_out_reason",
@@ -333,6 +339,7 @@
"WorkflowDefinitionOutput": ".workflow_definition_output",
"WorkflowListItem": ".workflow_list_item",
"WorkflowListStatus": ".workflow_list_status",
+ "WorkflowNameAvailabilityOut": ".workflow_name_availability_out",
"WorkflowOut": ".workflow_out",
"WorkflowRunDataAudioOut": ".workflow_run_data_audio_out",
"WorkflowRunDataAudioOutStatus": ".workflow_run_data_audio_out_status",
@@ -367,6 +374,7 @@
"WorkflowRunOverviewValidatorOut": ".workflow_run_overview_validator_out",
"WorkflowRunOverviewValidatorOutStatus": ".workflow_run_overview_validator_out_status",
"WorkflowRunOverviewWorkflowOut": ".workflow_run_overview_workflow_out",
+ "WorkflowRunStartIn": ".workflow_run_start_in",
"WorkflowRunStatusOut": ".workflow_run_status_out",
"WorkflowRunStepOut": ".workflow_run_step_out",
"WorkspaceInviteOut": ".workspace_invite_out",
@@ -442,6 +450,7 @@ def __dir__():
"ApiResponseUsageSummaryOut",
"ApiResponseVoiceFacetsOut",
"ApiResponseVoiceOut",
+ "ApiResponseWorkflowNameAvailabilityOut",
"ApiResponseWorkflowOut",
"ApiResponseWorkflowRunDetailOut",
"ApiResponseWorkflowRunOut",
@@ -493,6 +502,7 @@ def __dir__():
"PortOut",
"PronunciationSuggestion",
"ProviderGroupOut",
+ "RunStatusCounts",
"RunsSummaryOut",
"SlugAvailabilityOut",
"SlugAvailabilityOutReason",
@@ -541,6 +551,7 @@ def __dir__():
"WorkflowDefinitionOutput",
"WorkflowListItem",
"WorkflowListStatus",
+ "WorkflowNameAvailabilityOut",
"WorkflowOut",
"WorkflowRunDataAudioOut",
"WorkflowRunDataAudioOutStatus",
@@ -575,6 +586,7 @@ def __dir__():
"WorkflowRunOverviewValidatorOut",
"WorkflowRunOverviewValidatorOutStatus",
"WorkflowRunOverviewWorkflowOut",
+ "WorkflowRunStartIn",
"WorkflowRunStatusOut",
"WorkflowRunStepOut",
"WorkspaceInviteOut",
diff --git a/src/onepin/types/api_response_workflow_name_availability_out.py b/src/onepin/types/api_response_workflow_name_availability_out.py
new file mode 100644
index 0000000..6766db8
--- /dev/null
+++ b/src/onepin/types/api_response_workflow_name_availability_out.py
@@ -0,0 +1,22 @@
+# This file was auto-generated by Fern from our API Definition.
+
+import typing
+
+import pydantic
+from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel
+from .meta import Meta
+from .workflow_name_availability_out import WorkflowNameAvailabilityOut
+
+
+class ApiResponseWorkflowNameAvailabilityOut(UniversalBaseModel):
+ data: WorkflowNameAvailabilityOut
+ meta: Meta
+
+ if IS_PYDANTIC_V2:
+ model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2
+ else:
+
+ class Config:
+ frozen = True
+ smart_union = True
+ extra = pydantic.Extra.allow
diff --git a/src/onepin/types/catalog_provider_out.py b/src/onepin/types/catalog_provider_out.py
index 64fb849..b1445f5 100644
--- a/src/onepin/types/catalog_provider_out.py
+++ b/src/onepin/types/catalog_provider_out.py
@@ -11,7 +11,7 @@ class CatalogProviderOut(UniversalBaseModel):
"""
Catalog provider entry — lean, customer-safe.
- Sourced from the in-memory ``ProviderRegistry`` (TTS / ``processing`` kind).
+ Sourced from the in-memory ``ProviderRegistry`` (TTS / ``operator`` kind).
Cost, credentials, base_url, and enabled flags are excluded by construction.
"""
diff --git a/src/onepin/types/node_category.py b/src/onepin/types/node_category.py
index 178b9bf..b4806e5 100644
--- a/src/onepin/types/node_category.py
+++ b/src/onepin/types/node_category.py
@@ -2,4 +2,4 @@
import typing
-NodeCategory = typing.Union[typing.Literal["source", "processing", "validation", "output", "decision"], typing.Any]
+NodeCategory = typing.Union[typing.Literal["source", "operator", "validation", "output", "decision"], typing.Any]
diff --git a/src/onepin/types/node_detail_out.py b/src/onepin/types/node_detail_out.py
index 21bdb15..4d5b151 100644
--- a/src/onepin/types/node_detail_out.py
+++ b/src/onepin/types/node_detail_out.py
@@ -21,6 +21,7 @@ class NodeDetailOut(UniversalBaseModel):
category: NodeCategory
inputs: typing.List[PortOut]
outputs: typing.List[PortOut]
+ input_schema: typing.Optional[typing.Dict[str, typing.Any]] = None
config_schema: typing.Optional[typing.Dict[str, typing.Any]] = None
options: typing.Optional[typing.Dict[str, NodeDetailOutOptionsValue]] = None
diff --git a/src/onepin/types/node_ports_out.py b/src/onepin/types/node_ports_out.py
index b7a0fa3..9d57ff5 100644
--- a/src/onepin/types/node_ports_out.py
+++ b/src/onepin/types/node_ports_out.py
@@ -14,6 +14,7 @@ class NodePortsOut(UniversalBaseModel):
version: int
inputs: typing.List[PortOut]
outputs: typing.List[PortOut]
+ input_schema: typing.Optional[typing.Dict[str, typing.Any]] = None
config_schema: typing.Optional[typing.Dict[str, typing.Any]] = None
if IS_PYDANTIC_V2:
diff --git a/src/onepin/types/node_type.py b/src/onepin/types/node_type.py
index 121623c..eb0cd2f 100644
--- a/src/onepin/types/node_type.py
+++ b/src/onepin/types/node_type.py
@@ -5,9 +5,9 @@
NodeType = typing.Union[
typing.Literal[
"source_script",
- "processing_translator",
- "processing_booster",
- "processing_generator",
+ "operator_translator",
+ "operator_normalizer",
+ "operator_generator",
"sink_preview",
"validator_error_rate",
"validator_naturalness",
diff --git a/src/onepin/types/plan_limits.py b/src/onepin/types/plan_limits.py
index 55907d4..71c2c19 100644
--- a/src/onepin/types/plan_limits.py
+++ b/src/onepin/types/plan_limits.py
@@ -8,25 +8,16 @@
class PlanLimits(UniversalBaseModel):
"""
- Typed view of `Plan.limits` JSONB. Source of truth for enforcement gates.
-
- `null` allowlists mean "all DB-enabled" (effective allowlist = tier list ∩ DB-enabled set).
- `null` projects_per_workspace = unlimited; `null` retention_days = custom (Enterprise).
+ Typed view of `Plan.limits` JSONB. Source of truth for active plan gates.
"""
monthly_credits: int
- seats: typing.Optional[int] = None
workspaces_per_owner: int
- projects_per_workspace: typing.Optional[int] = None
concurrent_runs_per_user: int
storage_bytes_per_workspace: int
retention_days: typing.Optional[int] = None
byok_enabled: typing.Optional[bool] = None
auto_fix_enabled: typing.Optional[bool] = None
- auto_edit_enabled: typing.Optional[bool] = None
- tts_models_allowlist: typing.Optional[typing.List[str]] = None
- supported_languages: typing.Optional[typing.List[str]] = None
- human_validation_languages: typing.Optional[typing.List[str]] = None
if IS_PYDANTIC_V2:
model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2
diff --git a/src/onepin/types/run_status_counts.py b/src/onepin/types/run_status_counts.py
new file mode 100644
index 0000000..7dfd57f
--- /dev/null
+++ b/src/onepin/types/run_status_counts.py
@@ -0,0 +1,58 @@
+# This file was auto-generated by Fern from our API Definition.
+
+import typing
+
+import pydantic
+from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel
+
+
+class RunStatusCounts(UniversalBaseModel):
+ """
+ Per-raw-``RunStatus`` run counts for a workflow.
+
+ Computed over ALL runs of the workflow — the same population as ``runs_count`` —
+ so it is NOT affected by the list-level ``status`` filter. Every raw status is
+ always present; a status with no runs is ``0``. The sum of all fields equals
+ ``runs_count``, and each field matches the ``pagination.total`` of
+ ``GET /workflows/{id}/runs?status=``. These are RAW status counts (not the
+ UI-derived ``WorkflowListStatus`` buckets), so the caller can group them per tab.
+ """
+
+ pending: typing.Optional[int] = pydantic.Field(default=None)
+ """
+ Runs queued but not yet started.
+ """
+
+ running: typing.Optional[int] = pydantic.Field(default=None)
+ """
+ Runs currently executing.
+ """
+
+ completed: typing.Optional[int] = pydantic.Field(default=None)
+ """
+ Runs that finished successfully.
+ """
+
+ failed: typing.Optional[int] = pydantic.Field(default=None)
+ """
+ Runs that ended in a failure state.
+ """
+
+ cancelled: typing.Optional[int] = pydantic.Field(default=None)
+ """
+ Runs explicitly cancelled by a user.
+ """
+
+ paused: typing.Optional[int] = pydantic.Field(default=None)
+ """
+ Runs currently paused/parked.
+ """
+
+ if IS_PYDANTIC_V2:
+ model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2
+ else:
+
+ class Config:
+ frozen = True
+ smart_union = True
+ extra = pydantic.Extra.allow
diff --git a/src/onepin/types/upload_out.py b/src/onepin/types/upload_out.py
index c96ba9d..40f39a4 100644
--- a/src/onepin/types/upload_out.py
+++ b/src/onepin/types/upload_out.py
@@ -53,6 +53,16 @@ class UploadOut(UniversalBaseModel):
File size in bytes. Populated after a successful confirm.
"""
+ char_count: typing.Optional[int] = pydantic.Field(default=None)
+ """
+ Decoded character count of the uploaded script, using the same definition as usage/billing (`unit_chars`): the sum of Unicode code-point lengths of each stripped, non-empty line (leading/trailing whitespace, blank lines, and newlines excluded). Populated after a successful confirm for `script` uploads in plain-text (`.txt`) format only. `null` while pending, for `.csv` (script column is chosen after confirm), for other formats, and for `dictionary` (audio) uploads.
+ """
+
+ line_count: typing.Optional[int] = pydantic.Field(default=None)
+ """
+ Number of non-empty lines in the uploaded script. Populated alongside `char_count` (same scope: confirmed `script` `.txt` uploads); `null` otherwise.
+ """
+
download_url: typing.Optional[str] = pydantic.Field(default=None)
"""
Short-lived presigned URL for downloading the confirmed file. `null` for pending uploads.
@@ -60,7 +70,7 @@ class UploadOut(UniversalBaseModel):
context_type: typing.Optional[str] = pydantic.Field(default=None)
"""
- Resource type this upload is attached to (e.g. `workflow`). Set at confirm time.
+ Resource type this upload is attached to (e.g. `workflow` or `playground`). Set at confirm time.
"""
context_id: typing.Optional[str] = pydantic.Field(default=None)
diff --git a/src/onepin/types/workflow_list_item.py b/src/onepin/types/workflow_list_item.py
index b745eae..9cb2a30 100644
--- a/src/onepin/types/workflow_list_item.py
+++ b/src/onepin/types/workflow_list_item.py
@@ -5,6 +5,7 @@
import pydantic
from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel
+from .run_status_counts import RunStatusCounts
class WorkflowListItem(UniversalBaseModel):
@@ -53,6 +54,11 @@ class WorkflowListItem(UniversalBaseModel):
Raw RunStatus of the most recent run. One of: `pending`, `running`, `completed`, `failed`, `cancelled`, `paused`. Null if never run.
"""
+ run_status_counts: typing.Optional[RunStatusCounts] = pydantic.Field(default=None)
+ """
+ Number of runs per raw RunStatus, computed over ALL runs of this workflow (same population as `runs_count`; NOT affected by the list-level `status` filter). The sum of all values equals `runs_count`; statuses with no runs are 0.
+ """
+
if IS_PYDANTIC_V2:
model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2
else:
diff --git a/src/onepin/types/workflow_name_availability_out.py b/src/onepin/types/workflow_name_availability_out.py
new file mode 100644
index 0000000..591fae0
--- /dev/null
+++ b/src/onepin/types/workflow_name_availability_out.py
@@ -0,0 +1,31 @@
+# This file was auto-generated by Fern from our API Definition.
+
+import typing
+
+import pydantic
+from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel
+
+
+class WorkflowNameAvailabilityOut(UniversalBaseModel):
+ """
+ Result of a workflow-name availability check within a workspace.
+ """
+
+ name: str = pydantic.Field()
+ """
+ The normalized (trimmed) name that was checked.
+ """
+
+ available: bool = pydantic.Field()
+ """
+ True when no live workflow in the workspace already uses this name.
+ """
+
+ if IS_PYDANTIC_V2:
+ model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2
+ else:
+
+ class Config:
+ frozen = True
+ smart_union = True
+ extra = pydantic.Extra.allow
diff --git a/src/onepin/types/workflow_out.py b/src/onepin/types/workflow_out.py
index 46e9e87..5e16cb5 100644
--- a/src/onepin/types/workflow_out.py
+++ b/src/onepin/types/workflow_out.py
@@ -5,6 +5,7 @@
import pydantic
from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel
+from .run_status_counts import RunStatusCounts
class WorkflowOut(UniversalBaseModel):
@@ -58,6 +59,11 @@ class WorkflowOut(UniversalBaseModel):
Raw RunStatus of the most recent run. One of: `pending`, `running`, `completed`, `failed`, `cancelled`, `paused`. Null if never run.
"""
+ run_status_counts: typing.Optional[RunStatusCounts] = pydantic.Field(default=None)
+ """
+ Number of runs per raw RunStatus, computed over ALL runs of this workflow (same population as `runs_count`; NOT affected by the list-level `status` filter). The sum of all values equals `runs_count`; statuses with no runs are 0.
+ """
+
if IS_PYDANTIC_V2:
model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2
else:
diff --git a/src/onepin/types/workflow_run_data_validation_out.py b/src/onepin/types/workflow_run_data_validation_out.py
index 71299fc..4e01c17 100644
--- a/src/onepin/types/workflow_run_data_validation_out.py
+++ b/src/onepin/types/workflow_run_data_validation_out.py
@@ -14,6 +14,11 @@ class WorkflowRunDataValidationOut(UniversalBaseModel):
WER entries populate ``wer``/``cer``/``transcript``; naturalness entries
populate ``score`` (0-100). All fields except ``kind`` and ``status`` are
optional so future validator kinds can omit inapplicable fields.
+
+ ``scored_on`` records which error metric actually drove ``score`` for the
+ word-accuracy (WER) kind: ``"wer"`` for space-delimited scripts, ``"cer"``
+ for space-less scripts (ja/zh/th/…) where word-level WER degenerates. It is
+ ``None`` for kinds that don't score on an error rate (e.g. naturalness).
"""
kind: str
@@ -24,6 +29,7 @@ class WorkflowRunDataValidationOut(UniversalBaseModel):
wer: typing.Optional[float] = None
cer: typing.Optional[float] = None
transcript: typing.Optional[str] = None
+ scored_on: typing.Optional[str] = None
if IS_PYDANTIC_V2:
model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2
diff --git a/src/onepin/types/workflow_run_start_in.py b/src/onepin/types/workflow_run_start_in.py
new file mode 100644
index 0000000..42b86d3
--- /dev/null
+++ b/src/onepin/types/workflow_run_start_in.py
@@ -0,0 +1,36 @@
+# This file was auto-generated by Fern from our API Definition.
+
+import typing
+
+import pydantic
+from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel
+
+
+class WorkflowRunStartIn(UniversalBaseModel):
+ """
+ Optional run-scoped inputs for ``POST /runs``.
+
+ Overrides are applied to THIS run's definition snapshot only — the stored
+ workflow definition is never mutated, so concurrent runs with different
+ scripts cannot race each other. Requires the workflow graph to contain
+ exactly one ``source_script`` node.
+ """
+
+ script_text: typing.Optional[str] = pydantic.Field(default=None)
+ """
+ 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] = pydantic.Field(default=None)
+ """
+ BCP-47 language of script_text (e.g. en-us). Optional; when omitted the saved source_language (or automatic detection) applies.
+ """
+
+ if IS_PYDANTIC_V2:
+ model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2
+ else:
+
+ class Config:
+ frozen = True
+ smart_union = True
+ extra = pydantic.Extra.allow
diff --git a/src/onepin/uploads/client.py b/src/onepin/uploads/client.py
index 52336ca..ca8c311 100644
--- a/src/onepin/uploads/client.py
+++ b/src/onepin/uploads/client.py
@@ -133,7 +133,7 @@ def confirm(
upload_id : str
context_type : UploadConfirmRequestContextType
- Type of resource this upload is being attached to. Currently only `workflow` is supported.
+ Type of resource this upload is being attached to: `workflow` or `playground`.
context_id : str
ID of the resource to attach this upload to. Must be an existing resource of the given `context_type` that the caller has access to.
@@ -349,7 +349,7 @@ async def confirm(
upload_id : str
context_type : UploadConfirmRequestContextType
- Type of resource this upload is being attached to. Currently only `workflow` is supported.
+ Type of resource this upload is being attached to: `workflow` or `playground`.
context_id : str
ID of the resource to attach this upload to. Must be an existing resource of the given `context_type` that the caller has access to.
diff --git a/src/onepin/uploads/raw_client.py b/src/onepin/uploads/raw_client.py
index c91f170..01b6a6a 100644
--- a/src/onepin/uploads/raw_client.py
+++ b/src/onepin/uploads/raw_client.py
@@ -156,7 +156,7 @@ def confirm(
upload_id : str
context_type : UploadConfirmRequestContextType
- Type of resource this upload is being attached to. Currently only `workflow` is supported.
+ Type of resource this upload is being attached to: `workflow` or `playground`.
context_id : str
ID of the resource to attach this upload to. Must be an existing resource of the given `context_type` that the caller has access to.
@@ -426,7 +426,7 @@ async def confirm(
upload_id : str
context_type : UploadConfirmRequestContextType
- Type of resource this upload is being attached to. Currently only `workflow` is supported.
+ Type of resource this upload is being attached to: `workflow` or `playground`.
context_id : str
ID of the resource to attach this upload to. Must be an existing resource of the given `context_type` that the caller has access to.
diff --git a/src/onepin/uploads/types/upload_confirm_request_context_type.py b/src/onepin/uploads/types/upload_confirm_request_context_type.py
index 02be43a..182e6ce 100644
--- a/src/onepin/uploads/types/upload_confirm_request_context_type.py
+++ b/src/onepin/uploads/types/upload_confirm_request_context_type.py
@@ -2,4 +2,4 @@
import typing
-UploadConfirmRequestContextType = typing.Union[typing.Literal["workflow"], typing.Any]
+UploadConfirmRequestContextType = typing.Union[typing.Literal["workflow", "playground"], typing.Any]
diff --git a/src/onepin/users/client.py b/src/onepin/users/client.py
index bf12397..4de6674 100644
--- a/src/onepin/users/client.py
+++ b/src/onepin/users/client.py
@@ -74,10 +74,9 @@ def get_my_plan_limits(self, *, request_options: typing.Optional[RequestOptions]
Includes numeric quotas (`monthly_credits`, `concurrent_runs_per_user`,
`storage_bytes_per_workspace`, `workspaces_per_owner`) and feature flags
- (`byok_enabled`, `auto_fix_enabled`, `auto_edit_enabled`). `null` on list
- fields such as `tts_models_allowlist` or `supported_languages` means all
- available options are permitted. Use this endpoint to gate feature access in
- your application rather than hardcoding tier names, which may change.
+ (`byok_enabled`, `auto_fix_enabled`). Use this endpoint to gate feature
+ access in your application rather than hardcoding tier names, which may
+ change.
Parameters
----------
@@ -319,10 +318,9 @@ async def get_my_plan_limits(
Includes numeric quotas (`monthly_credits`, `concurrent_runs_per_user`,
`storage_bytes_per_workspace`, `workspaces_per_owner`) and feature flags
- (`byok_enabled`, `auto_fix_enabled`, `auto_edit_enabled`). `null` on list
- fields such as `tts_models_allowlist` or `supported_languages` means all
- available options are permitted. Use this endpoint to gate feature access in
- your application rather than hardcoding tier names, which may change.
+ (`byok_enabled`, `auto_fix_enabled`). Use this endpoint to gate feature
+ access in your application rather than hardcoding tier names, which may
+ change.
Parameters
----------
diff --git a/src/onepin/users/raw_client.py b/src/onepin/users/raw_client.py
index e252fe4..6da6376 100644
--- a/src/onepin/users/raw_client.py
+++ b/src/onepin/users/raw_client.py
@@ -96,10 +96,9 @@ def get_my_plan_limits(
Includes numeric quotas (`monthly_credits`, `concurrent_runs_per_user`,
`storage_bytes_per_workspace`, `workspaces_per_owner`) and feature flags
- (`byok_enabled`, `auto_fix_enabled`, `auto_edit_enabled`). `null` on list
- fields such as `tts_models_allowlist` or `supported_languages` means all
- available options are permitted. Use this endpoint to gate feature access in
- your application rather than hardcoding tier names, which may change.
+ (`byok_enabled`, `auto_fix_enabled`). Use this endpoint to gate feature
+ access in your application rather than hardcoding tier names, which may
+ change.
Parameters
----------
@@ -443,10 +442,9 @@ async def get_my_plan_limits(
Includes numeric quotas (`monthly_credits`, `concurrent_runs_per_user`,
`storage_bytes_per_workspace`, `workspaces_per_owner`) and feature flags
- (`byok_enabled`, `auto_fix_enabled`, `auto_edit_enabled`). `null` on list
- fields such as `tts_models_allowlist` or `supported_languages` means all
- available options are permitted. Use this endpoint to gate feature access in
- your application rather than hardcoding tier names, which may change.
+ (`byok_enabled`, `auto_fix_enabled`). Use this endpoint to gate feature
+ access in your application rather than hardcoding tier names, which may
+ change.
Parameters
----------
diff --git a/src/onepin/voices/__init__.py b/src/onepin/voices/__init__.py
index 2a2b490..ce7c6af 100644
--- a/src/onepin/voices/__init__.py
+++ b/src/onepin/voices/__init__.py
@@ -10,7 +10,6 @@
GetVoiceFacetsApiV1VoicesFacetsGetRequestSourceItem,
ListVoicesRequestLanguageItem,
ListVoicesRequestOrderItem,
- ListVoicesRequestProviderItem,
ListVoicesRequestSortItem,
ListVoicesRequestSourceItem,
)
@@ -18,7 +17,6 @@
"GetVoiceFacetsApiV1VoicesFacetsGetRequestSourceItem": ".types",
"ListVoicesRequestLanguageItem": ".types",
"ListVoicesRequestOrderItem": ".types",
- "ListVoicesRequestProviderItem": ".types",
"ListVoicesRequestSortItem": ".types",
"ListVoicesRequestSourceItem": ".types",
}
@@ -49,7 +47,6 @@ def __dir__():
"GetVoiceFacetsApiV1VoicesFacetsGetRequestSourceItem",
"ListVoicesRequestLanguageItem",
"ListVoicesRequestOrderItem",
- "ListVoicesRequestProviderItem",
"ListVoicesRequestSortItem",
"ListVoicesRequestSourceItem",
]
diff --git a/src/onepin/voices/client.py b/src/onepin/voices/client.py
index 7982af1..36cfe30 100644
--- a/src/onepin/voices/client.py
+++ b/src/onepin/voices/client.py
@@ -19,7 +19,6 @@
)
from .types.list_voices_request_language_item import ListVoicesRequestLanguageItem
from .types.list_voices_request_order_item import ListVoicesRequestOrderItem
-from .types.list_voices_request_provider_item import ListVoicesRequestProviderItem
from .types.list_voices_request_sort_item import ListVoicesRequestSortItem
from .types.list_voices_request_source_item import ListVoicesRequestSourceItem
@@ -53,7 +52,7 @@ def list(
search: typing.Optional[str] = None,
sort: typing.Optional[typing.Sequence[ListVoicesRequestSortItem]] = None,
order: typing.Optional[typing.Sequence[ListVoicesRequestOrderItem]] = None,
- provider: typing.Optional[typing.Sequence[ListVoicesRequestProviderItem]] = None,
+ provider: typing.Optional[typing.Sequence[str]] = None,
model: typing.Optional[typing.Sequence[str]] = None,
language: typing.Optional[typing.Sequence[ListVoicesRequestLanguageItem]] = None,
workspace_id: typing.Optional[str] = None,
@@ -115,7 +114,7 @@ def list(
order : typing.Optional[typing.Sequence[ListVoicesRequestOrderItem]]
Parallel to sort[]; shorter is padded with per-field defaults.
- provider : typing.Optional[typing.Sequence[ListVoicesRequestProviderItem]]
+ provider : typing.Optional[typing.Sequence[str]]
Repeat for OR, e.g. ?provider=elevenlabs&provider=rime
model : typing.Optional[typing.Sequence[str]]
@@ -493,7 +492,7 @@ async def list(
search: typing.Optional[str] = None,
sort: typing.Optional[typing.Sequence[ListVoicesRequestSortItem]] = None,
order: typing.Optional[typing.Sequence[ListVoicesRequestOrderItem]] = None,
- provider: typing.Optional[typing.Sequence[ListVoicesRequestProviderItem]] = None,
+ provider: typing.Optional[typing.Sequence[str]] = None,
model: typing.Optional[typing.Sequence[str]] = None,
language: typing.Optional[typing.Sequence[ListVoicesRequestLanguageItem]] = None,
workspace_id: typing.Optional[str] = None,
@@ -555,7 +554,7 @@ async def list(
order : typing.Optional[typing.Sequence[ListVoicesRequestOrderItem]]
Parallel to sort[]; shorter is padded with per-field defaults.
- provider : typing.Optional[typing.Sequence[ListVoicesRequestProviderItem]]
+ provider : typing.Optional[typing.Sequence[str]]
Repeat for OR, e.g. ?provider=elevenlabs&provider=rime
model : typing.Optional[typing.Sequence[str]]
diff --git a/src/onepin/voices/raw_client.py b/src/onepin/voices/raw_client.py
index 3894067..ce39276 100644
--- a/src/onepin/voices/raw_client.py
+++ b/src/onepin/voices/raw_client.py
@@ -25,7 +25,6 @@
)
from .types.list_voices_request_language_item import ListVoicesRequestLanguageItem
from .types.list_voices_request_order_item import ListVoicesRequestOrderItem
-from .types.list_voices_request_provider_item import ListVoicesRequestProviderItem
from .types.list_voices_request_sort_item import ListVoicesRequestSortItem
from .types.list_voices_request_source_item import ListVoicesRequestSourceItem
from pydantic import ValidationError
@@ -49,7 +48,7 @@ def list(
search: typing.Optional[str] = None,
sort: typing.Optional[typing.Sequence[ListVoicesRequestSortItem]] = None,
order: typing.Optional[typing.Sequence[ListVoicesRequestOrderItem]] = None,
- provider: typing.Optional[typing.Sequence[ListVoicesRequestProviderItem]] = None,
+ provider: typing.Optional[typing.Sequence[str]] = None,
model: typing.Optional[typing.Sequence[str]] = None,
language: typing.Optional[typing.Sequence[ListVoicesRequestLanguageItem]] = None,
workspace_id: typing.Optional[str] = None,
@@ -111,7 +110,7 @@ def list(
order : typing.Optional[typing.Sequence[ListVoicesRequestOrderItem]]
Parallel to sort[]; shorter is padded with per-field defaults.
- provider : typing.Optional[typing.Sequence[ListVoicesRequestProviderItem]]
+ provider : typing.Optional[typing.Sequence[str]]
Repeat for OR, e.g. ?provider=elevenlabs&provider=rime
model : typing.Optional[typing.Sequence[str]]
@@ -622,7 +621,7 @@ async def list(
search: typing.Optional[str] = None,
sort: typing.Optional[typing.Sequence[ListVoicesRequestSortItem]] = None,
order: typing.Optional[typing.Sequence[ListVoicesRequestOrderItem]] = None,
- provider: typing.Optional[typing.Sequence[ListVoicesRequestProviderItem]] = None,
+ provider: typing.Optional[typing.Sequence[str]] = None,
model: typing.Optional[typing.Sequence[str]] = None,
language: typing.Optional[typing.Sequence[ListVoicesRequestLanguageItem]] = None,
workspace_id: typing.Optional[str] = None,
@@ -684,7 +683,7 @@ async def list(
order : typing.Optional[typing.Sequence[ListVoicesRequestOrderItem]]
Parallel to sort[]; shorter is padded with per-field defaults.
- provider : typing.Optional[typing.Sequence[ListVoicesRequestProviderItem]]
+ provider : typing.Optional[typing.Sequence[str]]
Repeat for OR, e.g. ?provider=elevenlabs&provider=rime
model : typing.Optional[typing.Sequence[str]]
diff --git a/src/onepin/voices/types/__init__.py b/src/onepin/voices/types/__init__.py
index 7582c29..3c4972a 100644
--- a/src/onepin/voices/types/__init__.py
+++ b/src/onepin/voices/types/__init__.py
@@ -11,14 +11,12 @@
)
from .list_voices_request_language_item import ListVoicesRequestLanguageItem
from .list_voices_request_order_item import ListVoicesRequestOrderItem
- from .list_voices_request_provider_item import ListVoicesRequestProviderItem
from .list_voices_request_sort_item import ListVoicesRequestSortItem
from .list_voices_request_source_item import ListVoicesRequestSourceItem
_dynamic_imports: typing.Dict[str, str] = {
"GetVoiceFacetsApiV1VoicesFacetsGetRequestSourceItem": ".get_voice_facets_api_v1voices_facets_get_request_source_item",
"ListVoicesRequestLanguageItem": ".list_voices_request_language_item",
"ListVoicesRequestOrderItem": ".list_voices_request_order_item",
- "ListVoicesRequestProviderItem": ".list_voices_request_provider_item",
"ListVoicesRequestSortItem": ".list_voices_request_sort_item",
"ListVoicesRequestSourceItem": ".list_voices_request_source_item",
}
@@ -49,7 +47,6 @@ def __dir__():
"GetVoiceFacetsApiV1VoicesFacetsGetRequestSourceItem",
"ListVoicesRequestLanguageItem",
"ListVoicesRequestOrderItem",
- "ListVoicesRequestProviderItem",
"ListVoicesRequestSortItem",
"ListVoicesRequestSourceItem",
]
diff --git a/src/onepin/voices/types/list_voices_request_provider_item.py b/src/onepin/voices/types/list_voices_request_provider_item.py
deleted file mode 100644
index c52f936..0000000
--- a/src/onepin/voices/types/list_voices_request_provider_item.py
+++ /dev/null
@@ -1,7 +0,0 @@
-# This file was auto-generated by Fern from our API Definition.
-
-import typing
-
-ListVoicesRequestProviderItem = typing.Union[
- typing.Literal["cartesia", "elevenlabs", "naver", "respeecher", "rime"], typing.Any
-]
diff --git a/src/onepin/workflows/client.py b/src/onepin/workflows/client.py
index c941e78..7eb61fa 100644
--- a/src/onepin/workflows/client.py
+++ b/src/onepin/workflows/client.py
@@ -14,6 +14,7 @@
from ..types.api_response_estimate_response import ApiResponseEstimateResponse
from ..types.api_response_list_workflow_run_step_out import ApiResponseListWorkflowRunStepOut
from ..types.api_response_runs_summary_out import ApiResponseRunsSummaryOut
+from ..types.api_response_workflow_name_availability_out import ApiResponseWorkflowNameAvailabilityOut
from ..types.api_response_workflow_out import ApiResponseWorkflowOut
from ..types.api_response_workflow_run_out import ApiResponseWorkflowRunOut
from ..types.api_response_workflow_run_overview_out import ApiResponseWorkflowRunOverviewOut
@@ -67,7 +68,10 @@ def list(
Returns a counted, paginated list of workflows scoped to the `X-Workspace-Id`
header. Each item includes aggregate stats (`runs_count`, `last_run_at`,
- `last_run_status`) computed over all runs for that workflow.
+ `last_run_status`, `run_status_counts`) computed over all runs for that
+ workflow. `run_status_counts` is a per-raw-`RunStatus` map whose values sum to
+ `runs_count` and are NOT affected by the `status` filter below, so a collapsed
+ row can render an accurate per-tab total without a separate runs query.
**Status filter:** `status` narrows by the UI-derived state of the workflow's
most recent run. `completed` matches only workflows whose latest run succeeded
@@ -222,6 +226,58 @@ def create_workflow(
)
return _response.data
+ def check_workflow_name_availability(
+ self,
+ *,
+ name: str,
+ exclude_id: typing.Optional[str] = None,
+ workspace_id: typing.Optional[str] = None,
+ request_options: typing.Optional[RequestOptions] = None,
+ ) -> ApiResponseWorkflowNameAvailabilityOut:
+ """
+ Check whether a workflow name is free within the current workspace.
+
+ Workflow names are unique per workspace among live (non-deleted) workflows,
+ so this lets a client validate a name before create or rename. The `name` is
+ trimmed and validated with the same policy as create — an invalid name
+ returns 422. The check is case-sensitive and ignores soft-deleted workflows,
+ mirroring the underlying uniqueness constraint. Pass `exclude_id` when
+ renaming so the workflow's current name is not reported as taken by itself.
+
+ Parameters
+ ----------
+ name : str
+ Workflow name to check (trimmed before comparison).
+
+ exclude_id : typing.Optional[str]
+ Workflow to exclude from the check — its own name then reads as available. Pass the workflow's id when validating a rename.
+
+ workspace_id : typing.Optional[str]
+
+ request_options : typing.Optional[RequestOptions]
+ Request-specific configuration.
+
+ Returns
+ -------
+ ApiResponseWorkflowNameAvailabilityOut
+ Successful Response
+
+ Examples
+ --------
+ from onepin import OnePinClient
+
+ client = OnePinClient(
+ token="YOUR_TOKEN",
+ )
+ client.workflows.check_workflow_name_availability(
+ name="name",
+ )
+ """
+ _response = self._raw_client.check_workflow_name_availability(
+ name=name, exclude_id=exclude_id, workspace_id=workspace_id, request_options=request_options
+ )
+ return _response.data
+
def get(
self,
workflow_id: str,
@@ -668,8 +724,10 @@ def get_run_steps(
a separate download step.
`node_display_name` is resolved from the run's definition snapshot, so it
- reflects the name the node had when the run executed. Nodes that were
- retried appear as multiple steps with incrementing `iteration` values.
+ reflects the name the node had when the run executed. When multiple language
+ branches reach one output node, repeated sink steps receive locale suffixes
+ based on the language introduced by each iteration. Other retried nodes keep
+ their snapshot display name and incrementing `iteration` value.
For a higher-level view with aggregated metrics (pass rates, audio duration
by language), use `GET /runs/{run_id}/overview`. For paginated, grouped
@@ -724,6 +782,10 @@ def get_run_overview(
breakdowns and per-validator scoring summaries. Also includes a
`workflow_snapshot` with the graph definition and per-node completion states.
+ Repeated states for one shared output node use the same locale suffixes as
+ `GET /runs/{run_id}/steps`, based on the language introduced by each sink
+ iteration.
+
This endpoint is best suited for a summary/results view after a run
completes. It differs from the other run sub-resources as follows:
@@ -929,7 +991,7 @@ def download_run_node(
`node_id` must identify an output-category node in the run's definition
snapshot. Passing a node ID that belongs to a non-output node type (e.g.
- a processing or validation node) returns 404. Returns 404 if the node
+ an operator or validation node) returns 404. Returns 404 if the node
produced no audio files, and 409 if the run has not yet completed.
The URL is valid for 15 minutes. To download all output nodes in a single
@@ -1170,7 +1232,10 @@ async def list(
Returns a counted, paginated list of workflows scoped to the `X-Workspace-Id`
header. Each item includes aggregate stats (`runs_count`, `last_run_at`,
- `last_run_status`) computed over all runs for that workflow.
+ `last_run_status`, `run_status_counts`) computed over all runs for that
+ workflow. `run_status_counts` is a per-raw-`RunStatus` map whose values sum to
+ `runs_count` and are NOT affected by the `status` filter below, so a collapsed
+ row can render an accurate per-tab total without a separate runs query.
**Status filter:** `status` narrows by the UI-derived state of the workflow's
most recent run. `completed` matches only workflows whose latest run succeeded
@@ -1341,6 +1406,66 @@ async def main() -> None:
)
return _response.data
+ async def check_workflow_name_availability(
+ self,
+ *,
+ name: str,
+ exclude_id: typing.Optional[str] = None,
+ workspace_id: typing.Optional[str] = None,
+ request_options: typing.Optional[RequestOptions] = None,
+ ) -> ApiResponseWorkflowNameAvailabilityOut:
+ """
+ Check whether a workflow name is free within the current workspace.
+
+ Workflow names are unique per workspace among live (non-deleted) workflows,
+ so this lets a client validate a name before create or rename. The `name` is
+ trimmed and validated with the same policy as create — an invalid name
+ returns 422. The check is case-sensitive and ignores soft-deleted workflows,
+ mirroring the underlying uniqueness constraint. Pass `exclude_id` when
+ renaming so the workflow's current name is not reported as taken by itself.
+
+ Parameters
+ ----------
+ name : str
+ Workflow name to check (trimmed before comparison).
+
+ exclude_id : typing.Optional[str]
+ Workflow to exclude from the check — its own name then reads as available. Pass the workflow's id when validating a rename.
+
+ workspace_id : typing.Optional[str]
+
+ request_options : typing.Optional[RequestOptions]
+ Request-specific configuration.
+
+ Returns
+ -------
+ ApiResponseWorkflowNameAvailabilityOut
+ Successful Response
+
+ Examples
+ --------
+ import asyncio
+
+ from onepin import AsyncOnePinClient
+
+ client = AsyncOnePinClient(
+ token="YOUR_TOKEN",
+ )
+
+
+ async def main() -> None:
+ await client.workflows.check_workflow_name_availability(
+ name="name",
+ )
+
+
+ asyncio.run(main())
+ """
+ _response = await self._raw_client.check_workflow_name_availability(
+ name=name, exclude_id=exclude_id, workspace_id=workspace_id, request_options=request_options
+ )
+ return _response.data
+
async def get(
self,
workflow_id: str,
@@ -1851,8 +1976,10 @@ async def get_run_steps(
a separate download step.
`node_display_name` is resolved from the run's definition snapshot, so it
- reflects the name the node had when the run executed. Nodes that were
- retried appear as multiple steps with incrementing `iteration` values.
+ reflects the name the node had when the run executed. When multiple language
+ branches reach one output node, repeated sink steps receive locale suffixes
+ based on the language introduced by each iteration. Other retried nodes keep
+ their snapshot display name and incrementing `iteration` value.
For a higher-level view with aggregated metrics (pass rates, audio duration
by language), use `GET /runs/{run_id}/overview`. For paginated, grouped
@@ -1915,6 +2042,10 @@ async def get_run_overview(
breakdowns and per-validator scoring summaries. Also includes a
`workflow_snapshot` with the graph definition and per-node completion states.
+ Repeated states for one shared output node use the same locale suffixes as
+ `GET /runs/{run_id}/steps`, based on the language introduced by each sink
+ iteration.
+
This endpoint is best suited for a summary/results view after a run
completes. It differs from the other run sub-resources as follows:
@@ -2144,7 +2275,7 @@ async def download_run_node(
`node_id` must identify an output-category node in the run's definition
snapshot. Passing a node ID that belongs to a non-output node type (e.g.
- a processing or validation node) returns 404. Returns 404 if the node
+ an operator or validation node) returns 404. Returns 404 if the node
produced no audio files, and 409 if the run has not yet completed.
The URL is valid for 15 minutes. To download all output nodes in a single
diff --git a/src/onepin/workflows/raw_client.py b/src/onepin/workflows/raw_client.py
index 28712e1..e8ce115 100644
--- a/src/onepin/workflows/raw_client.py
+++ b/src/onepin/workflows/raw_client.py
@@ -23,6 +23,7 @@
from ..types.api_response_estimate_response import ApiResponseEstimateResponse
from ..types.api_response_list_workflow_run_step_out import ApiResponseListWorkflowRunStepOut
from ..types.api_response_runs_summary_out import ApiResponseRunsSummaryOut
+from ..types.api_response_workflow_name_availability_out import ApiResponseWorkflowNameAvailabilityOut
from ..types.api_response_workflow_out import ApiResponseWorkflowOut
from ..types.api_response_workflow_run_out import ApiResponseWorkflowRunOut
from ..types.api_response_workflow_run_overview_out import ApiResponseWorkflowRunOverviewOut
@@ -61,7 +62,10 @@ def list(
Returns a counted, paginated list of workflows scoped to the `X-Workspace-Id`
header. Each item includes aggregate stats (`runs_count`, `last_run_at`,
- `last_run_status`) computed over all runs for that workflow.
+ `last_run_status`, `run_status_counts`) computed over all runs for that
+ workflow. `run_status_counts` is a per-raw-`RunStatus` map whose values sum to
+ `runs_count` and are NOT affected by the `status` filter below, so a collapsed
+ row can render an accurate per-tab total without a separate runs query.
**Status filter:** `status` narrows by the UI-derived state of the workflow's
most recent run. `completed` matches only workflows whose latest run succeeded
@@ -268,6 +272,84 @@ def create_workflow(
)
raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json)
+ def check_workflow_name_availability(
+ self,
+ *,
+ name: str,
+ exclude_id: typing.Optional[str] = None,
+ workspace_id: typing.Optional[str] = None,
+ request_options: typing.Optional[RequestOptions] = None,
+ ) -> HttpResponse[ApiResponseWorkflowNameAvailabilityOut]:
+ """
+ Check whether a workflow name is free within the current workspace.
+
+ Workflow names are unique per workspace among live (non-deleted) workflows,
+ so this lets a client validate a name before create or rename. The `name` is
+ trimmed and validated with the same policy as create — an invalid name
+ returns 422. The check is case-sensitive and ignores soft-deleted workflows,
+ mirroring the underlying uniqueness constraint. Pass `exclude_id` when
+ renaming so the workflow's current name is not reported as taken by itself.
+
+ Parameters
+ ----------
+ name : str
+ Workflow name to check (trimmed before comparison).
+
+ exclude_id : typing.Optional[str]
+ Workflow to exclude from the check — its own name then reads as available. Pass the workflow's id when validating a rename.
+
+ workspace_id : typing.Optional[str]
+
+ request_options : typing.Optional[RequestOptions]
+ Request-specific configuration.
+
+ Returns
+ -------
+ HttpResponse[ApiResponseWorkflowNameAvailabilityOut]
+ Successful Response
+ """
+ _response = self._client_wrapper.httpx_client.request(
+ "api/v1/workflows/name-availability",
+ method="GET",
+ params={
+ "name": name,
+ "exclude_id": exclude_id,
+ },
+ headers={
+ "X-Workspace-Id": str(workspace_id) if workspace_id is not None else None,
+ },
+ request_options=request_options,
+ )
+ try:
+ if 200 <= _response.status_code < 300:
+ _data = typing.cast(
+ ApiResponseWorkflowNameAvailabilityOut,
+ parse_obj_as(
+ type_=ApiResponseWorkflowNameAvailabilityOut, # type: ignore
+ object_=_response.json(),
+ ),
+ )
+ return HttpResponse(response=_response, data=_data)
+ 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 get(
self,
workflow_id: str,
@@ -906,8 +988,10 @@ def get_run_steps(
a separate download step.
`node_display_name` is resolved from the run's definition snapshot, so it
- reflects the name the node had when the run executed. Nodes that were
- retried appear as multiple steps with incrementing `iteration` values.
+ reflects the name the node had when the run executed. When multiple language
+ branches reach one output node, repeated sink steps receive locale suffixes
+ based on the language introduced by each iteration. Other retried nodes keep
+ their snapshot display name and incrementing `iteration` value.
For a higher-level view with aggregated metrics (pass rates, audio duration
by language), use `GET /runs/{run_id}/overview`. For paginated, grouped
@@ -983,6 +1067,10 @@ def get_run_overview(
breakdowns and per-validator scoring summaries. Also includes a
`workflow_snapshot` with the graph definition and per-node completion states.
+ Repeated states for one shared output node use the same locale suffixes as
+ `GET /runs/{run_id}/steps`, based on the language introduced by each sink
+ iteration.
+
This endpoint is best suited for a summary/results view after a run
completes. It differs from the other run sub-resources as follows:
@@ -1272,7 +1360,7 @@ def download_run_node(
`node_id` must identify an output-category node in the run's definition
snapshot. Passing a node ID that belongs to a non-output node type (e.g.
- a processing or validation node) returns 404. Returns 404 if the node
+ an operator or validation node) returns 404. Returns 404 if the node
produced no audio files, and 409 if the run has not yet completed.
The URL is valid for 15 minutes. To download all output nodes in a single
@@ -1598,7 +1686,10 @@ async def list(
Returns a counted, paginated list of workflows scoped to the `X-Workspace-Id`
header. Each item includes aggregate stats (`runs_count`, `last_run_at`,
- `last_run_status`) computed over all runs for that workflow.
+ `last_run_status`, `run_status_counts`) computed over all runs for that
+ workflow. `run_status_counts` is a per-raw-`RunStatus` map whose values sum to
+ `runs_count` and are NOT affected by the `status` filter below, so a collapsed
+ row can render an accurate per-tab total without a separate runs query.
**Status filter:** `status` narrows by the UI-derived state of the workflow's
most recent run. `completed` matches only workflows whose latest run succeeded
@@ -1805,6 +1896,84 @@ async def create_workflow(
)
raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json)
+ async def check_workflow_name_availability(
+ self,
+ *,
+ name: str,
+ exclude_id: typing.Optional[str] = None,
+ workspace_id: typing.Optional[str] = None,
+ request_options: typing.Optional[RequestOptions] = None,
+ ) -> AsyncHttpResponse[ApiResponseWorkflowNameAvailabilityOut]:
+ """
+ Check whether a workflow name is free within the current workspace.
+
+ Workflow names are unique per workspace among live (non-deleted) workflows,
+ so this lets a client validate a name before create or rename. The `name` is
+ trimmed and validated with the same policy as create — an invalid name
+ returns 422. The check is case-sensitive and ignores soft-deleted workflows,
+ mirroring the underlying uniqueness constraint. Pass `exclude_id` when
+ renaming so the workflow's current name is not reported as taken by itself.
+
+ Parameters
+ ----------
+ name : str
+ Workflow name to check (trimmed before comparison).
+
+ exclude_id : typing.Optional[str]
+ Workflow to exclude from the check — its own name then reads as available. Pass the workflow's id when validating a rename.
+
+ workspace_id : typing.Optional[str]
+
+ request_options : typing.Optional[RequestOptions]
+ Request-specific configuration.
+
+ Returns
+ -------
+ AsyncHttpResponse[ApiResponseWorkflowNameAvailabilityOut]
+ Successful Response
+ """
+ _response = await self._client_wrapper.httpx_client.request(
+ "api/v1/workflows/name-availability",
+ method="GET",
+ params={
+ "name": name,
+ "exclude_id": exclude_id,
+ },
+ headers={
+ "X-Workspace-Id": str(workspace_id) if workspace_id is not None else None,
+ },
+ request_options=request_options,
+ )
+ try:
+ if 200 <= _response.status_code < 300:
+ _data = typing.cast(
+ ApiResponseWorkflowNameAvailabilityOut,
+ parse_obj_as(
+ type_=ApiResponseWorkflowNameAvailabilityOut, # type: ignore
+ object_=_response.json(),
+ ),
+ )
+ return AsyncHttpResponse(response=_response, data=_data)
+ 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 get(
self,
workflow_id: str,
@@ -2443,8 +2612,10 @@ async def get_run_steps(
a separate download step.
`node_display_name` is resolved from the run's definition snapshot, so it
- reflects the name the node had when the run executed. Nodes that were
- retried appear as multiple steps with incrementing `iteration` values.
+ reflects the name the node had when the run executed. When multiple language
+ branches reach one output node, repeated sink steps receive locale suffixes
+ based on the language introduced by each iteration. Other retried nodes keep
+ their snapshot display name and incrementing `iteration` value.
For a higher-level view with aggregated metrics (pass rates, audio duration
by language), use `GET /runs/{run_id}/overview`. For paginated, grouped
@@ -2520,6 +2691,10 @@ async def get_run_overview(
breakdowns and per-validator scoring summaries. Also includes a
`workflow_snapshot` with the graph definition and per-node completion states.
+ Repeated states for one shared output node use the same locale suffixes as
+ `GET /runs/{run_id}/steps`, based on the language introduced by each sink
+ iteration.
+
This endpoint is best suited for a summary/results view after a run
completes. It differs from the other run sub-resources as follows:
@@ -2809,7 +2984,7 @@ async def download_run_node(
`node_id` must identify an output-category node in the run's definition
snapshot. Passing a node ID that belongs to a non-output node type (e.g.
- a processing or validation node) returns 404. Returns 404 if the node
+ an operator or validation node) returns 404. Returns 404 if the node
produced no audio files, and 409 if the run has not yet completed.
The URL is valid for 15 minutes. To download all output nodes in a single
diff --git a/src/onepin/workflows/runs/client.py b/src/onepin/workflows/runs/client.py
index e5e2327..4e36b4f 100644
--- a/src/onepin/workflows/runs/client.py
+++ b/src/onepin/workflows/runs/client.py
@@ -8,10 +8,14 @@
from ...types.api_response_workflow_run_detail_out import ApiResponseWorkflowRunDetailOut
from ...types.api_response_workflow_run_out import ApiResponseWorkflowRunOut
from ...types.api_response_workflow_run_status_out import ApiResponseWorkflowRunStatusOut
+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
+# this is used as the default value for optional parameters
+OMIT = typing.cast(typing.Any, ...)
+
class RunsClient:
def __init__(self, *, client_wrapper: SyncClientWrapper):
@@ -119,6 +123,7 @@ def start(
workflow_id: str,
*,
workspace_id: typing.Optional[str] = None,
+ request: typing.Optional[WorkflowRunStartIn] = None,
request_options: typing.Optional[RequestOptions] = None,
) -> ApiResponseWorkflowRunOut:
"""
@@ -130,6 +135,12 @@ def start(
lightweight progress updates, or `GET /runs/{run_id}` once to load the
immutable definition snapshot.
+ The optional request body supplies run-scoped inputs: `script_text`
+ (and optionally `source_language`) replaces the source_script text for
+ THIS run's snapshot only — the stored workflow definition is not
+ modified, so concurrent runs with different scripts cannot race.
+ Requires exactly one source_script node (422 otherwise).
+
Use `POST /runs/preview` or `POST /estimate` to compute the credit cost
before committing to an actual run — those endpoints are read-only and
incur no charges.
@@ -143,6 +154,8 @@ def start(
workspace_id : typing.Optional[str]
+ request : typing.Optional[WorkflowRunStartIn]
+
request_options : typing.Optional[RequestOptions]
Request-specific configuration.
@@ -153,16 +166,19 @@ def start(
Examples
--------
- from onepin import OnePinClient
+ from onepin import OnePinClient, WorkflowRunStartIn
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_options=request_options)
+ _response = self._raw_client.start(
+ workflow_id, workspace_id=workspace_id, request=request, request_options=request_options
+ )
return _response.data
def get(
@@ -451,6 +467,7 @@ async def start(
workflow_id: str,
*,
workspace_id: typing.Optional[str] = None,
+ request: typing.Optional[WorkflowRunStartIn] = None,
request_options: typing.Optional[RequestOptions] = None,
) -> ApiResponseWorkflowRunOut:
"""
@@ -462,6 +479,12 @@ async def start(
lightweight progress updates, or `GET /runs/{run_id}` once to load the
immutable definition snapshot.
+ The optional request body supplies run-scoped inputs: `script_text`
+ (and optionally `source_language`) replaces the source_script text for
+ THIS run's snapshot only — the stored workflow definition is not
+ modified, so concurrent runs with different scripts cannot race.
+ Requires exactly one source_script node (422 otherwise).
+
Use `POST /runs/preview` or `POST /estimate` to compute the credit cost
before committing to an actual run — those endpoints are read-only and
incur no charges.
@@ -475,6 +498,8 @@ async def start(
workspace_id : typing.Optional[str]
+ request : typing.Optional[WorkflowRunStartIn]
+
request_options : typing.Optional[RequestOptions]
Request-specific configuration.
@@ -487,7 +512,7 @@ async def start(
--------
import asyncio
- from onepin import AsyncOnePinClient
+ from onepin import AsyncOnePinClient, WorkflowRunStartIn
client = AsyncOnePinClient(
token="YOUR_TOKEN",
@@ -497,13 +522,14 @@ 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_options=request_options
+ workflow_id, workspace_id=workspace_id, request=request, 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 fc71b49..b848485 100644
--- a/src/onepin/workflows/runs/raw_client.py
+++ b/src/onepin/workflows/runs/raw_client.py
@@ -10,15 +10,20 @@
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_workflow_run_detail_out import ApiResponseWorkflowRunDetailOut
from ...types.api_response_workflow_run_out import ApiResponseWorkflowRunOut
from ...types.api_response_workflow_run_status_out import ApiResponseWorkflowRunStatusOut
+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
+# this is used as the default value for optional parameters
+OMIT = typing.cast(typing.Any, ...)
+
class RawRunsClient:
def __init__(self, *, client_wrapper: SyncClientWrapper):
@@ -137,6 +142,7 @@ def start(
workflow_id: str,
*,
workspace_id: typing.Optional[str] = None,
+ request: typing.Optional[WorkflowRunStartIn] = None,
request_options: typing.Optional[RequestOptions] = None,
) -> HttpResponse[ApiResponseWorkflowRunOut]:
"""
@@ -148,6 +154,12 @@ def start(
lightweight progress updates, or `GET /runs/{run_id}` once to load the
immutable definition snapshot.
+ The optional request body supplies run-scoped inputs: `script_text`
+ (and optionally `source_language`) replaces the source_script text for
+ THIS run's snapshot only — the stored workflow definition is not
+ modified, so concurrent runs with different scripts cannot race.
+ Requires exactly one source_script node (422 otherwise).
+
Use `POST /runs/preview` or `POST /estimate` to compute the credit cost
before committing to an actual run — those endpoints are read-only and
incur no charges.
@@ -161,6 +173,8 @@ def start(
workspace_id : typing.Optional[str]
+ request : typing.Optional[WorkflowRunStartIn]
+
request_options : typing.Optional[RequestOptions]
Request-specific configuration.
@@ -172,10 +186,15 @@ 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"
+ ),
headers={
+ "content-type": "application/json",
"X-Workspace-Id": str(workspace_id) if workspace_id is not None else None,
},
request_options=request_options,
+ omit=OMIT,
)
try:
if 200 <= _response.status_code < 300:
@@ -559,6 +578,7 @@ async def start(
workflow_id: str,
*,
workspace_id: typing.Optional[str] = None,
+ request: typing.Optional[WorkflowRunStartIn] = None,
request_options: typing.Optional[RequestOptions] = None,
) -> AsyncHttpResponse[ApiResponseWorkflowRunOut]:
"""
@@ -570,6 +590,12 @@ async def start(
lightweight progress updates, or `GET /runs/{run_id}` once to load the
immutable definition snapshot.
+ The optional request body supplies run-scoped inputs: `script_text`
+ (and optionally `source_language`) replaces the source_script text for
+ THIS run's snapshot only — the stored workflow definition is not
+ modified, so concurrent runs with different scripts cannot race.
+ Requires exactly one source_script node (422 otherwise).
+
Use `POST /runs/preview` or `POST /estimate` to compute the credit cost
before committing to an actual run — those endpoints are read-only and
incur no charges.
@@ -583,6 +609,8 @@ async def start(
workspace_id : typing.Optional[str]
+ request : typing.Optional[WorkflowRunStartIn]
+
request_options : typing.Optional[RequestOptions]
Request-specific configuration.
@@ -594,10 +622,15 @@ 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"
+ ),
headers={
+ "content-type": "application/json",
"X-Workspace-Id": str(workspace_id) if workspace_id is not None else None,
},
request_options=request_options,
+ omit=OMIT,
)
try:
if 200 <= _response.status_code < 300:
diff --git a/src/onepin/workspace_members/client.py b/src/onepin/workspace_members/client.py
index 77c4e22..b645ce2 100644
--- a/src/onepin/workspace_members/client.py
+++ b/src/onepin/workspace_members/client.py
@@ -92,8 +92,6 @@ def create_invite(
pending invite exists returns 409. Inviting an address that already
belongs to an active member also returns 409.
- The total number of active members plus pending invites is counted against
- the workspace owner's plan seat limit. Exceeding the limit returns 402.
The invitee's role can be updated before acceptance via
`PATCH /workspaces/{ws_id}/invites/{invite_id}`, or the invite can be
cancelled via `DELETE /workspaces/{ws_id}/invites/{invite_id}`.
@@ -344,10 +342,6 @@ def accept_invite(self, token: str, *, request_options: typing.Optional[RequestO
- Invite was revoked by an admin.
- Invite has expired (14-day TTL from creation).
- The workspace owner's plan seat limit is re-checked at accept time in case
- the plan was downgraded after the invite was sent; exceeding the limit
- returns 402.
-
Parameters
----------
token : str
@@ -461,8 +455,6 @@ async def create_invite(
pending invite exists returns 409. Inviting an address that already
belongs to an active member also returns 409.
- The total number of active members plus pending invites is counted against
- the workspace owner's plan seat limit. Exceeding the limit returns 402.
The invitee's role can be updated before acceptance via
`PATCH /workspaces/{ws_id}/invites/{invite_id}`, or the invite can be
cancelled via `DELETE /workspaces/{ws_id}/invites/{invite_id}`.
@@ -759,10 +751,6 @@ async def accept_invite(
- Invite was revoked by an admin.
- Invite has expired (14-day TTL from creation).
- The workspace owner's plan seat limit is re-checked at accept time in case
- the plan was downgraded after the invite was sent; exceeding the limit
- returns 402.
-
Parameters
----------
token : str
diff --git a/src/onepin/workspace_members/raw_client.py b/src/onepin/workspace_members/raw_client.py
index 4149352..7fa6e03 100644
--- a/src/onepin/workspace_members/raw_client.py
+++ b/src/onepin/workspace_members/raw_client.py
@@ -109,8 +109,6 @@ def create_invite(
pending invite exists returns 409. Inviting an address that already
belongs to an active member also returns 409.
- The total number of active members plus pending invites is counted against
- the workspace owner's plan seat limit. Exceeding the limit returns 402.
The invitee's role can be updated before acceptance via
`PATCH /workspaces/{ws_id}/invites/{invite_id}`, or the invite can be
cancelled via `DELETE /workspaces/{ws_id}/invites/{invite_id}`.
@@ -482,10 +480,6 @@ def accept_invite(
- Invite was revoked by an admin.
- Invite has expired (14-day TTL from creation).
- The workspace owner's plan seat limit is re-checked at accept time in case
- the plan was downgraded after the invite was sent; exceeding the limit
- returns 402.
-
Parameters
----------
token : str
@@ -622,8 +616,6 @@ async def create_invite(
pending invite exists returns 409. Inviting an address that already
belongs to an active member also returns 409.
- The total number of active members plus pending invites is counted against
- the workspace owner's plan seat limit. Exceeding the limit returns 402.
The invitee's role can be updated before acceptance via
`PATCH /workspaces/{ws_id}/invites/{invite_id}`, or the invite can be
cancelled via `DELETE /workspaces/{ws_id}/invites/{invite_id}`.
@@ -995,10 +987,6 @@ async def accept_invite(
- Invite was revoked by an admin.
- Invite has expired (14-day TTL from creation).
- The workspace owner's plan seat limit is re-checked at accept time in case
- the plan was downgraded after the invite was sent; exceeding the limit
- returns 402.
-
Parameters
----------
token : str