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