From b3a0cd82f61b4b732738d40b11d4248e44368fcf Mon Sep 17 00:00:00 2001 From: "onepin-pipeline-bot[bot]" <290953255+onepin-pipeline-bot[bot]@users.noreply.github.com> Date: Fri, 3 Jul 2026 06:02:46 +0000 Subject: [PATCH 1/2] feat: sync SDK to OnePin API v0.41.33 --- .spec-sha | 2 +- src/onepin/.fern/metadata.json | 2 +- src/onepin/README.md | 20 +- src/onepin/__init__.py | 141 +- src/onepin/api_keys/__init__.py | 4 - src/onepin/api_keys/client.py | 646 --- src/onepin/api_keys/raw_client.py | 877 --- src/onepin/auth/client.py | 24 +- src/onepin/auth/raw_client.py | 24 +- src/onepin/billing/__init__.py | 4 - src/onepin/billing/client.py | 273 - src/onepin/billing/raw_client.py | 352 -- src/onepin/client.py | 142 +- src/onepin/core/client_wrapper.py | 32 +- src/onepin/core/http_client.py | 12 +- src/onepin/core/http_sse/_api.py | 14 +- src/onepin/core/request_options.py | 2 + src/onepin/dictionary/client.py | 236 +- src/onepin/dictionary/raw_client.py | 236 +- src/onepin/health/__init__.py | 4 - src/onepin/health/client.py | 159 - src/onepin/health/raw_client.py | 186 - src/onepin/nodes/client.py | 106 +- src/onepin/nodes/raw_client.py | 106 +- src/onepin/provider_keys/__init__.py | 4 - src/onepin/provider_keys/client.py | 305 -- src/onepin/provider_keys/raw_client.py | 408 -- src/onepin/providers/client.py | 124 +- src/onepin/providers/raw_client.py | 124 +- src/onepin/reference.md | 4834 ++++++----------- src/onepin/templates/client.py | 302 +- src/onepin/templates/raw_client.py | 302 +- src/onepin/types/__init__.py | 122 +- .../api_counted_list_response_api_key_out.py | 24 - src/onepin/types/api_key_created_out.py | 40 - src/onepin/types/api_key_list_status.py | 5 - src/onepin/types/api_key_out.py | 35 - src/onepin/types/api_key_rotate_out.py | 22 - ...pi_list_response_customer_plan_response.py | 24 - .../types/api_response_api_key_created_out.py | 22 - .../types/api_response_api_key_rotate_out.py | 22 - .../types/api_response_checkout_response.py | 22 - ...e_customer_plan_change_preview_response.py | 22 - ...response_customer_subscription_response.py | 22 - .../api_response_invoice_list_response.py | 22 - ...i_response_list_payment_method_response.py | 22 - .../api_response_provider_key_item_out.py | 22 - ...api_response_provider_keys_manifest_out.py | 22 - .../api_response_setup_intent_response.py | 22 - ...ustomer_subscription_response_none_type.py | 22 - ...ut.py => api_response_voice_facets_out.py} | 6 +- .../api_response_workspace_runs_stats_out.py | 22 - ..._response_workspace_workflows_stats_out.py | 22 - src/onepin/types/catalog_model_out.py | 9 +- src/onepin/types/catalog_voice_out.py | 3 +- src/onepin/types/checkout_response.py | 19 - .../customer_plan_change_preview_response.py | 25 - src/onepin/types/customer_plan_response.py | 29 - .../types/customer_subscription_response.py | 29 - src/onepin/types/dictionary_language_out.py | 11 +- src/onepin/types/dictionary_out.py | 60 +- src/onepin/types/download_url_out.py | 17 +- .../email_notification_preferences_out.py | 11 +- src/onepin/types/invoice_list_response.py | 21 - src/onepin/types/invoice_response.py | 29 - src/onepin/types/node_type.py | 1 + src/onepin/types/payment_method_response.py | 24 - src/onepin/types/plan_details.py | 20 - src/onepin/types/plan_details_item.py | 20 - src/onepin/types/plan_details_section.py | 21 - src/onepin/types/pronunciation_suggestion.py | 11 +- src/onepin/types/provider_key_item_out.py | 28 - src/onepin/types/provider_key_provider.py | 7 - src/onepin/types/provider_key_status.py | 5 - .../types/provider_keys_manifest_out.py | 20 - src/onepin/types/runs_summary_out.py | 53 +- src/onepin/types/setup_intent_response.py | 19 - .../types/template_estimate_response.py | 101 +- src/onepin/types/template_out.py | 65 +- src/onepin/types/upload_create_response.py | 11 +- src/onepin/types/upload_out.py | 72 +- src/onepin/types/usage_activity_bucket_out.py | 41 +- src/onepin/types/usage_activity_out.py | 47 +- .../types/usage_activity_resource_out.py | 17 +- .../types/usage_activity_summary_out.py | 29 +- src/onepin/types/usage_activity_user_out.py | 11 +- src/onepin/types/usage_by_language_out.py | 35 +- src/onepin/types/usage_characters_out.py | 5 +- src/onepin/types/usage_credits_out.py | 11 +- src/onepin/types/usage_daily_out.py | 29 +- src/onepin/types/usage_language_row_out.py | 37 +- src/onepin/types/usage_lines_out.py | 11 +- src/onepin/types/usage_period_out.py | 11 +- src/onepin/types/usage_runs_out.py | 41 +- src/onepin/types/usage_summary_out.py | 59 +- src/onepin/types/voice_facet_item.py | 41 + src/onepin/types/voice_facets_out.py | 48 + src/onepin/types/voice_out.py | 137 +- src/onepin/types/voice_similar_out.py | 143 +- src/onepin/types/workflow_list_item.py | 50 +- src/onepin/types/workflow_out.py | 56 +- src/onepin/types/workspace_invite_out.py | 41 +- src/onepin/types/workspace_member_out.py | 53 +- .../types/workspace_member_role_update.py | 5 +- src/onepin/types/workspace_out.py | 51 +- src/onepin/types/workspace_runs_stats_out.py | 25 - src/onepin/types/workspace_settings_out.py | 11 +- .../types/workspace_workflows_stats_out.py | 36 - src/onepin/uploads/client.py | 146 +- src/onepin/uploads/raw_client.py | 146 +- src/onepin/usage/client.py | 114 +- src/onepin/usage/raw_client.py | 114 +- src/onepin/users/client.py | 836 +-- src/onepin/users/raw_client.py | 1401 +---- src/onepin/voices/__init__.py | 3 + src/onepin/voices/client.py | 330 +- src/onepin/voices/raw_client.py | 372 +- src/onepin/voices/types/__init__.py | 5 + ...v1voices_facets_get_request_source_item.py | 7 + src/onepin/webhooks/__init__.py | 4 - src/onepin/webhooks/client.py | 160 - src/onepin/webhooks/raw_client.py | 183 - src/onepin/workflows/client.py | 546 +- src/onepin/workflows/raw_client.py | 546 +- src/onepin/workflows/runs/client.py | 188 +- src/onepin/workflows/runs/raw_client.py | 188 +- src/onepin/workspace/client.py | 30 +- src/onepin/workspace/raw_client.py | 30 +- src/onepin/workspace_aggregates/__init__.py | 4 - src/onepin/workspace_aggregates/client.py | 230 - src/onepin/workspace_aggregates/raw_client.py | 311 -- src/onepin/workspace_members/client.py | 206 +- src/onepin/workspace_members/raw_client.py | 208 +- src/onepin/workspaces/client.py | 198 +- src/onepin/workspaces/raw_client.py | 186 +- 135 files changed, 7688 insertions(+), 11397 deletions(-) delete mode 100644 src/onepin/api_keys/__init__.py delete mode 100644 src/onepin/api_keys/client.py delete mode 100644 src/onepin/api_keys/raw_client.py delete mode 100644 src/onepin/billing/__init__.py delete mode 100644 src/onepin/billing/client.py delete mode 100644 src/onepin/billing/raw_client.py delete mode 100644 src/onepin/health/__init__.py delete mode 100644 src/onepin/health/client.py delete mode 100644 src/onepin/health/raw_client.py delete mode 100644 src/onepin/provider_keys/__init__.py delete mode 100644 src/onepin/provider_keys/client.py delete mode 100644 src/onepin/provider_keys/raw_client.py delete mode 100644 src/onepin/types/api_counted_list_response_api_key_out.py delete mode 100644 src/onepin/types/api_key_created_out.py delete mode 100644 src/onepin/types/api_key_list_status.py delete mode 100644 src/onepin/types/api_key_out.py delete mode 100644 src/onepin/types/api_key_rotate_out.py delete mode 100644 src/onepin/types/api_list_response_customer_plan_response.py delete mode 100644 src/onepin/types/api_response_api_key_created_out.py delete mode 100644 src/onepin/types/api_response_api_key_rotate_out.py delete mode 100644 src/onepin/types/api_response_checkout_response.py delete mode 100644 src/onepin/types/api_response_customer_plan_change_preview_response.py delete mode 100644 src/onepin/types/api_response_customer_subscription_response.py delete mode 100644 src/onepin/types/api_response_invoice_list_response.py delete mode 100644 src/onepin/types/api_response_list_payment_method_response.py delete mode 100644 src/onepin/types/api_response_provider_key_item_out.py delete mode 100644 src/onepin/types/api_response_provider_keys_manifest_out.py delete mode 100644 src/onepin/types/api_response_setup_intent_response.py delete mode 100644 src/onepin/types/api_response_union_customer_subscription_response_none_type.py rename src/onepin/types/{api_response_api_key_out.py => api_response_voice_facets_out.py} (80%) delete mode 100644 src/onepin/types/api_response_workspace_runs_stats_out.py delete mode 100644 src/onepin/types/api_response_workspace_workflows_stats_out.py delete mode 100644 src/onepin/types/checkout_response.py delete mode 100644 src/onepin/types/customer_plan_change_preview_response.py delete mode 100644 src/onepin/types/customer_plan_response.py delete mode 100644 src/onepin/types/customer_subscription_response.py delete mode 100644 src/onepin/types/invoice_list_response.py delete mode 100644 src/onepin/types/invoice_response.py delete mode 100644 src/onepin/types/payment_method_response.py delete mode 100644 src/onepin/types/plan_details.py delete mode 100644 src/onepin/types/plan_details_item.py delete mode 100644 src/onepin/types/plan_details_section.py delete mode 100644 src/onepin/types/provider_key_item_out.py delete mode 100644 src/onepin/types/provider_key_provider.py delete mode 100644 src/onepin/types/provider_key_status.py delete mode 100644 src/onepin/types/provider_keys_manifest_out.py delete mode 100644 src/onepin/types/setup_intent_response.py create mode 100644 src/onepin/types/voice_facet_item.py create mode 100644 src/onepin/types/voice_facets_out.py delete mode 100644 src/onepin/types/workspace_runs_stats_out.py delete mode 100644 src/onepin/types/workspace_workflows_stats_out.py create mode 100644 src/onepin/voices/types/get_voice_facets_api_v1voices_facets_get_request_source_item.py delete mode 100644 src/onepin/webhooks/__init__.py delete mode 100644 src/onepin/webhooks/client.py delete mode 100644 src/onepin/webhooks/raw_client.py delete mode 100644 src/onepin/workspace_aggregates/__init__.py delete mode 100644 src/onepin/workspace_aggregates/client.py delete mode 100644 src/onepin/workspace_aggregates/raw_client.py diff --git a/.spec-sha b/.spec-sha index 3b27483..bf9bed9 100644 --- a/.spec-sha +++ b/.spec-sha @@ -1 +1 @@ -8f933ed904093dc331cc8d2a8bc9c6cf4ac5e03b +fc36985f78cde936ebe11de2da4310102b7a6c80 diff --git a/src/onepin/.fern/metadata.json b/src/onepin/.fern/metadata.json index 2e6b6c3..53334d1 100644 --- a/src/onepin/.fern/metadata.json +++ b/src/onepin/.fern/metadata.json @@ -17,7 +17,7 @@ } ] }, - "originGitCommit": "aa97888912968233d7b8278a5e028789d15ef281", + "originGitCommit": "76a0b71893b02defdab9fb41fea3c42d842d4578", "originGitCommitIsDirty": false, "invokedBy": "ci", "ciProvider": "github" diff --git a/src/onepin/README.md b/src/onepin/README.md index 652cae2..b16310d 100644 --- a/src/onepin/README.md +++ b/src/onepin/README.md @@ -42,7 +42,11 @@ client = OnePinClient( token="", ) -client.webhooks.clerk_webhook() +client.dictionary.create_dictionary_entry( + word="word", + method="spelled", + language="language", +) ``` ## Environments @@ -73,7 +77,11 @@ client = AsyncOnePinClient( async def main() -> None: - await client.webhooks.clerk_webhook() + await client.dictionary.create_dictionary_entry( + word="word", + method="spelled", + language="language", + ) asyncio.run(main()) @@ -88,7 +96,7 @@ will be thrown. from onepin.core.api_error import ApiError try: - client.webhooks.clerk_webhook() + client.dictionary.create_dictionary_entry(...) except ApiError as e: print(e.status_code) print(e.body) @@ -128,7 +136,7 @@ The `.with_raw_response` property returns a "raw" client that can be used to acc from onepin import OnePinClient client = OnePinClient(...) -response = client.webhooks.with_raw_response.clerk_webhook() +response = client.dictionary.with_raw_response.create_dictionary_entry(...) print(response.headers) # access the response headers print(response.status_code) # access the response status code print(response.data) # access the underlying object @@ -159,7 +167,7 @@ Which status codes are retried depends on the `retryStatusCodes` generator confi Use the `max_retries` request option to configure this behavior. ```python -client.webhooks.clerk_webhook(request_options={ +client.dictionary.create_dictionary_entry(..., request_options={ "max_retries": 1 }) ``` @@ -174,7 +182,7 @@ from onepin import OnePinClient client = OnePinClient(..., timeout=20.0) # Override timeout for a specific method -client.webhooks.clerk_webhook(request_options={ +client.dictionary.create_dictionary_entry(..., request_options={ "timeout_in_seconds": 1 }) ``` diff --git a/src/onepin/__init__.py b/src/onepin/__init__.py index b10060b..e36cecb 100644 --- a/src/onepin/__init__.py +++ b/src/onepin/__init__.py @@ -7,7 +7,6 @@ if typing.TYPE_CHECKING: from .types import ( - ApiCountedListResponseApiKeyOut, ApiCountedListResponseCatalogVoiceOut, ApiCountedListResponseVoiceOut, ApiCountedListResponseWorkflowListItem, @@ -15,14 +14,9 @@ ApiErrorBody, ApiErrorDetail, ApiErrorResponse, - ApiKeyCreatedOut, - ApiKeyListStatus, - ApiKeyOut, - ApiKeyRotateOut, ApiKeyScope, ApiListResponseCatalogModelOut, ApiListResponseCatalogProviderOut, - ApiListResponseCustomerPlanResponse, ApiListResponseDictionaryOut, ApiListResponseNodePortsOut, ApiListResponseTemplateOut, @@ -31,40 +25,29 @@ ApiListResponseVoiceSimilarOut, ApiListResponseWorkspaceMemberOut, ApiListResponseWorkspaceOut, - ApiResponseApiKeyCreatedOut, - ApiResponseApiKeyOut, - ApiResponseApiKeyRotateOut, ApiResponseAuthWhoamiOut, ApiResponseBalanceResponse, ApiResponseCatalogModelOut, ApiResponseCatalogProviderOut, - ApiResponseCheckoutResponse, - ApiResponseCustomerPlanChangePreviewResponse, - ApiResponseCustomerSubscriptionResponse, ApiResponseDict, ApiResponseDictionaryOut, ApiResponseDownloadUrlOut, ApiResponseEmailNotificationPreferencesOut, ApiResponseEstimateResponse, - ApiResponseInvoiceListResponse, ApiResponseListDictionaryLanguageOut, - ApiResponseListPaymentMethodResponse, ApiResponseListWorkflowRunStepOut, ApiResponseNodeDetailOut, ApiResponsePlanLimits, ApiResponsePronunciationSuggestion, - ApiResponseProviderKeyItemOut, - ApiResponseProviderKeysManifestOut, ApiResponseRunsSummaryOut, - ApiResponseSetupIntentResponse, ApiResponseSlugAvailabilityOut, ApiResponseTemplateEstimateResponse, ApiResponseTemplateOut, - ApiResponseUnionCustomerSubscriptionResponseNoneType, ApiResponseUploadCreateResponse, ApiResponseUploadOut, ApiResponseUsageByLanguageOut, ApiResponseUsageSummaryOut, + ApiResponseVoiceFacetsOut, ApiResponseVoiceOut, ApiResponseWorkflowOut, ApiResponseWorkflowRunDetailOut, @@ -73,9 +56,7 @@ ApiResponseWorkflowRunStatusOut, ApiResponseWorkspaceInviteOut, ApiResponseWorkspaceOut, - ApiResponseWorkspaceRunsStatsOut, ApiResponseWorkspaceSettingsOut, - ApiResponseWorkspaceWorkflowsStatsOut, AuthWhoamiOut, AuthWhoamiOutAuthKind, BalanceResponse, @@ -83,11 +64,7 @@ CatalogModelOut, CatalogProviderOut, CatalogVoiceOut, - CheckoutResponse, CountedPaginationMeta, - CustomerPlanChangePreviewResponse, - CustomerPlanResponse, - CustomerSubscriptionResponse, DictionaryLanguageOut, DictionaryMethod, DictionaryOut, @@ -101,8 +78,6 @@ GraphEdge, GraphNode, HttpValidationError, - InvoiceListResponse, - InvoiceResponse, Meta, ModelOut, NodeCategory, @@ -120,21 +95,12 @@ NodeType, NumericOption, PaginationMeta, - PaymentMethodResponse, - PlanDetails, - PlanDetailsItem, - PlanDetailsSection, PlanLimits, PlanTier, PortOut, PronunciationSuggestion, ProviderGroupOut, - ProviderKeyItemOut, - ProviderKeyProvider, - ProviderKeyStatus, - ProviderKeysManifestOut, RunsSummaryOut, - SetupIntentResponse, SlugAvailabilityOut, SlugAvailabilityOutReason, TemplateCategory, @@ -172,6 +138,8 @@ VoiceAccent, VoiceAge, VoiceCategory, + VoiceFacetItem, + VoiceFacetsOut, VoiceGender, VoiceOut, VoiceSimilarOut, @@ -221,29 +189,21 @@ WorkspaceMemberRoleUpdate, WorkspaceOut, WorkspaceRole, - WorkspaceRunsStatsOut, WorkspaceSettingsOut, - WorkspaceWorkflowsStatsOut, ) from .errors import BadRequestError, ConflictError, ForbiddenError, NotFoundError, UnprocessableEntityError from . import ( - api_keys, auth, - billing, dictionary, - health, nodes, - provider_keys, providers, templates, uploads, usage, users, voices, - webhooks, workflows, workspace, - workspace_aggregates, workspace_members, workspaces, ) @@ -270,6 +230,7 @@ ) from .users import ListMyTemplatesApiV1UsersMeTemplatesGetRequestSort from .voices import ( + GetVoiceFacetsApiV1VoicesFacetsGetRequestSourceItem, ListVoicesRequestLanguageItem, ListVoicesRequestOrderItem, ListVoicesRequestProviderItem, @@ -278,7 +239,6 @@ ) from .workflows import ListWorkflowsRequestOrderItem, ListWorkflowsRequestSortItem _dynamic_imports: typing.Dict[str, str] = { - "ApiCountedListResponseApiKeyOut": ".types", "ApiCountedListResponseCatalogVoiceOut": ".types", "ApiCountedListResponseVoiceOut": ".types", "ApiCountedListResponseWorkflowListItem": ".types", @@ -286,14 +246,9 @@ "ApiErrorBody": ".types", "ApiErrorDetail": ".types", "ApiErrorResponse": ".types", - "ApiKeyCreatedOut": ".types", - "ApiKeyListStatus": ".types", - "ApiKeyOut": ".types", - "ApiKeyRotateOut": ".types", "ApiKeyScope": ".types", "ApiListResponseCatalogModelOut": ".types", "ApiListResponseCatalogProviderOut": ".types", - "ApiListResponseCustomerPlanResponse": ".types", "ApiListResponseDictionaryOut": ".types", "ApiListResponseNodePortsOut": ".types", "ApiListResponseTemplateOut": ".types", @@ -302,40 +257,29 @@ "ApiListResponseVoiceSimilarOut": ".types", "ApiListResponseWorkspaceMemberOut": ".types", "ApiListResponseWorkspaceOut": ".types", - "ApiResponseApiKeyCreatedOut": ".types", - "ApiResponseApiKeyOut": ".types", - "ApiResponseApiKeyRotateOut": ".types", "ApiResponseAuthWhoamiOut": ".types", "ApiResponseBalanceResponse": ".types", "ApiResponseCatalogModelOut": ".types", "ApiResponseCatalogProviderOut": ".types", - "ApiResponseCheckoutResponse": ".types", - "ApiResponseCustomerPlanChangePreviewResponse": ".types", - "ApiResponseCustomerSubscriptionResponse": ".types", "ApiResponseDict": ".types", "ApiResponseDictionaryOut": ".types", "ApiResponseDownloadUrlOut": ".types", "ApiResponseEmailNotificationPreferencesOut": ".types", "ApiResponseEstimateResponse": ".types", - "ApiResponseInvoiceListResponse": ".types", "ApiResponseListDictionaryLanguageOut": ".types", - "ApiResponseListPaymentMethodResponse": ".types", "ApiResponseListWorkflowRunStepOut": ".types", "ApiResponseNodeDetailOut": ".types", "ApiResponsePlanLimits": ".types", "ApiResponsePronunciationSuggestion": ".types", - "ApiResponseProviderKeyItemOut": ".types", - "ApiResponseProviderKeysManifestOut": ".types", "ApiResponseRunsSummaryOut": ".types", - "ApiResponseSetupIntentResponse": ".types", "ApiResponseSlugAvailabilityOut": ".types", "ApiResponseTemplateEstimateResponse": ".types", "ApiResponseTemplateOut": ".types", - "ApiResponseUnionCustomerSubscriptionResponseNoneType": ".types", "ApiResponseUploadCreateResponse": ".types", "ApiResponseUploadOut": ".types", "ApiResponseUsageByLanguageOut": ".types", "ApiResponseUsageSummaryOut": ".types", + "ApiResponseVoiceFacetsOut": ".types", "ApiResponseVoiceOut": ".types", "ApiResponseWorkflowOut": ".types", "ApiResponseWorkflowRunDetailOut": ".types", @@ -344,9 +288,7 @@ "ApiResponseWorkflowRunStatusOut": ".types", "ApiResponseWorkspaceInviteOut": ".types", "ApiResponseWorkspaceOut": ".types", - "ApiResponseWorkspaceRunsStatsOut": ".types", "ApiResponseWorkspaceSettingsOut": ".types", - "ApiResponseWorkspaceWorkflowsStatsOut": ".types", "AsyncOnePinClient": ".client", "AuthWhoamiOut": ".types", "AuthWhoamiOutAuthKind": ".types", @@ -356,12 +298,8 @@ "CatalogModelOut": ".types", "CatalogProviderOut": ".types", "CatalogVoiceOut": ".types", - "CheckoutResponse": ".types", "ConflictError": ".errors", "CountedPaginationMeta": ".types", - "CustomerPlanChangePreviewResponse": ".types", - "CustomerPlanResponse": ".types", - "CustomerSubscriptionResponse": ".types", "DefaultAioHttpClient": "._default_clients", "DefaultAsyncHttpxClient": "._default_clients", "DictionaryLanguageOut": ".types", @@ -373,13 +311,12 @@ "EstimateResponse": ".types", "ExecutionDefinition": ".types", "ForbiddenError": ".errors", + "GetVoiceFacetsApiV1VoicesFacetsGetRequestSourceItem": ".voices", "GraphDefinitionInput": ".types", "GraphDefinitionOutput": ".types", "GraphEdge": ".types", "GraphNode": ".types", "HttpValidationError": ".types", - "InvoiceListResponse": ".types", - "InvoiceResponse": ".types", "ListDictionaryEntriesApiV1DictionaryGetRequestLanguage": ".dictionary", "ListDictionaryEntriesApiV1DictionaryGetRequestOrder": ".dictionary", "ListDictionaryEntriesApiV1DictionaryGetRequestSort": ".dictionary", @@ -413,24 +350,15 @@ "OnePinClientEnvironment": ".environment", "OnePinUpgradeRequiredError": "._version_gate", "PaginationMeta": ".types", - "PaymentMethodResponse": ".types", - "PlanDetails": ".types", - "PlanDetailsItem": ".types", - "PlanDetailsSection": ".types", "PlanLimits": ".types", "PlanTier": ".types", "PortOut": ".types", "PronunciationSuggestion": ".types", "ProviderGroupOut": ".types", - "ProviderKeyItemOut": ".types", - "ProviderKeyProvider": ".types", - "ProviderKeyStatus": ".types", - "ProviderKeysManifestOut": ".types", "RunsSummaryOut": ".types", "SearchDictionaryEntriesApiV1DictionarySearchGetRequestLanguageItem": ".dictionary", "SearchDictionaryEntriesApiV1DictionarySearchGetRequestOrder": ".dictionary", "SearchDictionaryEntriesApiV1DictionarySearchGetRequestSort": ".dictionary", - "SetupIntentResponse": ".types", "SlugAvailabilityOut": ".types", "SlugAvailabilityOutReason": ".types", "TemplateCategory": ".types", @@ -476,6 +404,8 @@ "VoiceAccent": ".types", "VoiceAge": ".types", "VoiceCategory": ".types", + "VoiceFacetItem": ".types", + "VoiceFacetsOut": ".types", "VoiceGender": ".types", "VoiceOut": ".types", "VoiceSimilarOut": ".types", @@ -525,28 +455,20 @@ "WorkspaceMemberRoleUpdate": ".types", "WorkspaceOut": ".types", "WorkspaceRole": ".types", - "WorkspaceRunsStatsOut": ".types", "WorkspaceSettingsOut": ".types", - "WorkspaceWorkflowsStatsOut": ".types", - "api_keys": ".api_keys", "auth": ".auth", - "billing": ".billing", "dictionary": ".dictionary", - "health": ".health", "make_async_client": "._version_gate", "make_client": "._version_gate", "nodes": ".nodes", - "provider_keys": ".provider_keys", "providers": ".providers", "templates": ".templates", "uploads": ".uploads", "usage": ".usage", "users": ".users", "voices": ".voices", - "webhooks": ".webhooks", "workflows": ".workflows", "workspace": ".workspace", - "workspace_aggregates": ".workspace_aggregates", "workspace_members": ".workspace_members", "workspaces": ".workspaces", } @@ -574,7 +496,6 @@ def __dir__(): __all__ = [ - "ApiCountedListResponseApiKeyOut", "ApiCountedListResponseCatalogVoiceOut", "ApiCountedListResponseVoiceOut", "ApiCountedListResponseWorkflowListItem", @@ -582,14 +503,9 @@ def __dir__(): "ApiErrorBody", "ApiErrorDetail", "ApiErrorResponse", - "ApiKeyCreatedOut", - "ApiKeyListStatus", - "ApiKeyOut", - "ApiKeyRotateOut", "ApiKeyScope", "ApiListResponseCatalogModelOut", "ApiListResponseCatalogProviderOut", - "ApiListResponseCustomerPlanResponse", "ApiListResponseDictionaryOut", "ApiListResponseNodePortsOut", "ApiListResponseTemplateOut", @@ -598,40 +514,29 @@ def __dir__(): "ApiListResponseVoiceSimilarOut", "ApiListResponseWorkspaceMemberOut", "ApiListResponseWorkspaceOut", - "ApiResponseApiKeyCreatedOut", - "ApiResponseApiKeyOut", - "ApiResponseApiKeyRotateOut", "ApiResponseAuthWhoamiOut", "ApiResponseBalanceResponse", "ApiResponseCatalogModelOut", "ApiResponseCatalogProviderOut", - "ApiResponseCheckoutResponse", - "ApiResponseCustomerPlanChangePreviewResponse", - "ApiResponseCustomerSubscriptionResponse", "ApiResponseDict", "ApiResponseDictionaryOut", "ApiResponseDownloadUrlOut", "ApiResponseEmailNotificationPreferencesOut", "ApiResponseEstimateResponse", - "ApiResponseInvoiceListResponse", "ApiResponseListDictionaryLanguageOut", - "ApiResponseListPaymentMethodResponse", "ApiResponseListWorkflowRunStepOut", "ApiResponseNodeDetailOut", "ApiResponsePlanLimits", "ApiResponsePronunciationSuggestion", - "ApiResponseProviderKeyItemOut", - "ApiResponseProviderKeysManifestOut", "ApiResponseRunsSummaryOut", - "ApiResponseSetupIntentResponse", "ApiResponseSlugAvailabilityOut", "ApiResponseTemplateEstimateResponse", "ApiResponseTemplateOut", - "ApiResponseUnionCustomerSubscriptionResponseNoneType", "ApiResponseUploadCreateResponse", "ApiResponseUploadOut", "ApiResponseUsageByLanguageOut", "ApiResponseUsageSummaryOut", + "ApiResponseVoiceFacetsOut", "ApiResponseVoiceOut", "ApiResponseWorkflowOut", "ApiResponseWorkflowRunDetailOut", @@ -640,9 +545,7 @@ def __dir__(): "ApiResponseWorkflowRunStatusOut", "ApiResponseWorkspaceInviteOut", "ApiResponseWorkspaceOut", - "ApiResponseWorkspaceRunsStatsOut", "ApiResponseWorkspaceSettingsOut", - "ApiResponseWorkspaceWorkflowsStatsOut", "AsyncOnePinClient", "AuthWhoamiOut", "AuthWhoamiOutAuthKind", @@ -652,12 +555,8 @@ def __dir__(): "CatalogModelOut", "CatalogProviderOut", "CatalogVoiceOut", - "CheckoutResponse", "ConflictError", "CountedPaginationMeta", - "CustomerPlanChangePreviewResponse", - "CustomerPlanResponse", - "CustomerSubscriptionResponse", "DefaultAioHttpClient", "DefaultAsyncHttpxClient", "DictionaryLanguageOut", @@ -669,13 +568,12 @@ def __dir__(): "EstimateResponse", "ExecutionDefinition", "ForbiddenError", + "GetVoiceFacetsApiV1VoicesFacetsGetRequestSourceItem", "GraphDefinitionInput", "GraphDefinitionOutput", "GraphEdge", "GraphNode", "HttpValidationError", - "InvoiceListResponse", - "InvoiceResponse", "ListDictionaryEntriesApiV1DictionaryGetRequestLanguage", "ListDictionaryEntriesApiV1DictionaryGetRequestOrder", "ListDictionaryEntriesApiV1DictionaryGetRequestSort", @@ -709,24 +607,15 @@ def __dir__(): "OnePinClientEnvironment", "OnePinUpgradeRequiredError", "PaginationMeta", - "PaymentMethodResponse", - "PlanDetails", - "PlanDetailsItem", - "PlanDetailsSection", "PlanLimits", "PlanTier", "PortOut", "PronunciationSuggestion", "ProviderGroupOut", - "ProviderKeyItemOut", - "ProviderKeyProvider", - "ProviderKeyStatus", - "ProviderKeysManifestOut", "RunsSummaryOut", "SearchDictionaryEntriesApiV1DictionarySearchGetRequestLanguageItem", "SearchDictionaryEntriesApiV1DictionarySearchGetRequestOrder", "SearchDictionaryEntriesApiV1DictionarySearchGetRequestSort", - "SetupIntentResponse", "SlugAvailabilityOut", "SlugAvailabilityOutReason", "TemplateCategory", @@ -772,6 +661,8 @@ def __dir__(): "VoiceAccent", "VoiceAge", "VoiceCategory", + "VoiceFacetItem", + "VoiceFacetsOut", "VoiceGender", "VoiceOut", "VoiceSimilarOut", @@ -821,28 +712,20 @@ def __dir__(): "WorkspaceMemberRoleUpdate", "WorkspaceOut", "WorkspaceRole", - "WorkspaceRunsStatsOut", "WorkspaceSettingsOut", - "WorkspaceWorkflowsStatsOut", - "api_keys", "auth", - "billing", "dictionary", - "health", "make_async_client", "make_client", "nodes", - "provider_keys", "providers", "templates", "uploads", "usage", "users", "voices", - "webhooks", "workflows", "workspace", - "workspace_aggregates", "workspace_members", "workspaces", ] diff --git a/src/onepin/api_keys/__init__.py b/src/onepin/api_keys/__init__.py deleted file mode 100644 index 5cde020..0000000 --- a/src/onepin/api_keys/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -# isort: skip_file - diff --git a/src/onepin/api_keys/client.py b/src/onepin/api_keys/client.py deleted file mode 100644 index c3496a9..0000000 --- a/src/onepin/api_keys/client.py +++ /dev/null @@ -1,646 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper -from ..core.request_options import RequestOptions -from ..types.api_counted_list_response_api_key_out import ApiCountedListResponseApiKeyOut -from ..types.api_key_list_status import ApiKeyListStatus -from ..types.api_key_scope import ApiKeyScope -from ..types.api_response_api_key_created_out import ApiResponseApiKeyCreatedOut -from ..types.api_response_api_key_out import ApiResponseApiKeyOut -from ..types.api_response_api_key_rotate_out import ApiResponseApiKeyRotateOut -from .raw_client import AsyncRawApiKeysClient, RawApiKeysClient - -# this is used as the default value for optional parameters -OMIT = typing.cast(typing.Any, ...) - - -class ApiKeysClient: - def __init__(self, *, client_wrapper: SyncClientWrapper): - self._raw_client = RawApiKeysClient(client_wrapper=client_wrapper) - - @property - def with_raw_response(self) -> RawApiKeysClient: - """ - Retrieves a raw implementation of this client that returns raw responses. - - Returns - ------- - RawApiKeysClient - """ - return self._raw_client - - def list_api_keys( - self, - *, - offset: typing.Optional[int] = None, - limit: typing.Optional[int] = None, - status: typing.Optional[ApiKeyListStatus] = None, - workspace_id: typing.Optional[str] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> ApiCountedListResponseApiKeyOut: - """ - List API keys for the current workspace without secret material. - - Parameters - ---------- - offset : typing.Optional[int] - - limit : typing.Optional[int] - - status : typing.Optional[ApiKeyListStatus] - API-key list status filter. Defaults to currently usable keys. `revoked` returns unavailable keys (`active=false` or `revoked_at` is set); `all` returns all workspace API-key metadata rows. - - workspace_id : typing.Optional[str] - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ApiCountedListResponseApiKeyOut - Successful Response - - Examples - -------- - from onepin import OnePinClient - - client = OnePinClient( - token="YOUR_TOKEN", - ) - client.api_keys.list_api_keys() - """ - _response = self._raw_client.list_api_keys( - offset=offset, limit=limit, status=status, workspace_id=workspace_id, request_options=request_options - ) - return _response.data - - def create_api_key( - self, - *, - name: str, - workspace_id: typing.Optional[str] = None, - scopes: typing.Optional[typing.Sequence[ApiKeyScope]] = OMIT, - rate_limit_per_min: typing.Optional[int] = OMIT, - key_type: typing.Optional[str] = OMIT, - request_options: typing.Optional[RequestOptions] = None, - ) -> ApiResponseApiKeyCreatedOut: - """ - Create a live API key and return its plaintext value exactly once. - - Parameters - ---------- - name : str - - workspace_id : typing.Optional[str] - - scopes : typing.Optional[typing.Sequence[ApiKeyScope]] - - rate_limit_per_min : typing.Optional[int] - - key_type : typing.Optional[str] - Phase 1 supports live bearer keys only; test/public are reserved. - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ApiResponseApiKeyCreatedOut - Successful Response - - Examples - -------- - from onepin import OnePinClient - - client = OnePinClient( - token="YOUR_TOKEN", - ) - client.api_keys.create_api_key( - name="name", - ) - """ - _response = self._raw_client.create_api_key( - name=name, - workspace_id=workspace_id, - scopes=scopes, - rate_limit_per_min=rate_limit_per_min, - key_type=key_type, - request_options=request_options, - ) - return _response.data - - def get_api_key( - self, - key_id: str, - *, - workspace_id: typing.Optional[str] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> ApiResponseApiKeyOut: - """ - Get one API-key metadata record for the current workspace. - - Parameters - ---------- - key_id : str - - workspace_id : typing.Optional[str] - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ApiResponseApiKeyOut - Successful Response - - Examples - -------- - from onepin import OnePinClient - - client = OnePinClient( - token="YOUR_TOKEN", - ) - client.api_keys.get_api_key( - key_id="key_id", - ) - """ - _response = self._raw_client.get_api_key(key_id, workspace_id=workspace_id, request_options=request_options) - return _response.data - - def delete_api_key( - self, - key_id: str, - *, - workspace_id: typing.Optional[str] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> ApiResponseApiKeyOut: - """ - Soft-revoke an API key for the current workspace. - - Parameters - ---------- - key_id : str - - workspace_id : typing.Optional[str] - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ApiResponseApiKeyOut - Successful Response - - Examples - -------- - from onepin import OnePinClient - - client = OnePinClient( - token="YOUR_TOKEN", - ) - client.api_keys.delete_api_key( - key_id="key_id", - ) - """ - _response = self._raw_client.delete_api_key(key_id, workspace_id=workspace_id, request_options=request_options) - return _response.data - - def update_api_key( - self, - key_id: str, - *, - workspace_id: typing.Optional[str] = None, - name: typing.Optional[str] = OMIT, - scopes: typing.Optional[typing.Sequence[ApiKeyScope]] = OMIT, - rate_limit_per_min: typing.Optional[int] = OMIT, - active: typing.Optional[bool] = OMIT, - request_options: typing.Optional[RequestOptions] = None, - ) -> ApiResponseApiKeyOut: - """ - Update API-key metadata, scopes, rate limit, or active state. - - Parameters - ---------- - key_id : str - - workspace_id : typing.Optional[str] - - name : typing.Optional[str] - - scopes : typing.Optional[typing.Sequence[ApiKeyScope]] - - rate_limit_per_min : typing.Optional[int] - - active : typing.Optional[bool] - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ApiResponseApiKeyOut - Successful Response - - Examples - -------- - from onepin import OnePinClient - - client = OnePinClient( - token="YOUR_TOKEN", - ) - client.api_keys.update_api_key( - key_id="key_id", - ) - """ - _response = self._raw_client.update_api_key( - key_id, - workspace_id=workspace_id, - name=name, - scopes=scopes, - rate_limit_per_min=rate_limit_per_min, - active=active, - request_options=request_options, - ) - return _response.data - - def rotate_api_key( - self, - key_id: str, - *, - workspace_id: typing.Optional[str] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> ApiResponseApiKeyRotateOut: - """ - Rotate an API key by revoking the old row and creating a new key. - - Parameters - ---------- - key_id : str - - workspace_id : typing.Optional[str] - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ApiResponseApiKeyRotateOut - Successful Response - - Examples - -------- - from onepin import OnePinClient - - client = OnePinClient( - token="YOUR_TOKEN", - ) - client.api_keys.rotate_api_key( - key_id="key_id", - ) - """ - _response = self._raw_client.rotate_api_key(key_id, workspace_id=workspace_id, request_options=request_options) - return _response.data - - -class AsyncApiKeysClient: - def __init__(self, *, client_wrapper: AsyncClientWrapper): - self._raw_client = AsyncRawApiKeysClient(client_wrapper=client_wrapper) - - @property - def with_raw_response(self) -> AsyncRawApiKeysClient: - """ - Retrieves a raw implementation of this client that returns raw responses. - - Returns - ------- - AsyncRawApiKeysClient - """ - return self._raw_client - - async def list_api_keys( - self, - *, - offset: typing.Optional[int] = None, - limit: typing.Optional[int] = None, - status: typing.Optional[ApiKeyListStatus] = None, - workspace_id: typing.Optional[str] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> ApiCountedListResponseApiKeyOut: - """ - List API keys for the current workspace without secret material. - - Parameters - ---------- - offset : typing.Optional[int] - - limit : typing.Optional[int] - - status : typing.Optional[ApiKeyListStatus] - API-key list status filter. Defaults to currently usable keys. `revoked` returns unavailable keys (`active=false` or `revoked_at` is set); `all` returns all workspace API-key metadata rows. - - workspace_id : typing.Optional[str] - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ApiCountedListResponseApiKeyOut - Successful Response - - Examples - -------- - import asyncio - - from onepin import AsyncOnePinClient - - client = AsyncOnePinClient( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.api_keys.list_api_keys() - - - asyncio.run(main()) - """ - _response = await self._raw_client.list_api_keys( - offset=offset, limit=limit, status=status, workspace_id=workspace_id, request_options=request_options - ) - return _response.data - - async def create_api_key( - self, - *, - name: str, - workspace_id: typing.Optional[str] = None, - scopes: typing.Optional[typing.Sequence[ApiKeyScope]] = OMIT, - rate_limit_per_min: typing.Optional[int] = OMIT, - key_type: typing.Optional[str] = OMIT, - request_options: typing.Optional[RequestOptions] = None, - ) -> ApiResponseApiKeyCreatedOut: - """ - Create a live API key and return its plaintext value exactly once. - - Parameters - ---------- - name : str - - workspace_id : typing.Optional[str] - - scopes : typing.Optional[typing.Sequence[ApiKeyScope]] - - rate_limit_per_min : typing.Optional[int] - - key_type : typing.Optional[str] - Phase 1 supports live bearer keys only; test/public are reserved. - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ApiResponseApiKeyCreatedOut - Successful Response - - Examples - -------- - import asyncio - - from onepin import AsyncOnePinClient - - client = AsyncOnePinClient( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.api_keys.create_api_key( - name="name", - ) - - - asyncio.run(main()) - """ - _response = await self._raw_client.create_api_key( - name=name, - workspace_id=workspace_id, - scopes=scopes, - rate_limit_per_min=rate_limit_per_min, - key_type=key_type, - request_options=request_options, - ) - return _response.data - - async def get_api_key( - self, - key_id: str, - *, - workspace_id: typing.Optional[str] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> ApiResponseApiKeyOut: - """ - Get one API-key metadata record for the current workspace. - - Parameters - ---------- - key_id : str - - workspace_id : typing.Optional[str] - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ApiResponseApiKeyOut - Successful Response - - Examples - -------- - import asyncio - - from onepin import AsyncOnePinClient - - client = AsyncOnePinClient( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.api_keys.get_api_key( - key_id="key_id", - ) - - - asyncio.run(main()) - """ - _response = await self._raw_client.get_api_key( - key_id, workspace_id=workspace_id, request_options=request_options - ) - return _response.data - - async def delete_api_key( - self, - key_id: str, - *, - workspace_id: typing.Optional[str] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> ApiResponseApiKeyOut: - """ - Soft-revoke an API key for the current workspace. - - Parameters - ---------- - key_id : str - - workspace_id : typing.Optional[str] - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ApiResponseApiKeyOut - Successful Response - - Examples - -------- - import asyncio - - from onepin import AsyncOnePinClient - - client = AsyncOnePinClient( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.api_keys.delete_api_key( - key_id="key_id", - ) - - - asyncio.run(main()) - """ - _response = await self._raw_client.delete_api_key( - key_id, workspace_id=workspace_id, request_options=request_options - ) - return _response.data - - async def update_api_key( - self, - key_id: str, - *, - workspace_id: typing.Optional[str] = None, - name: typing.Optional[str] = OMIT, - scopes: typing.Optional[typing.Sequence[ApiKeyScope]] = OMIT, - rate_limit_per_min: typing.Optional[int] = OMIT, - active: typing.Optional[bool] = OMIT, - request_options: typing.Optional[RequestOptions] = None, - ) -> ApiResponseApiKeyOut: - """ - Update API-key metadata, scopes, rate limit, or active state. - - Parameters - ---------- - key_id : str - - workspace_id : typing.Optional[str] - - name : typing.Optional[str] - - scopes : typing.Optional[typing.Sequence[ApiKeyScope]] - - rate_limit_per_min : typing.Optional[int] - - active : typing.Optional[bool] - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ApiResponseApiKeyOut - Successful Response - - Examples - -------- - import asyncio - - from onepin import AsyncOnePinClient - - client = AsyncOnePinClient( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.api_keys.update_api_key( - key_id="key_id", - ) - - - asyncio.run(main()) - """ - _response = await self._raw_client.update_api_key( - key_id, - workspace_id=workspace_id, - name=name, - scopes=scopes, - rate_limit_per_min=rate_limit_per_min, - active=active, - request_options=request_options, - ) - return _response.data - - async def rotate_api_key( - self, - key_id: str, - *, - workspace_id: typing.Optional[str] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> ApiResponseApiKeyRotateOut: - """ - Rotate an API key by revoking the old row and creating a new key. - - Parameters - ---------- - key_id : str - - workspace_id : typing.Optional[str] - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ApiResponseApiKeyRotateOut - Successful Response - - Examples - -------- - import asyncio - - from onepin import AsyncOnePinClient - - client = AsyncOnePinClient( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.api_keys.rotate_api_key( - key_id="key_id", - ) - - - asyncio.run(main()) - """ - _response = await self._raw_client.rotate_api_key( - key_id, workspace_id=workspace_id, request_options=request_options - ) - return _response.data diff --git a/src/onepin/api_keys/raw_client.py b/src/onepin/api_keys/raw_client.py deleted file mode 100644 index b174ee6..0000000 --- a/src/onepin/api_keys/raw_client.py +++ /dev/null @@ -1,877 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing -from json.decoder import JSONDecodeError - -from ..core.api_error import ApiError -from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper -from ..core.http_response import AsyncHttpResponse, HttpResponse -from ..core.jsonable_encoder import encode_path_param -from ..core.parse_error import ParsingError -from ..core.pydantic_utilities import parse_obj_as -from ..core.request_options import RequestOptions -from ..errors.unprocessable_entity_error import UnprocessableEntityError -from ..types.api_counted_list_response_api_key_out import ApiCountedListResponseApiKeyOut -from ..types.api_key_list_status import ApiKeyListStatus -from ..types.api_key_scope import ApiKeyScope -from ..types.api_response_api_key_created_out import ApiResponseApiKeyCreatedOut -from ..types.api_response_api_key_out import ApiResponseApiKeyOut -from ..types.api_response_api_key_rotate_out import ApiResponseApiKeyRotateOut -from pydantic import ValidationError - -# this is used as the default value for optional parameters -OMIT = typing.cast(typing.Any, ...) - - -class RawApiKeysClient: - def __init__(self, *, client_wrapper: SyncClientWrapper): - self._client_wrapper = client_wrapper - - def list_api_keys( - self, - *, - offset: typing.Optional[int] = None, - limit: typing.Optional[int] = None, - status: typing.Optional[ApiKeyListStatus] = None, - workspace_id: typing.Optional[str] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> HttpResponse[ApiCountedListResponseApiKeyOut]: - """ - List API keys for the current workspace without secret material. - - Parameters - ---------- - offset : typing.Optional[int] - - limit : typing.Optional[int] - - status : typing.Optional[ApiKeyListStatus] - API-key list status filter. Defaults to currently usable keys. `revoked` returns unavailable keys (`active=false` or `revoked_at` is set); `all` returns all workspace API-key metadata rows. - - workspace_id : typing.Optional[str] - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - HttpResponse[ApiCountedListResponseApiKeyOut] - Successful Response - """ - _response = self._client_wrapper.httpx_client.request( - "api/v1/api-keys", - method="GET", - params={ - "offset": offset, - "limit": limit, - "status": status, - }, - 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( - ApiCountedListResponseApiKeyOut, - parse_obj_as( - type_=ApiCountedListResponseApiKeyOut, # 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 create_api_key( - self, - *, - name: str, - workspace_id: typing.Optional[str] = None, - scopes: typing.Optional[typing.Sequence[ApiKeyScope]] = OMIT, - rate_limit_per_min: typing.Optional[int] = OMIT, - key_type: typing.Optional[str] = OMIT, - request_options: typing.Optional[RequestOptions] = None, - ) -> HttpResponse[ApiResponseApiKeyCreatedOut]: - """ - Create a live API key and return its plaintext value exactly once. - - Parameters - ---------- - name : str - - workspace_id : typing.Optional[str] - - scopes : typing.Optional[typing.Sequence[ApiKeyScope]] - - rate_limit_per_min : typing.Optional[int] - - key_type : typing.Optional[str] - Phase 1 supports live bearer keys only; test/public are reserved. - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - HttpResponse[ApiResponseApiKeyCreatedOut] - Successful Response - """ - _response = self._client_wrapper.httpx_client.request( - "api/v1/api-keys", - method="POST", - json={ - "name": name, - "scopes": scopes, - "rate_limit_per_min": rate_limit_per_min, - "key_type": key_type, - }, - 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: - _data = typing.cast( - ApiResponseApiKeyCreatedOut, - parse_obj_as( - type_=ApiResponseApiKeyCreatedOut, # 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_api_key( - self, - key_id: str, - *, - workspace_id: typing.Optional[str] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> HttpResponse[ApiResponseApiKeyOut]: - """ - Get one API-key metadata record for the current workspace. - - Parameters - ---------- - key_id : str - - workspace_id : typing.Optional[str] - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - HttpResponse[ApiResponseApiKeyOut] - Successful Response - """ - _response = self._client_wrapper.httpx_client.request( - f"api/v1/api-keys/{encode_path_param(key_id)}", - method="GET", - 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( - ApiResponseApiKeyOut, - parse_obj_as( - type_=ApiResponseApiKeyOut, # 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 delete_api_key( - self, - key_id: str, - *, - workspace_id: typing.Optional[str] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> HttpResponse[ApiResponseApiKeyOut]: - """ - Soft-revoke an API key for the current workspace. - - Parameters - ---------- - key_id : str - - workspace_id : typing.Optional[str] - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - HttpResponse[ApiResponseApiKeyOut] - Successful Response - """ - _response = self._client_wrapper.httpx_client.request( - f"api/v1/api-keys/{encode_path_param(key_id)}", - method="DELETE", - 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( - ApiResponseApiKeyOut, - parse_obj_as( - type_=ApiResponseApiKeyOut, # 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 update_api_key( - self, - key_id: str, - *, - workspace_id: typing.Optional[str] = None, - name: typing.Optional[str] = OMIT, - scopes: typing.Optional[typing.Sequence[ApiKeyScope]] = OMIT, - rate_limit_per_min: typing.Optional[int] = OMIT, - active: typing.Optional[bool] = OMIT, - request_options: typing.Optional[RequestOptions] = None, - ) -> HttpResponse[ApiResponseApiKeyOut]: - """ - Update API-key metadata, scopes, rate limit, or active state. - - Parameters - ---------- - key_id : str - - workspace_id : typing.Optional[str] - - name : typing.Optional[str] - - scopes : typing.Optional[typing.Sequence[ApiKeyScope]] - - rate_limit_per_min : typing.Optional[int] - - active : typing.Optional[bool] - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - HttpResponse[ApiResponseApiKeyOut] - Successful Response - """ - _response = self._client_wrapper.httpx_client.request( - f"api/v1/api-keys/{encode_path_param(key_id)}", - method="PATCH", - json={ - "name": name, - "scopes": scopes, - "rate_limit_per_min": rate_limit_per_min, - "active": active, - }, - 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: - _data = typing.cast( - ApiResponseApiKeyOut, - parse_obj_as( - type_=ApiResponseApiKeyOut, # 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 rotate_api_key( - self, - key_id: str, - *, - workspace_id: typing.Optional[str] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> HttpResponse[ApiResponseApiKeyRotateOut]: - """ - Rotate an API key by revoking the old row and creating a new key. - - Parameters - ---------- - key_id : str - - workspace_id : typing.Optional[str] - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - HttpResponse[ApiResponseApiKeyRotateOut] - Successful Response - """ - _response = self._client_wrapper.httpx_client.request( - f"api/v1/api-keys/{encode_path_param(key_id)}/rotate", - method="PATCH", - 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( - ApiResponseApiKeyRotateOut, - parse_obj_as( - type_=ApiResponseApiKeyRotateOut, # 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) - - -class AsyncRawApiKeysClient: - def __init__(self, *, client_wrapper: AsyncClientWrapper): - self._client_wrapper = client_wrapper - - async def list_api_keys( - self, - *, - offset: typing.Optional[int] = None, - limit: typing.Optional[int] = None, - status: typing.Optional[ApiKeyListStatus] = None, - workspace_id: typing.Optional[str] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> AsyncHttpResponse[ApiCountedListResponseApiKeyOut]: - """ - List API keys for the current workspace without secret material. - - Parameters - ---------- - offset : typing.Optional[int] - - limit : typing.Optional[int] - - status : typing.Optional[ApiKeyListStatus] - API-key list status filter. Defaults to currently usable keys. `revoked` returns unavailable keys (`active=false` or `revoked_at` is set); `all` returns all workspace API-key metadata rows. - - workspace_id : typing.Optional[str] - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AsyncHttpResponse[ApiCountedListResponseApiKeyOut] - Successful Response - """ - _response = await self._client_wrapper.httpx_client.request( - "api/v1/api-keys", - method="GET", - params={ - "offset": offset, - "limit": limit, - "status": status, - }, - 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( - ApiCountedListResponseApiKeyOut, - parse_obj_as( - type_=ApiCountedListResponseApiKeyOut, # 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 create_api_key( - self, - *, - name: str, - workspace_id: typing.Optional[str] = None, - scopes: typing.Optional[typing.Sequence[ApiKeyScope]] = OMIT, - rate_limit_per_min: typing.Optional[int] = OMIT, - key_type: typing.Optional[str] = OMIT, - request_options: typing.Optional[RequestOptions] = None, - ) -> AsyncHttpResponse[ApiResponseApiKeyCreatedOut]: - """ - Create a live API key and return its plaintext value exactly once. - - Parameters - ---------- - name : str - - workspace_id : typing.Optional[str] - - scopes : typing.Optional[typing.Sequence[ApiKeyScope]] - - rate_limit_per_min : typing.Optional[int] - - key_type : typing.Optional[str] - Phase 1 supports live bearer keys only; test/public are reserved. - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AsyncHttpResponse[ApiResponseApiKeyCreatedOut] - Successful Response - """ - _response = await self._client_wrapper.httpx_client.request( - "api/v1/api-keys", - method="POST", - json={ - "name": name, - "scopes": scopes, - "rate_limit_per_min": rate_limit_per_min, - "key_type": key_type, - }, - 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: - _data = typing.cast( - ApiResponseApiKeyCreatedOut, - parse_obj_as( - type_=ApiResponseApiKeyCreatedOut, # 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_api_key( - self, - key_id: str, - *, - workspace_id: typing.Optional[str] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> AsyncHttpResponse[ApiResponseApiKeyOut]: - """ - Get one API-key metadata record for the current workspace. - - Parameters - ---------- - key_id : str - - workspace_id : typing.Optional[str] - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AsyncHttpResponse[ApiResponseApiKeyOut] - Successful Response - """ - _response = await self._client_wrapper.httpx_client.request( - f"api/v1/api-keys/{encode_path_param(key_id)}", - method="GET", - 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( - ApiResponseApiKeyOut, - parse_obj_as( - type_=ApiResponseApiKeyOut, # 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 delete_api_key( - self, - key_id: str, - *, - workspace_id: typing.Optional[str] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> AsyncHttpResponse[ApiResponseApiKeyOut]: - """ - Soft-revoke an API key for the current workspace. - - Parameters - ---------- - key_id : str - - workspace_id : typing.Optional[str] - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AsyncHttpResponse[ApiResponseApiKeyOut] - Successful Response - """ - _response = await self._client_wrapper.httpx_client.request( - f"api/v1/api-keys/{encode_path_param(key_id)}", - method="DELETE", - 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( - ApiResponseApiKeyOut, - parse_obj_as( - type_=ApiResponseApiKeyOut, # 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 update_api_key( - self, - key_id: str, - *, - workspace_id: typing.Optional[str] = None, - name: typing.Optional[str] = OMIT, - scopes: typing.Optional[typing.Sequence[ApiKeyScope]] = OMIT, - rate_limit_per_min: typing.Optional[int] = OMIT, - active: typing.Optional[bool] = OMIT, - request_options: typing.Optional[RequestOptions] = None, - ) -> AsyncHttpResponse[ApiResponseApiKeyOut]: - """ - Update API-key metadata, scopes, rate limit, or active state. - - Parameters - ---------- - key_id : str - - workspace_id : typing.Optional[str] - - name : typing.Optional[str] - - scopes : typing.Optional[typing.Sequence[ApiKeyScope]] - - rate_limit_per_min : typing.Optional[int] - - active : typing.Optional[bool] - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AsyncHttpResponse[ApiResponseApiKeyOut] - Successful Response - """ - _response = await self._client_wrapper.httpx_client.request( - f"api/v1/api-keys/{encode_path_param(key_id)}", - method="PATCH", - json={ - "name": name, - "scopes": scopes, - "rate_limit_per_min": rate_limit_per_min, - "active": active, - }, - 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: - _data = typing.cast( - ApiResponseApiKeyOut, - parse_obj_as( - type_=ApiResponseApiKeyOut, # 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 rotate_api_key( - self, - key_id: str, - *, - workspace_id: typing.Optional[str] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> AsyncHttpResponse[ApiResponseApiKeyRotateOut]: - """ - Rotate an API key by revoking the old row and creating a new key. - - Parameters - ---------- - key_id : str - - workspace_id : typing.Optional[str] - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AsyncHttpResponse[ApiResponseApiKeyRotateOut] - Successful Response - """ - _response = await self._client_wrapper.httpx_client.request( - f"api/v1/api-keys/{encode_path_param(key_id)}/rotate", - method="PATCH", - 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( - ApiResponseApiKeyRotateOut, - parse_obj_as( - type_=ApiResponseApiKeyRotateOut, # 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) diff --git a/src/onepin/auth/client.py b/src/onepin/auth/client.py index 4be582e..6d0361c 100644 --- a/src/onepin/auth/client.py +++ b/src/onepin/auth/client.py @@ -25,7 +25,17 @@ def with_raw_response(self) -> RawAuthClient: def whoami(self, *, request_options: typing.Optional[RequestOptions] = None) -> ApiResponseAuthWhoamiOut: """ - Return the resolved Clerk or API-key authentication context. + Return the resolved authentication context for the current credential. + + Useful for verifying that a Bearer JWT or API key is valid and discovering + which workspace and permission scopes it grants — call this first when + debugging authentication issues or bootstrapping an SDK integration. + + The `auth_kind` field indicates whether the credential is a session token + (`clerk`) or a programmatic key (`api_key`). For API keys, `workspace_id` + and `api_key_id` are always populated; for session tokens, `workspace_id` + reflects the `X-Workspace-Id` header value (if present) and `api_key_id` + is `null`. The `scopes` list is sorted and deduplicated. Parameters ---------- @@ -67,7 +77,17 @@ def with_raw_response(self) -> AsyncRawAuthClient: async def whoami(self, *, request_options: typing.Optional[RequestOptions] = None) -> ApiResponseAuthWhoamiOut: """ - Return the resolved Clerk or API-key authentication context. + Return the resolved authentication context for the current credential. + + Useful for verifying that a Bearer JWT or API key is valid and discovering + which workspace and permission scopes it grants — call this first when + debugging authentication issues or bootstrapping an SDK integration. + + The `auth_kind` field indicates whether the credential is a session token + (`clerk`) or a programmatic key (`api_key`). For API keys, `workspace_id` + and `api_key_id` are always populated; for session tokens, `workspace_id` + reflects the `X-Workspace-Id` header value (if present) and `api_key_id` + is `null`. The `scopes` list is sorted and deduplicated. Parameters ---------- diff --git a/src/onepin/auth/raw_client.py b/src/onepin/auth/raw_client.py index 1b3300d..b4e1d11 100644 --- a/src/onepin/auth/raw_client.py +++ b/src/onepin/auth/raw_client.py @@ -22,7 +22,17 @@ def whoami( self, *, request_options: typing.Optional[RequestOptions] = None ) -> HttpResponse[ApiResponseAuthWhoamiOut]: """ - Return the resolved Clerk or API-key authentication context. + Return the resolved authentication context for the current credential. + + Useful for verifying that a Bearer JWT or API key is valid and discovering + which workspace and permission scopes it grants — call this first when + debugging authentication issues or bootstrapping an SDK integration. + + The `auth_kind` field indicates whether the credential is a session token + (`clerk`) or a programmatic key (`api_key`). For API keys, `workspace_id` + and `api_key_id` are always populated; for session tokens, `workspace_id` + reflects the `X-Workspace-Id` header value (if present) and `api_key_id` + is `null`. The `scopes` list is sorted and deduplicated. Parameters ---------- @@ -78,7 +88,17 @@ async def whoami( self, *, request_options: typing.Optional[RequestOptions] = None ) -> AsyncHttpResponse[ApiResponseAuthWhoamiOut]: """ - Return the resolved Clerk or API-key authentication context. + Return the resolved authentication context for the current credential. + + Useful for verifying that a Bearer JWT or API key is valid and discovering + which workspace and permission scopes it grants — call this first when + debugging authentication issues or bootstrapping an SDK integration. + + The `auth_kind` field indicates whether the credential is a session token + (`clerk`) or a programmatic key (`api_key`). For API keys, `workspace_id` + and `api_key_id` are always populated; for session tokens, `workspace_id` + reflects the `X-Workspace-Id` header value (if present) and `api_key_id` + is `null`. The `scopes` list is sorted and deduplicated. Parameters ---------- diff --git a/src/onepin/billing/__init__.py b/src/onepin/billing/__init__.py deleted file mode 100644 index 5cde020..0000000 --- a/src/onepin/billing/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -# isort: skip_file - diff --git a/src/onepin/billing/client.py b/src/onepin/billing/client.py deleted file mode 100644 index 3c21766..0000000 --- a/src/onepin/billing/client.py +++ /dev/null @@ -1,273 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper -from ..core.request_options import RequestOptions -from ..types.api_list_response_customer_plan_response import ApiListResponseCustomerPlanResponse -from ..types.api_response_checkout_response import ApiResponseCheckoutResponse -from ..types.api_response_customer_plan_change_preview_response import ApiResponseCustomerPlanChangePreviewResponse -from .raw_client import AsyncRawBillingClient, RawBillingClient - -# this is used as the default value for optional parameters -OMIT = typing.cast(typing.Any, ...) - - -class BillingClient: - def __init__(self, *, client_wrapper: SyncClientWrapper): - self._raw_client = RawBillingClient(client_wrapper=client_wrapper) - - @property - def with_raw_response(self) -> RawBillingClient: - """ - Retrieves a raw implementation of this client that returns raw responses. - - Returns - ------- - RawBillingClient - """ - return self._raw_client - - def list_plans( - self, *, request_options: typing.Optional[RequestOptions] = None - ) -> ApiListResponseCustomerPlanResponse: - """ - List subscription plans and features (public, no authentication). - - Public so the marketing site (Framer) can render live pricing without a - Clerk session. Returns the same active, non-custom plan catalog as before - (name, price, interval, limits, localized ``plan_details``). Honors - ``X-Language`` / ``Accept-Language`` for ``plan_details`` (defaults ``en``). - - Parameters - ---------- - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ApiListResponseCustomerPlanResponse - Successful Response - - Examples - -------- - from onepin import OnePinClient - - client = OnePinClient( - token="YOUR_TOKEN", - ) - client.billing.list_plans() - """ - _response = self._raw_client.list_plans(request_options=request_options) - return _response.data - - def preview_plan_change( - self, plan_id: str, *, request_options: typing.Optional[RequestOptions] = None - ) -> ApiResponseCustomerPlanChangePreviewResponse: - """ - Preview the cost of changing to the given plan. - - Parameters - ---------- - plan_id : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ApiResponseCustomerPlanChangePreviewResponse - Successful Response - - Examples - -------- - from onepin import OnePinClient - - client = OnePinClient( - token="YOUR_TOKEN", - ) - client.billing.preview_plan_change( - plan_id="plan_id", - ) - """ - _response = self._raw_client.preview_plan_change(plan_id, request_options=request_options) - return _response.data - - def create_checkout( - self, *, plan_id: str, return_url: str, request_options: typing.Optional[RequestOptions] = None - ) -> ApiResponseCheckoutResponse: - """ - Create a Stripe Checkout session for the given plan. - - Parameters - ---------- - plan_id : str - - return_url : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ApiResponseCheckoutResponse - Successful Response - - Examples - -------- - from onepin import OnePinClient - - client = OnePinClient( - token="YOUR_TOKEN", - ) - client.billing.create_checkout( - plan_id="plan_id", - return_url="return_url", - ) - """ - _response = self._raw_client.create_checkout( - plan_id=plan_id, return_url=return_url, request_options=request_options - ) - return _response.data - - -class AsyncBillingClient: - def __init__(self, *, client_wrapper: AsyncClientWrapper): - self._raw_client = AsyncRawBillingClient(client_wrapper=client_wrapper) - - @property - def with_raw_response(self) -> AsyncRawBillingClient: - """ - Retrieves a raw implementation of this client that returns raw responses. - - Returns - ------- - AsyncRawBillingClient - """ - return self._raw_client - - async def list_plans( - self, *, request_options: typing.Optional[RequestOptions] = None - ) -> ApiListResponseCustomerPlanResponse: - """ - List subscription plans and features (public, no authentication). - - Public so the marketing site (Framer) can render live pricing without a - Clerk session. Returns the same active, non-custom plan catalog as before - (name, price, interval, limits, localized ``plan_details``). Honors - ``X-Language`` / ``Accept-Language`` for ``plan_details`` (defaults ``en``). - - Parameters - ---------- - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ApiListResponseCustomerPlanResponse - Successful Response - - Examples - -------- - import asyncio - - from onepin import AsyncOnePinClient - - client = AsyncOnePinClient( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.billing.list_plans() - - - asyncio.run(main()) - """ - _response = await self._raw_client.list_plans(request_options=request_options) - return _response.data - - async def preview_plan_change( - self, plan_id: str, *, request_options: typing.Optional[RequestOptions] = None - ) -> ApiResponseCustomerPlanChangePreviewResponse: - """ - Preview the cost of changing to the given plan. - - Parameters - ---------- - plan_id : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ApiResponseCustomerPlanChangePreviewResponse - Successful Response - - Examples - -------- - import asyncio - - from onepin import AsyncOnePinClient - - client = AsyncOnePinClient( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.billing.preview_plan_change( - plan_id="plan_id", - ) - - - asyncio.run(main()) - """ - _response = await self._raw_client.preview_plan_change(plan_id, request_options=request_options) - return _response.data - - async def create_checkout( - self, *, plan_id: str, return_url: str, request_options: typing.Optional[RequestOptions] = None - ) -> ApiResponseCheckoutResponse: - """ - Create a Stripe Checkout session for the given plan. - - Parameters - ---------- - plan_id : str - - return_url : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ApiResponseCheckoutResponse - Successful Response - - Examples - -------- - import asyncio - - from onepin import AsyncOnePinClient - - client = AsyncOnePinClient( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.billing.create_checkout( - plan_id="plan_id", - return_url="return_url", - ) - - - asyncio.run(main()) - """ - _response = await self._raw_client.create_checkout( - plan_id=plan_id, return_url=return_url, request_options=request_options - ) - return _response.data diff --git a/src/onepin/billing/raw_client.py b/src/onepin/billing/raw_client.py deleted file mode 100644 index 1885d3c..0000000 --- a/src/onepin/billing/raw_client.py +++ /dev/null @@ -1,352 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing -from json.decoder import JSONDecodeError - -from ..core.api_error import ApiError -from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper -from ..core.http_response import AsyncHttpResponse, HttpResponse -from ..core.jsonable_encoder import encode_path_param -from ..core.parse_error import ParsingError -from ..core.pydantic_utilities import parse_obj_as -from ..core.request_options import RequestOptions -from ..errors.unprocessable_entity_error import UnprocessableEntityError -from ..types.api_list_response_customer_plan_response import ApiListResponseCustomerPlanResponse -from ..types.api_response_checkout_response import ApiResponseCheckoutResponse -from ..types.api_response_customer_plan_change_preview_response import ApiResponseCustomerPlanChangePreviewResponse -from pydantic import ValidationError - -# this is used as the default value for optional parameters -OMIT = typing.cast(typing.Any, ...) - - -class RawBillingClient: - def __init__(self, *, client_wrapper: SyncClientWrapper): - self._client_wrapper = client_wrapper - - def list_plans( - self, *, request_options: typing.Optional[RequestOptions] = None - ) -> HttpResponse[ApiListResponseCustomerPlanResponse]: - """ - List subscription plans and features (public, no authentication). - - Public so the marketing site (Framer) can render live pricing without a - Clerk session. Returns the same active, non-custom plan catalog as before - (name, price, interval, limits, localized ``plan_details``). Honors - ``X-Language`` / ``Accept-Language`` for ``plan_details`` (defaults ``en``). - - Parameters - ---------- - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - HttpResponse[ApiListResponseCustomerPlanResponse] - Successful Response - """ - _response = self._client_wrapper.httpx_client.request( - "api/v1/billing/plans", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - ApiListResponseCustomerPlanResponse, - parse_obj_as( - type_=ApiListResponseCustomerPlanResponse, # type: ignore - object_=_response.json(), - ), - ) - return HttpResponse(response=_response, data=_data) - _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 preview_plan_change( - self, plan_id: str, *, request_options: typing.Optional[RequestOptions] = None - ) -> HttpResponse[ApiResponseCustomerPlanChangePreviewResponse]: - """ - Preview the cost of changing to the given plan. - - Parameters - ---------- - plan_id : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - HttpResponse[ApiResponseCustomerPlanChangePreviewResponse] - Successful Response - """ - _response = self._client_wrapper.httpx_client.request( - f"api/v1/billing/plans/{encode_path_param(plan_id)}/preview", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - ApiResponseCustomerPlanChangePreviewResponse, - parse_obj_as( - type_=ApiResponseCustomerPlanChangePreviewResponse, # 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 create_checkout( - self, *, plan_id: str, return_url: str, request_options: typing.Optional[RequestOptions] = None - ) -> HttpResponse[ApiResponseCheckoutResponse]: - """ - Create a Stripe Checkout session for the given plan. - - Parameters - ---------- - plan_id : str - - return_url : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - HttpResponse[ApiResponseCheckoutResponse] - Successful Response - """ - _response = self._client_wrapper.httpx_client.request( - "api/v1/billing/checkout", - method="POST", - json={ - "plan_id": plan_id, - "return_url": return_url, - }, - headers={ - "content-type": "application/json", - }, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - ApiResponseCheckoutResponse, - parse_obj_as( - type_=ApiResponseCheckoutResponse, # 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) - - -class AsyncRawBillingClient: - def __init__(self, *, client_wrapper: AsyncClientWrapper): - self._client_wrapper = client_wrapper - - async def list_plans( - self, *, request_options: typing.Optional[RequestOptions] = None - ) -> AsyncHttpResponse[ApiListResponseCustomerPlanResponse]: - """ - List subscription plans and features (public, no authentication). - - Public so the marketing site (Framer) can render live pricing without a - Clerk session. Returns the same active, non-custom plan catalog as before - (name, price, interval, limits, localized ``plan_details``). Honors - ``X-Language`` / ``Accept-Language`` for ``plan_details`` (defaults ``en``). - - Parameters - ---------- - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AsyncHttpResponse[ApiListResponseCustomerPlanResponse] - Successful Response - """ - _response = await self._client_wrapper.httpx_client.request( - "api/v1/billing/plans", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - ApiListResponseCustomerPlanResponse, - parse_obj_as( - type_=ApiListResponseCustomerPlanResponse, # type: ignore - object_=_response.json(), - ), - ) - return AsyncHttpResponse(response=_response, data=_data) - _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 preview_plan_change( - self, plan_id: str, *, request_options: typing.Optional[RequestOptions] = None - ) -> AsyncHttpResponse[ApiResponseCustomerPlanChangePreviewResponse]: - """ - Preview the cost of changing to the given plan. - - Parameters - ---------- - plan_id : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AsyncHttpResponse[ApiResponseCustomerPlanChangePreviewResponse] - Successful Response - """ - _response = await self._client_wrapper.httpx_client.request( - f"api/v1/billing/plans/{encode_path_param(plan_id)}/preview", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - ApiResponseCustomerPlanChangePreviewResponse, - parse_obj_as( - type_=ApiResponseCustomerPlanChangePreviewResponse, # 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 create_checkout( - self, *, plan_id: str, return_url: str, request_options: typing.Optional[RequestOptions] = None - ) -> AsyncHttpResponse[ApiResponseCheckoutResponse]: - """ - Create a Stripe Checkout session for the given plan. - - Parameters - ---------- - plan_id : str - - return_url : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AsyncHttpResponse[ApiResponseCheckoutResponse] - Successful Response - """ - _response = await self._client_wrapper.httpx_client.request( - "api/v1/billing/checkout", - method="POST", - json={ - "plan_id": plan_id, - "return_url": return_url, - }, - headers={ - "content-type": "application/json", - }, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - ApiResponseCheckoutResponse, - parse_obj_as( - type_=ApiResponseCheckoutResponse, # 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) diff --git a/src/onepin/client.py b/src/onepin/client.py index 9fcc96a..0752617 100644 --- a/src/onepin/client.py +++ b/src/onepin/client.py @@ -10,23 +10,17 @@ from .environment import OnePinClientEnvironment if typing.TYPE_CHECKING: - from .api_keys.client import ApiKeysClient, AsyncApiKeysClient from .auth.client import AsyncAuthClient, AuthClient - from .billing.client import AsyncBillingClient, BillingClient from .dictionary.client import AsyncDictionaryClient, DictionaryClient - from .health.client import AsyncHealthClient, HealthClient from .nodes.client import AsyncNodesClient, NodesClient - from .provider_keys.client import AsyncProviderKeysClient, ProviderKeysClient from .providers.client import AsyncProvidersClient, ProvidersClient from .templates.client import AsyncTemplatesClient, TemplatesClient from .uploads.client import AsyncUploadsClient, UploadsClient from .usage.client import AsyncUsageClient, UsageClient from .users.client import AsyncUsersClient, UsersClient from .voices.client import AsyncVoicesClient, VoicesClient - from .webhooks.client import AsyncWebhooksClient, WebhooksClient from .workflows.client import AsyncWorkflowsClient, WorkflowsClient from .workspace.client import AsyncWorkspaceClient, WorkspaceClient - from .workspace_aggregates.client import AsyncWorkspaceAggregatesClient, WorkspaceAggregatesClient from .workspace_members.client import AsyncWorkspaceMembersClient, WorkspaceMembersClient from .workspaces.client import AsyncWorkspacesClient, WorkspacesClient @@ -59,6 +53,12 @@ class OnePinClient: max_retries : typing.Optional[int] The default maximum number of retries for failed requests. Defaults to 2. Per-request `max_retries` in `request_options` takes precedence over this value. + stream_reconnection_enabled : typing.Optional[bool] + Whether to automatically reconnect on stream disconnection for resumable streaming endpoints. Defaults to True. Per-request `stream_reconnection_enabled` in `request_options` takes precedence over this value. + + max_stream_reconnection_attempts : typing.Optional[int] + The maximum number of reconnection attempts for resumable streaming endpoints. Defaults to no limit. Per-request `max_stream_reconnection_attempts` in `request_options` takes precedence over this value. + follow_redirects : typing.Optional[bool] Whether the default httpx client follows redirects or not, this is irrelevant if a custom httpx client is passed in. @@ -86,13 +86,13 @@ def __init__( headers: typing.Optional[typing.Dict[str, str]] = None, timeout: typing.Optional[float] = None, max_retries: typing.Optional[int] = None, + stream_reconnection_enabled: typing.Optional[bool] = None, + max_stream_reconnection_attempts: typing.Optional[int] = None, follow_redirects: typing.Optional[bool] = True, httpx_client: typing.Optional[httpx.Client] = None, logging: typing.Optional[typing.Union[LogConfig, Logger]] = None, ): - _defaulted_timeout = ( - timeout if timeout is not None else 60 if httpx_client is None else httpx_client.timeout.read - ) + _defaulted_timeout = timeout if timeout is not None else 60 if httpx_client is None else None _defaulted_max_retries = max_retries if max_retries is not None else 2 self._client_wrapper = SyncClientWrapper( base_url=_get_base_url(base_url=base_url, environment=environment), @@ -105,44 +105,24 @@ def __init__( else httpx.Client(timeout=_defaulted_timeout), timeout=_defaulted_timeout, max_retries=_defaulted_max_retries, + stream_reconnection_enabled=stream_reconnection_enabled, + max_stream_reconnection_attempts=max_stream_reconnection_attempts, logging=logging, ) - self._health: typing.Optional[HealthClient] = None - self._webhooks: typing.Optional[WebhooksClient] = None self._auth: typing.Optional[AuthClient] = None - self._api_keys: typing.Optional[ApiKeysClient] = None self._dictionary: typing.Optional[DictionaryClient] = None self._nodes: typing.Optional[NodesClient] = None - self._provider_keys: typing.Optional[ProviderKeysClient] = None self._providers: typing.Optional[ProvidersClient] = None self._templates: typing.Optional[TemplatesClient] = None self._voices: typing.Optional[VoicesClient] = None self._workspace: typing.Optional[WorkspaceClient] = None - self._workspace_aggregates: typing.Optional[WorkspaceAggregatesClient] = None self._workspace_members: typing.Optional[WorkspaceMembersClient] = None self._workspaces: typing.Optional[WorkspacesClient] = None self._uploads: typing.Optional[UploadsClient] = None self._usage: typing.Optional[UsageClient] = None - self._billing: typing.Optional[BillingClient] = None self._users: typing.Optional[UsersClient] = None self._workflows: typing.Optional[WorkflowsClient] = None - @property - def health(self): - if self._health is None: - from .health.client import HealthClient # noqa: E402 - - self._health = HealthClient(client_wrapper=self._client_wrapper) - return self._health - - @property - def webhooks(self): - if self._webhooks is None: - from .webhooks.client import WebhooksClient # noqa: E402 - - self._webhooks = WebhooksClient(client_wrapper=self._client_wrapper) - return self._webhooks - @property def auth(self): if self._auth is None: @@ -151,14 +131,6 @@ def auth(self): self._auth = AuthClient(client_wrapper=self._client_wrapper) return self._auth - @property - def api_keys(self): - if self._api_keys is None: - from .api_keys.client import ApiKeysClient # noqa: E402 - - self._api_keys = ApiKeysClient(client_wrapper=self._client_wrapper) - return self._api_keys - @property def dictionary(self): if self._dictionary is None: @@ -175,14 +147,6 @@ def nodes(self): self._nodes = NodesClient(client_wrapper=self._client_wrapper) return self._nodes - @property - def provider_keys(self): - if self._provider_keys is None: - from .provider_keys.client import ProviderKeysClient # noqa: E402 - - self._provider_keys = ProviderKeysClient(client_wrapper=self._client_wrapper) - return self._provider_keys - @property def providers(self): if self._providers is None: @@ -215,14 +179,6 @@ def workspace(self): self._workspace = WorkspaceClient(client_wrapper=self._client_wrapper) return self._workspace - @property - def workspace_aggregates(self): - if self._workspace_aggregates is None: - from .workspace_aggregates.client import WorkspaceAggregatesClient # noqa: E402 - - self._workspace_aggregates = WorkspaceAggregatesClient(client_wrapper=self._client_wrapper) - return self._workspace_aggregates - @property def workspace_members(self): if self._workspace_members is None: @@ -255,14 +211,6 @@ def usage(self): self._usage = UsageClient(client_wrapper=self._client_wrapper) return self._usage - @property - def billing(self): - if self._billing is None: - from .billing.client import BillingClient # noqa: E402 - - self._billing = BillingClient(client_wrapper=self._client_wrapper) - return self._billing - @property def users(self): if self._users is None: @@ -329,6 +277,12 @@ class AsyncOnePinClient: max_retries : typing.Optional[int] The default maximum number of retries for failed requests. Defaults to 2. Per-request `max_retries` in `request_options` takes precedence over this value. + stream_reconnection_enabled : typing.Optional[bool] + Whether to automatically reconnect on stream disconnection for resumable streaming endpoints. Defaults to True. Per-request `stream_reconnection_enabled` in `request_options` takes precedence over this value. + + max_stream_reconnection_attempts : typing.Optional[int] + The maximum number of reconnection attempts for resumable streaming endpoints. Defaults to no limit. Per-request `max_stream_reconnection_attempts` in `request_options` takes precedence over this value. + follow_redirects : typing.Optional[bool] Whether the default httpx client follows redirects or not, this is irrelevant if a custom httpx client is passed in. @@ -357,13 +311,13 @@ def __init__( async_token: typing.Optional[typing.Callable[[], typing.Awaitable[str]]] = None, timeout: typing.Optional[float] = None, max_retries: typing.Optional[int] = None, + stream_reconnection_enabled: typing.Optional[bool] = None, + max_stream_reconnection_attempts: typing.Optional[int] = None, follow_redirects: typing.Optional[bool] = True, httpx_client: typing.Optional[httpx.AsyncClient] = None, logging: typing.Optional[typing.Union[LogConfig, Logger]] = None, ): - _defaulted_timeout = ( - timeout if timeout is not None else 60 if httpx_client is None else httpx_client.timeout.read - ) + _defaulted_timeout = timeout if timeout is not None else 60 if httpx_client is None else None _defaulted_max_retries = max_retries if max_retries is not None else 2 self._client_wrapper = AsyncClientWrapper( base_url=_get_base_url(base_url=base_url, environment=environment), @@ -375,44 +329,24 @@ def __init__( else _make_default_async_client(timeout=_defaulted_timeout, follow_redirects=follow_redirects), timeout=_defaulted_timeout, max_retries=_defaulted_max_retries, + stream_reconnection_enabled=stream_reconnection_enabled, + max_stream_reconnection_attempts=max_stream_reconnection_attempts, logging=logging, ) - self._health: typing.Optional[AsyncHealthClient] = None - self._webhooks: typing.Optional[AsyncWebhooksClient] = None self._auth: typing.Optional[AsyncAuthClient] = None - self._api_keys: typing.Optional[AsyncApiKeysClient] = None self._dictionary: typing.Optional[AsyncDictionaryClient] = None self._nodes: typing.Optional[AsyncNodesClient] = None - self._provider_keys: typing.Optional[AsyncProviderKeysClient] = None self._providers: typing.Optional[AsyncProvidersClient] = None self._templates: typing.Optional[AsyncTemplatesClient] = None self._voices: typing.Optional[AsyncVoicesClient] = None self._workspace: typing.Optional[AsyncWorkspaceClient] = None - self._workspace_aggregates: typing.Optional[AsyncWorkspaceAggregatesClient] = None self._workspace_members: typing.Optional[AsyncWorkspaceMembersClient] = None self._workspaces: typing.Optional[AsyncWorkspacesClient] = None self._uploads: typing.Optional[AsyncUploadsClient] = None self._usage: typing.Optional[AsyncUsageClient] = None - self._billing: typing.Optional[AsyncBillingClient] = None self._users: typing.Optional[AsyncUsersClient] = None self._workflows: typing.Optional[AsyncWorkflowsClient] = None - @property - def health(self): - if self._health is None: - from .health.client import AsyncHealthClient # noqa: E402 - - self._health = AsyncHealthClient(client_wrapper=self._client_wrapper) - return self._health - - @property - def webhooks(self): - if self._webhooks is None: - from .webhooks.client import AsyncWebhooksClient # noqa: E402 - - self._webhooks = AsyncWebhooksClient(client_wrapper=self._client_wrapper) - return self._webhooks - @property def auth(self): if self._auth is None: @@ -421,14 +355,6 @@ def auth(self): self._auth = AsyncAuthClient(client_wrapper=self._client_wrapper) return self._auth - @property - def api_keys(self): - if self._api_keys is None: - from .api_keys.client import AsyncApiKeysClient # noqa: E402 - - self._api_keys = AsyncApiKeysClient(client_wrapper=self._client_wrapper) - return self._api_keys - @property def dictionary(self): if self._dictionary is None: @@ -445,14 +371,6 @@ def nodes(self): self._nodes = AsyncNodesClient(client_wrapper=self._client_wrapper) return self._nodes - @property - def provider_keys(self): - if self._provider_keys is None: - from .provider_keys.client import AsyncProviderKeysClient # noqa: E402 - - self._provider_keys = AsyncProviderKeysClient(client_wrapper=self._client_wrapper) - return self._provider_keys - @property def providers(self): if self._providers is None: @@ -485,14 +403,6 @@ def workspace(self): self._workspace = AsyncWorkspaceClient(client_wrapper=self._client_wrapper) return self._workspace - @property - def workspace_aggregates(self): - if self._workspace_aggregates is None: - from .workspace_aggregates.client import AsyncWorkspaceAggregatesClient # noqa: E402 - - self._workspace_aggregates = AsyncWorkspaceAggregatesClient(client_wrapper=self._client_wrapper) - return self._workspace_aggregates - @property def workspace_members(self): if self._workspace_members is None: @@ -525,14 +435,6 @@ def usage(self): self._usage = AsyncUsageClient(client_wrapper=self._client_wrapper) return self._usage - @property - def billing(self): - if self._billing is None: - from .billing.client import AsyncBillingClient # noqa: E402 - - self._billing = AsyncBillingClient(client_wrapper=self._client_wrapper) - return self._billing - @property def users(self): if self._users is None: diff --git a/src/onepin/core/client_wrapper.py b/src/onepin/core/client_wrapper.py index 82c42af..f88ecaa 100644 --- a/src/onepin/core/client_wrapper.py +++ b/src/onepin/core/client_wrapper.py @@ -16,6 +16,8 @@ def __init__( base_url: str, timeout: typing.Optional[float] = None, max_retries: int = 2, + stream_reconnection_enabled: typing.Optional[bool] = None, + max_stream_reconnection_attempts: typing.Optional[int] = None, logging: typing.Optional[typing.Union[LogConfig, Logger]] = None, ): self._token = token @@ -23,6 +25,8 @@ def __init__( self._base_url = base_url self._timeout = timeout self._max_retries = max_retries + self._stream_reconnection_enabled = stream_reconnection_enabled + self._max_stream_reconnection_attempts = max_stream_reconnection_attempts self._logging = logging def get_headers(self) -> typing.Dict[str, str]: @@ -58,6 +62,12 @@ def get_timeout(self) -> typing.Optional[float]: def get_max_retries(self) -> int: return self._max_retries + def get_stream_reconnection_enabled(self) -> bool: + return self._stream_reconnection_enabled if self._stream_reconnection_enabled is not None else True + + def get_max_stream_reconnection_attempts(self) -> typing.Optional[int]: + return self._max_stream_reconnection_attempts + class SyncClientWrapper(BaseClientWrapper): def __init__( @@ -68,11 +78,20 @@ def __init__( base_url: str, timeout: typing.Optional[float] = None, max_retries: int = 2, + stream_reconnection_enabled: typing.Optional[bool] = None, + max_stream_reconnection_attempts: typing.Optional[int] = None, logging: typing.Optional[typing.Union[LogConfig, Logger]] = None, httpx_client: httpx.Client, ): super().__init__( - token=token, headers=headers, base_url=base_url, timeout=timeout, max_retries=max_retries, logging=logging + token=token, + headers=headers, + base_url=base_url, + timeout=timeout, + max_retries=max_retries, + stream_reconnection_enabled=stream_reconnection_enabled, + max_stream_reconnection_attempts=max_stream_reconnection_attempts, + logging=logging, ) self.httpx_client = HttpClient( httpx_client=httpx_client, @@ -93,12 +112,21 @@ def __init__( base_url: str, timeout: typing.Optional[float] = None, max_retries: int = 2, + stream_reconnection_enabled: typing.Optional[bool] = None, + max_stream_reconnection_attempts: typing.Optional[int] = None, logging: typing.Optional[typing.Union[LogConfig, Logger]] = None, async_token: typing.Optional[typing.Callable[[], typing.Awaitable[str]]] = None, httpx_client: httpx.AsyncClient, ): super().__init__( - token=token, headers=headers, base_url=base_url, timeout=timeout, max_retries=max_retries, logging=logging + token=token, + headers=headers, + base_url=base_url, + timeout=timeout, + max_retries=max_retries, + stream_reconnection_enabled=stream_reconnection_enabled, + max_stream_reconnection_attempts=max_stream_reconnection_attempts, + logging=logging, ) self._async_token = async_token self.httpx_client = AsyncHttpClient( diff --git a/src/onepin/core/http_client.py b/src/onepin/core/http_client.py index f686c57..aae91a2 100644 --- a/src/onepin/core/http_client.py +++ b/src/onepin/core/http_client.py @@ -312,11 +312,12 @@ def request( force_multipart: typing.Optional[bool] = None, ) -> httpx.Response: base_url = self.get_base_url(base_url) - timeout = ( + _timeout = ( 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() ) + timeout = _timeout if _timeout is not None else httpx.USE_CLIENT_DEFAULT json_body, data_body = get_request_body(json=json, data=data, request_options=request_options, omit=omit) @@ -472,11 +473,12 @@ def stream( force_multipart: typing.Optional[bool] = None, ) -> typing.Iterator[httpx.Response]: base_url = self.get_base_url(base_url) - timeout = ( + _timeout = ( 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() ) + timeout = _timeout if _timeout is not None else httpx.USE_CLIENT_DEFAULT request_files: typing.Optional[RequestFiles] = ( convert_file_dict_to_httpx_tuples(remove_omit_from_dict(remove_none_from_dict(files), omit)) @@ -601,11 +603,12 @@ async def request( force_multipart: typing.Optional[bool] = None, ) -> httpx.Response: base_url = self.get_base_url(base_url) - timeout = ( + _timeout = ( 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() ) + timeout = _timeout if _timeout is not None else httpx.USE_CLIENT_DEFAULT request_files: typing.Optional[RequestFiles] = ( convert_file_dict_to_httpx_tuples(remove_omit_from_dict(remove_none_from_dict(files), omit)) @@ -764,11 +767,12 @@ async def stream( force_multipart: typing.Optional[bool] = None, ) -> typing.AsyncIterator[httpx.Response]: base_url = self.get_base_url(base_url) - timeout = ( + _timeout = ( 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() ) + timeout = _timeout if _timeout is not None else httpx.USE_CLIENT_DEFAULT request_files: typing.Optional[RequestFiles] = ( convert_file_dict_to_httpx_tuples(remove_omit_from_dict(remove_none_from_dict(files), omit)) diff --git a/src/onepin/core/http_sse/_api.py b/src/onepin/core/http_sse/_api.py index fd13730..192ec0d 100644 --- a/src/onepin/core/http_sse/_api.py +++ b/src/onepin/core/http_sse/_api.py @@ -3,7 +3,7 @@ import codecs import re from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncGenerator, AsyncIterator, Iterator +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, Optional import httpx from ._decoders import SSEDecoder @@ -14,8 +14,18 @@ class EventSource: - def __init__(self, response: httpx.Response) -> None: + def __init__( + self, + response: httpx.Response, + *, + resumable: bool = False, + stream_reconnection_enabled: bool = True, + max_stream_reconnection_attempts: Optional[int] = None, + ) -> None: self._response = response + self._resumable = resumable + self._stream_reconnection_enabled = stream_reconnection_enabled + self._max_stream_reconnection_attempts = max_stream_reconnection_attempts def _check_content_type(self) -> None: content_type = self._response.headers.get("content-type", "").partition(";")[0] diff --git a/src/onepin/core/request_options.py b/src/onepin/core/request_options.py index 1b38804..ebf17bc 100644 --- a/src/onepin/core/request_options.py +++ b/src/onepin/core/request_options.py @@ -33,3 +33,5 @@ class RequestOptions(typing.TypedDict, total=False): additional_query_parameters: NotRequired[typing.Dict[str, typing.Any]] additional_body_parameters: NotRequired[typing.Dict[str, typing.Any]] chunk_size: NotRequired[int] + stream_reconnection_enabled: NotRequired[bool] + max_stream_reconnection_attempts: NotRequired[int] diff --git a/src/onepin/dictionary/client.py b/src/onepin/dictionary/client.py index cad7026..564dafe 100644 --- a/src/onepin/dictionary/client.py +++ b/src/onepin/dictionary/client.py @@ -62,7 +62,16 @@ def list_dictionary_entries( request_options: typing.Optional[RequestOptions] = None, ) -> ApiListResponseDictionaryOut: """ - List dictionary entries for a single language in the current workspace. + List dictionary entries for a single locale in the current workspace. + + Returns a paginated list of entries for the BCP-47 `language` locale + specified via the `?language=` query parameter (required, e.g. `ko-kr`). + Use `GET /dictionary/search` instead when you need to match by word text + across multiple locales, or `GET /dictionary/languages` to discover which + locales have entries before filtering here. + + `audio_url` on entries with `method=recorded` is a short-lived presigned URL + — do not cache it across sessions. Parameters ---------- @@ -70,15 +79,19 @@ def list_dictionary_entries( BCP-47 language code, e.g. en-us, ko-kr method : typing.Optional[typing.Sequence[DictionaryMethod]] - Repeat for OR, e.g. ?method=spelled&method=recorded + Filter by one or more entry methods. Repeat to OR: `?method=spelled&method=recorded`. Omit to return all methods. sort : typing.Optional[ListDictionaryEntriesApiV1DictionaryGetRequestSort] + Field to sort by. `uses_count` ranks the most-applied entries first, useful for auditing high-impact corrections. order : typing.Optional[ListDictionaryEntriesApiV1DictionaryGetRequestOrder] + Sort direction. offset : typing.Optional[int] + Zero-based pagination offset. limit : typing.Optional[int] + Page size (max 50). workspace_id : typing.Optional[str] @@ -127,26 +140,52 @@ def create_dictionary_entry( request_options: typing.Optional[RequestOptions] = None, ) -> ApiResponseDictionaryOut: """ - Create a new dictionary entry in the current workspace. + Create a pronunciation dictionary entry in the current workspace. + + Dictionary entries teach the synthesis pipeline how to pronounce words that + it would otherwise handle incorrectly — brand names, acronyms, technical + terms, proper nouns, and foreign loanwords. Each entry is scoped to a single + BCP-47 locale and is applied during workflow execution when that locale is + the synthesis target. + + Three methods are supported via the `method` field: + + - `spelled` — provide a phonetic respelling in `pronunciation` (e.g. + `"Poh-doh-nohs"`). `pronunciation` is required for this method. + - `recorded` — attach a reference audio clip by supplying an `upload_id` + from a completed `/uploads` staging upload with category `dictionary`. + The audio is copied to permanent storage on create; the upload slot is + consumed and cannot be reused for a different entry. + - `ipa` — supply an IPA transcription in `ipa`. `pronunciation` is optional + as a human-readable gloss alongside the IPA. + + Returns 409 if a `(word, language)` pair already exists in the workspace. + Requires `editor` workspace role and the `dictionary:write` scope. Parameters ---------- word : str + The surface form of the word or phrase as it appears in a script. method : DictionaryMethod + Pronunciation method: `spelled` (phonetic respelling), `recorded` (reference audio clip), or `ipa` (IPA transcription). language : str + BCP-47 locale this entry applies to (e.g. `ko-kr`). Case-insensitive; stored lowercase. workspace_id : typing.Optional[str] description : typing.Optional[str] + Optional human-readable note about the entry (e.g. context, source). pronunciation : typing.Optional[str] + Phonetic respelling. Required when `method` is `spelled`. upload_id : typing.Optional[str] + ID of a completed staging upload (category `dictionary`). Required when `method` is `recorded`; consumed on create. ipa : typing.Optional[str] - User-provided IPA transcription. Persisted as-is. Auto-generation via phonemizer/LLM is a POD-256 follow-up. + IPA transcription of the word. Supplied by the caller; automatic generation is a planned enhancement. request_options : typing.Optional[RequestOptions] Request-specific configuration. @@ -197,19 +236,32 @@ def search_dictionary_entries( request_options: typing.Optional[RequestOptions] = None, ) -> ApiListResponseDictionaryOut: """ - Cross-language search over dictionary entries. + Search dictionary entries by word text across one or more locales. + + Performs a case-insensitive substring match on the `word` field. Optionally + narrow to one or more BCP-47 locales by repeating `?language=` (OR logic). + Omitting `language` searches across all locales in the workspace. + + Use `GET /dictionary` (locale-scoped list) when you want the full entry list + for a specific locale; use this endpoint when you need to find how a word + is defined across languages or when the user is typing a search query. Parameters ---------- search : str + Substring to match against the `word` field (case-insensitive). sort : typing.Optional[SearchDictionaryEntriesApiV1DictionarySearchGetRequestSort] + Field to sort by. order : typing.Optional[SearchDictionaryEntriesApiV1DictionarySearchGetRequestOrder] + Sort direction. offset : typing.Optional[int] + Zero-based pagination offset. limit : typing.Optional[int] + Page size (max 50). language : typing.Optional[typing.Sequence[SearchDictionaryEntriesApiV1DictionarySearchGetRequestLanguageItem]] Repeat for OR, e.g. ?language=en-us&language=ko-kr @@ -251,7 +303,12 @@ def list_dictionary_languages( self, *, workspace_id: typing.Optional[str] = None, request_options: typing.Optional[RequestOptions] = None ) -> ApiResponseListDictionaryLanguageOut: """ - Return distinct languages with entry counts, ordered by count DESC, code ASC. + Return all locales that have at least one dictionary entry, with entry counts. + + Results are ordered by entry count descending, then BCP-47 locale code + ascending. Use this endpoint to populate a locale filter dropdown before + calling `GET /dictionary?language=`, rather than hard-coding the supported + locale list in your client. Parameters ---------- @@ -288,21 +345,25 @@ def suggest_pronunciation( request_options: typing.Optional[RequestOptions] = None, ) -> ApiResponsePronunciationSuggestion: """ - Return a deterministic FE-parity pronunciation fallback. + Generate a pronunciation suggestion for a word before saving it as a dictionary entry. - ``language`` is reserved for future per-locale rules; ``ipa`` is reserved - for a future generator and is always ``None`` in this version. + Returns a `pronunciation` string suitable for use as the `pronunciation` field + when creating a `spelled`-method dictionary entry. The suggestion is + deterministic (same word always returns the same result) and is intended as a + starting point for human review, not as a production-ready transcription. - Workspace scoping is enforced via ``get_current_workspace`` even though the - response is workspace-independent today: this keeps all ``/api/v1/`` - endpoints uniform (see CLAUDE.md §Workspace Scoping) and leaves room for - per-workspace dictionary overrides when the generator lands. + `language` is accepted to maintain a consistent request shape for future + per-locale phonetic rules; it does not affect the current output. `ipa` is + always `null` in this version — automatic IPA generation is a planned + enhancement. Parameters ---------- word : str + The word or phrase to generate a pronunciation suggestion for. language : str + BCP-47 locale of the word. Reserved for future per-locale phonetic rules; does not affect current output. workspace_id : typing.Optional[str] @@ -346,7 +407,20 @@ def update_dictionary_entry( request_options: typing.Optional[RequestOptions] = None, ) -> ApiResponseDictionaryOut: """ - Update a dictionary entry scoped to the current workspace. + Update fields on an existing dictionary entry in the current workspace. + + Supports partial updates — only the fields included in the request body are + changed; omitted fields retain their current values. Passing `null` for + `word`, `method`, or `language` is rejected with 422, as these fields are + required on the stored entry. + + To replace the reference audio on a `recorded`-method entry, supply a new + `upload_id` pointing to a completed staging upload. The previous audio is + orphaned (not deleted from storage) and the new file is copied to permanent + storage atomically. + + Returns 409 if the new `(word, language)` combination already exists in the + workspace. Requires `editor` workspace role and the `dictionary:write` scope. Parameters ---------- @@ -355,19 +429,25 @@ def update_dictionary_entry( workspace_id : typing.Optional[str] word : typing.Optional[str] + Updated surface form. Omit to leave unchanged. description : typing.Optional[str] + Updated human-readable note. Omit to leave unchanged. pronunciation : typing.Optional[str] + Updated phonetic respelling. Required when changing `method` to `spelled`. upload_id : typing.Optional[str] + New staging upload ID to replace the reference audio. Required when changing `method` to `recorded`. method : typing.Optional[DictionaryMethod] + Updated pronunciation method. Omit to leave unchanged. language : typing.Optional[str] + Updated BCP-47 locale. Omit to leave unchanged. ipa : typing.Optional[str] - User-provided IPA transcription. Persisted as-is. Auto-generation via phonemizer/LLM is a POD-256 follow-up. + Updated IPA transcription. Omit to leave unchanged; supply `null` explicitly to clear. request_options : typing.Optional[RequestOptions] Request-specific configuration. @@ -410,7 +490,13 @@ def delete_dictionary_entry( request_options: typing.Optional[RequestOptions] = None, ) -> ApiResponseDict: """ - Soft-delete a dictionary entry scoped to the current workspace. + Delete a dictionary entry from the current workspace. + + The entry is removed from the workspace's dictionary and will no longer + influence synthesis output in subsequent workflow runs. The operation is not + reversible via the API — create a new entry to restore the pronunciation. + Returns an empty `data` object on success. Requires `editor` workspace role + and the `dictionary:write` scope. Parameters ---------- @@ -471,7 +557,16 @@ async def list_dictionary_entries( request_options: typing.Optional[RequestOptions] = None, ) -> ApiListResponseDictionaryOut: """ - List dictionary entries for a single language in the current workspace. + List dictionary entries for a single locale in the current workspace. + + Returns a paginated list of entries for the BCP-47 `language` locale + specified via the `?language=` query parameter (required, e.g. `ko-kr`). + Use `GET /dictionary/search` instead when you need to match by word text + across multiple locales, or `GET /dictionary/languages` to discover which + locales have entries before filtering here. + + `audio_url` on entries with `method=recorded` is a short-lived presigned URL + — do not cache it across sessions. Parameters ---------- @@ -479,15 +574,19 @@ async def list_dictionary_entries( BCP-47 language code, e.g. en-us, ko-kr method : typing.Optional[typing.Sequence[DictionaryMethod]] - Repeat for OR, e.g. ?method=spelled&method=recorded + Filter by one or more entry methods. Repeat to OR: `?method=spelled&method=recorded`. Omit to return all methods. sort : typing.Optional[ListDictionaryEntriesApiV1DictionaryGetRequestSort] + Field to sort by. `uses_count` ranks the most-applied entries first, useful for auditing high-impact corrections. order : typing.Optional[ListDictionaryEntriesApiV1DictionaryGetRequestOrder] + Sort direction. offset : typing.Optional[int] + Zero-based pagination offset. limit : typing.Optional[int] + Page size (max 50). workspace_id : typing.Optional[str] @@ -544,26 +643,52 @@ async def create_dictionary_entry( request_options: typing.Optional[RequestOptions] = None, ) -> ApiResponseDictionaryOut: """ - Create a new dictionary entry in the current workspace. + Create a pronunciation dictionary entry in the current workspace. + + Dictionary entries teach the synthesis pipeline how to pronounce words that + it would otherwise handle incorrectly — brand names, acronyms, technical + terms, proper nouns, and foreign loanwords. Each entry is scoped to a single + BCP-47 locale and is applied during workflow execution when that locale is + the synthesis target. + + Three methods are supported via the `method` field: + + - `spelled` — provide a phonetic respelling in `pronunciation` (e.g. + `"Poh-doh-nohs"`). `pronunciation` is required for this method. + - `recorded` — attach a reference audio clip by supplying an `upload_id` + from a completed `/uploads` staging upload with category `dictionary`. + The audio is copied to permanent storage on create; the upload slot is + consumed and cannot be reused for a different entry. + - `ipa` — supply an IPA transcription in `ipa`. `pronunciation` is optional + as a human-readable gloss alongside the IPA. + + Returns 409 if a `(word, language)` pair already exists in the workspace. + Requires `editor` workspace role and the `dictionary:write` scope. Parameters ---------- word : str + The surface form of the word or phrase as it appears in a script. method : DictionaryMethod + Pronunciation method: `spelled` (phonetic respelling), `recorded` (reference audio clip), or `ipa` (IPA transcription). language : str + BCP-47 locale this entry applies to (e.g. `ko-kr`). Case-insensitive; stored lowercase. workspace_id : typing.Optional[str] description : typing.Optional[str] + Optional human-readable note about the entry (e.g. context, source). pronunciation : typing.Optional[str] + Phonetic respelling. Required when `method` is `spelled`. upload_id : typing.Optional[str] + ID of a completed staging upload (category `dictionary`). Required when `method` is `recorded`; consumed on create. ipa : typing.Optional[str] - User-provided IPA transcription. Persisted as-is. Auto-generation via phonemizer/LLM is a POD-256 follow-up. + IPA transcription of the word. Supplied by the caller; automatic generation is a planned enhancement. request_options : typing.Optional[RequestOptions] Request-specific configuration. @@ -622,19 +747,32 @@ async def search_dictionary_entries( request_options: typing.Optional[RequestOptions] = None, ) -> ApiListResponseDictionaryOut: """ - Cross-language search over dictionary entries. + Search dictionary entries by word text across one or more locales. + + Performs a case-insensitive substring match on the `word` field. Optionally + narrow to one or more BCP-47 locales by repeating `?language=` (OR logic). + Omitting `language` searches across all locales in the workspace. + + Use `GET /dictionary` (locale-scoped list) when you want the full entry list + for a specific locale; use this endpoint when you need to find how a word + is defined across languages or when the user is typing a search query. Parameters ---------- search : str + Substring to match against the `word` field (case-insensitive). sort : typing.Optional[SearchDictionaryEntriesApiV1DictionarySearchGetRequestSort] + Field to sort by. order : typing.Optional[SearchDictionaryEntriesApiV1DictionarySearchGetRequestOrder] + Sort direction. offset : typing.Optional[int] + Zero-based pagination offset. limit : typing.Optional[int] + Page size (max 50). language : typing.Optional[typing.Sequence[SearchDictionaryEntriesApiV1DictionarySearchGetRequestLanguageItem]] Repeat for OR, e.g. ?language=en-us&language=ko-kr @@ -684,7 +822,12 @@ async def list_dictionary_languages( self, *, workspace_id: typing.Optional[str] = None, request_options: typing.Optional[RequestOptions] = None ) -> ApiResponseListDictionaryLanguageOut: """ - Return distinct languages with entry counts, ordered by count DESC, code ASC. + Return all locales that have at least one dictionary entry, with entry counts. + + Results are ordered by entry count descending, then BCP-47 locale code + ascending. Use this endpoint to populate a locale filter dropdown before + calling `GET /dictionary?language=`, rather than hard-coding the supported + locale list in your client. Parameters ---------- @@ -729,21 +872,25 @@ async def suggest_pronunciation( request_options: typing.Optional[RequestOptions] = None, ) -> ApiResponsePronunciationSuggestion: """ - Return a deterministic FE-parity pronunciation fallback. + Generate a pronunciation suggestion for a word before saving it as a dictionary entry. - ``language`` is reserved for future per-locale rules; ``ipa`` is reserved - for a future generator and is always ``None`` in this version. + Returns a `pronunciation` string suitable for use as the `pronunciation` field + when creating a `spelled`-method dictionary entry. The suggestion is + deterministic (same word always returns the same result) and is intended as a + starting point for human review, not as a production-ready transcription. - Workspace scoping is enforced via ``get_current_workspace`` even though the - response is workspace-independent today: this keeps all ``/api/v1/`` - endpoints uniform (see CLAUDE.md §Workspace Scoping) and leaves room for - per-workspace dictionary overrides when the generator lands. + `language` is accepted to maintain a consistent request shape for future + per-locale phonetic rules; it does not affect the current output. `ipa` is + always `null` in this version — automatic IPA generation is a planned + enhancement. Parameters ---------- word : str + The word or phrase to generate a pronunciation suggestion for. language : str + BCP-47 locale of the word. Reserved for future per-locale phonetic rules; does not affect current output. workspace_id : typing.Optional[str] @@ -795,7 +942,20 @@ async def update_dictionary_entry( request_options: typing.Optional[RequestOptions] = None, ) -> ApiResponseDictionaryOut: """ - Update a dictionary entry scoped to the current workspace. + Update fields on an existing dictionary entry in the current workspace. + + Supports partial updates — only the fields included in the request body are + changed; omitted fields retain their current values. Passing `null` for + `word`, `method`, or `language` is rejected with 422, as these fields are + required on the stored entry. + + To replace the reference audio on a `recorded`-method entry, supply a new + `upload_id` pointing to a completed staging upload. The previous audio is + orphaned (not deleted from storage) and the new file is copied to permanent + storage atomically. + + Returns 409 if the new `(word, language)` combination already exists in the + workspace. Requires `editor` workspace role and the `dictionary:write` scope. Parameters ---------- @@ -804,19 +964,25 @@ async def update_dictionary_entry( workspace_id : typing.Optional[str] word : typing.Optional[str] + Updated surface form. Omit to leave unchanged. description : typing.Optional[str] + Updated human-readable note. Omit to leave unchanged. pronunciation : typing.Optional[str] + Updated phonetic respelling. Required when changing `method` to `spelled`. upload_id : typing.Optional[str] + New staging upload ID to replace the reference audio. Required when changing `method` to `recorded`. method : typing.Optional[DictionaryMethod] + Updated pronunciation method. Omit to leave unchanged. language : typing.Optional[str] + Updated BCP-47 locale. Omit to leave unchanged. ipa : typing.Optional[str] - User-provided IPA transcription. Persisted as-is. Auto-generation via phonemizer/LLM is a POD-256 follow-up. + Updated IPA transcription. Omit to leave unchanged; supply `null` explicitly to clear. request_options : typing.Optional[RequestOptions] Request-specific configuration. @@ -867,7 +1033,13 @@ async def delete_dictionary_entry( request_options: typing.Optional[RequestOptions] = None, ) -> ApiResponseDict: """ - Soft-delete a dictionary entry scoped to the current workspace. + Delete a dictionary entry from the current workspace. + + The entry is removed from the workspace's dictionary and will no longer + influence synthesis output in subsequent workflow runs. The operation is not + reversible via the API — create a new entry to restore the pronunciation. + Returns an empty `data` object on success. Requires `editor` workspace role + and the `dictionary:write` scope. Parameters ---------- diff --git a/src/onepin/dictionary/raw_client.py b/src/onepin/dictionary/raw_client.py index 83e78ca..be72790 100644 --- a/src/onepin/dictionary/raw_client.py +++ b/src/onepin/dictionary/raw_client.py @@ -58,7 +58,16 @@ def list_dictionary_entries( request_options: typing.Optional[RequestOptions] = None, ) -> HttpResponse[ApiListResponseDictionaryOut]: """ - List dictionary entries for a single language in the current workspace. + List dictionary entries for a single locale in the current workspace. + + Returns a paginated list of entries for the BCP-47 `language` locale + specified via the `?language=` query parameter (required, e.g. `ko-kr`). + Use `GET /dictionary/search` instead when you need to match by word text + across multiple locales, or `GET /dictionary/languages` to discover which + locales have entries before filtering here. + + `audio_url` on entries with `method=recorded` is a short-lived presigned URL + — do not cache it across sessions. Parameters ---------- @@ -66,15 +75,19 @@ def list_dictionary_entries( BCP-47 language code, e.g. en-us, ko-kr method : typing.Optional[typing.Sequence[DictionaryMethod]] - Repeat for OR, e.g. ?method=spelled&method=recorded + Filter by one or more entry methods. Repeat to OR: `?method=spelled&method=recorded`. Omit to return all methods. sort : typing.Optional[ListDictionaryEntriesApiV1DictionaryGetRequestSort] + Field to sort by. `uses_count` ranks the most-applied entries first, useful for auditing high-impact corrections. order : typing.Optional[ListDictionaryEntriesApiV1DictionaryGetRequestOrder] + Sort direction. offset : typing.Optional[int] + Zero-based pagination offset. limit : typing.Optional[int] + Page size (max 50). workspace_id : typing.Optional[str] @@ -146,26 +159,52 @@ def create_dictionary_entry( request_options: typing.Optional[RequestOptions] = None, ) -> HttpResponse[ApiResponseDictionaryOut]: """ - Create a new dictionary entry in the current workspace. + Create a pronunciation dictionary entry in the current workspace. + + Dictionary entries teach the synthesis pipeline how to pronounce words that + it would otherwise handle incorrectly — brand names, acronyms, technical + terms, proper nouns, and foreign loanwords. Each entry is scoped to a single + BCP-47 locale and is applied during workflow execution when that locale is + the synthesis target. + + Three methods are supported via the `method` field: + + - `spelled` — provide a phonetic respelling in `pronunciation` (e.g. + `"Poh-doh-nohs"`). `pronunciation` is required for this method. + - `recorded` — attach a reference audio clip by supplying an `upload_id` + from a completed `/uploads` staging upload with category `dictionary`. + The audio is copied to permanent storage on create; the upload slot is + consumed and cannot be reused for a different entry. + - `ipa` — supply an IPA transcription in `ipa`. `pronunciation` is optional + as a human-readable gloss alongside the IPA. + + Returns 409 if a `(word, language)` pair already exists in the workspace. + Requires `editor` workspace role and the `dictionary:write` scope. Parameters ---------- word : str + The surface form of the word or phrase as it appears in a script. method : DictionaryMethod + Pronunciation method: `spelled` (phonetic respelling), `recorded` (reference audio clip), or `ipa` (IPA transcription). language : str + BCP-47 locale this entry applies to (e.g. `ko-kr`). Case-insensitive; stored lowercase. workspace_id : typing.Optional[str] description : typing.Optional[str] + Optional human-readable note about the entry (e.g. context, source). pronunciation : typing.Optional[str] + Phonetic respelling. Required when `method` is `spelled`. upload_id : typing.Optional[str] + ID of a completed staging upload (category `dictionary`). Required when `method` is `recorded`; consumed on create. ipa : typing.Optional[str] - User-provided IPA transcription. Persisted as-is. Auto-generation via phonemizer/LLM is a POD-256 follow-up. + IPA transcription of the word. Supplied by the caller; automatic generation is a planned enhancement. request_options : typing.Optional[RequestOptions] Request-specific configuration. @@ -239,19 +278,32 @@ def search_dictionary_entries( request_options: typing.Optional[RequestOptions] = None, ) -> HttpResponse[ApiListResponseDictionaryOut]: """ - Cross-language search over dictionary entries. + Search dictionary entries by word text across one or more locales. + + Performs a case-insensitive substring match on the `word` field. Optionally + narrow to one or more BCP-47 locales by repeating `?language=` (OR logic). + Omitting `language` searches across all locales in the workspace. + + Use `GET /dictionary` (locale-scoped list) when you want the full entry list + for a specific locale; use this endpoint when you need to find how a word + is defined across languages or when the user is typing a search query. Parameters ---------- search : str + Substring to match against the `word` field (case-insensitive). sort : typing.Optional[SearchDictionaryEntriesApiV1DictionarySearchGetRequestSort] + Field to sort by. order : typing.Optional[SearchDictionaryEntriesApiV1DictionarySearchGetRequestOrder] + Sort direction. offset : typing.Optional[int] + Zero-based pagination offset. limit : typing.Optional[int] + Page size (max 50). language : typing.Optional[typing.Sequence[SearchDictionaryEntriesApiV1DictionarySearchGetRequestLanguageItem]] Repeat for OR, e.g. ?language=en-us&language=ko-kr @@ -316,7 +368,12 @@ def list_dictionary_languages( self, *, workspace_id: typing.Optional[str] = None, request_options: typing.Optional[RequestOptions] = None ) -> HttpResponse[ApiResponseListDictionaryLanguageOut]: """ - Return distinct languages with entry counts, ordered by count DESC, code ASC. + Return all locales that have at least one dictionary entry, with entry counts. + + Results are ordered by entry count descending, then BCP-47 locale code + ascending. Use this endpoint to populate a locale filter dropdown before + calling `GET /dictionary?language=`, rather than hard-coding the supported + locale list in your client. Parameters ---------- @@ -377,21 +434,25 @@ def suggest_pronunciation( request_options: typing.Optional[RequestOptions] = None, ) -> HttpResponse[ApiResponsePronunciationSuggestion]: """ - Return a deterministic FE-parity pronunciation fallback. + Generate a pronunciation suggestion for a word before saving it as a dictionary entry. - ``language`` is reserved for future per-locale rules; ``ipa`` is reserved - for a future generator and is always ``None`` in this version. + Returns a `pronunciation` string suitable for use as the `pronunciation` field + when creating a `spelled`-method dictionary entry. The suggestion is + deterministic (same word always returns the same result) and is intended as a + starting point for human review, not as a production-ready transcription. - Workspace scoping is enforced via ``get_current_workspace`` even though the - response is workspace-independent today: this keeps all ``/api/v1/`` - endpoints uniform (see CLAUDE.md §Workspace Scoping) and leaves room for - per-workspace dictionary overrides when the generator lands. + `language` is accepted to maintain a consistent request shape for future + per-locale phonetic rules; it does not affect the current output. `ipa` is + always `null` in this version — automatic IPA generation is a planned + enhancement. Parameters ---------- word : str + The word or phrase to generate a pronunciation suggestion for. language : str + BCP-47 locale of the word. Reserved for future per-locale phonetic rules; does not affect current output. workspace_id : typing.Optional[str] @@ -462,7 +523,20 @@ def update_dictionary_entry( request_options: typing.Optional[RequestOptions] = None, ) -> HttpResponse[ApiResponseDictionaryOut]: """ - Update a dictionary entry scoped to the current workspace. + Update fields on an existing dictionary entry in the current workspace. + + Supports partial updates — only the fields included in the request body are + changed; omitted fields retain their current values. Passing `null` for + `word`, `method`, or `language` is rejected with 422, as these fields are + required on the stored entry. + + To replace the reference audio on a `recorded`-method entry, supply a new + `upload_id` pointing to a completed staging upload. The previous audio is + orphaned (not deleted from storage) and the new file is copied to permanent + storage atomically. + + Returns 409 if the new `(word, language)` combination already exists in the + workspace. Requires `editor` workspace role and the `dictionary:write` scope. Parameters ---------- @@ -471,19 +545,25 @@ def update_dictionary_entry( workspace_id : typing.Optional[str] word : typing.Optional[str] + Updated surface form. Omit to leave unchanged. description : typing.Optional[str] + Updated human-readable note. Omit to leave unchanged. pronunciation : typing.Optional[str] + Updated phonetic respelling. Required when changing `method` to `spelled`. upload_id : typing.Optional[str] + New staging upload ID to replace the reference audio. Required when changing `method` to `recorded`. method : typing.Optional[DictionaryMethod] + Updated pronunciation method. Omit to leave unchanged. language : typing.Optional[str] + Updated BCP-47 locale. Omit to leave unchanged. ipa : typing.Optional[str] - User-provided IPA transcription. Persisted as-is. Auto-generation via phonemizer/LLM is a POD-256 follow-up. + Updated IPA transcription. Omit to leave unchanged; supply `null` explicitly to clear. request_options : typing.Optional[RequestOptions] Request-specific configuration. @@ -550,7 +630,13 @@ def delete_dictionary_entry( request_options: typing.Optional[RequestOptions] = None, ) -> HttpResponse[ApiResponseDict]: """ - Soft-delete a dictionary entry scoped to the current workspace. + Delete a dictionary entry from the current workspace. + + The entry is removed from the workspace's dictionary and will no longer + influence synthesis output in subsequent workflow runs. The operation is not + reversible via the API — create a new entry to restore the pronunciation. + Returns an empty `data` object on success. Requires `editor` workspace role + and the `dictionary:write` scope. Parameters ---------- @@ -622,7 +708,16 @@ async def list_dictionary_entries( request_options: typing.Optional[RequestOptions] = None, ) -> AsyncHttpResponse[ApiListResponseDictionaryOut]: """ - List dictionary entries for a single language in the current workspace. + List dictionary entries for a single locale in the current workspace. + + Returns a paginated list of entries for the BCP-47 `language` locale + specified via the `?language=` query parameter (required, e.g. `ko-kr`). + Use `GET /dictionary/search` instead when you need to match by word text + across multiple locales, or `GET /dictionary/languages` to discover which + locales have entries before filtering here. + + `audio_url` on entries with `method=recorded` is a short-lived presigned URL + — do not cache it across sessions. Parameters ---------- @@ -630,15 +725,19 @@ async def list_dictionary_entries( BCP-47 language code, e.g. en-us, ko-kr method : typing.Optional[typing.Sequence[DictionaryMethod]] - Repeat for OR, e.g. ?method=spelled&method=recorded + Filter by one or more entry methods. Repeat to OR: `?method=spelled&method=recorded`. Omit to return all methods. sort : typing.Optional[ListDictionaryEntriesApiV1DictionaryGetRequestSort] + Field to sort by. `uses_count` ranks the most-applied entries first, useful for auditing high-impact corrections. order : typing.Optional[ListDictionaryEntriesApiV1DictionaryGetRequestOrder] + Sort direction. offset : typing.Optional[int] + Zero-based pagination offset. limit : typing.Optional[int] + Page size (max 50). workspace_id : typing.Optional[str] @@ -710,26 +809,52 @@ async def create_dictionary_entry( request_options: typing.Optional[RequestOptions] = None, ) -> AsyncHttpResponse[ApiResponseDictionaryOut]: """ - Create a new dictionary entry in the current workspace. + Create a pronunciation dictionary entry in the current workspace. + + Dictionary entries teach the synthesis pipeline how to pronounce words that + it would otherwise handle incorrectly — brand names, acronyms, technical + terms, proper nouns, and foreign loanwords. Each entry is scoped to a single + BCP-47 locale and is applied during workflow execution when that locale is + the synthesis target. + + Three methods are supported via the `method` field: + + - `spelled` — provide a phonetic respelling in `pronunciation` (e.g. + `"Poh-doh-nohs"`). `pronunciation` is required for this method. + - `recorded` — attach a reference audio clip by supplying an `upload_id` + from a completed `/uploads` staging upload with category `dictionary`. + The audio is copied to permanent storage on create; the upload slot is + consumed and cannot be reused for a different entry. + - `ipa` — supply an IPA transcription in `ipa`. `pronunciation` is optional + as a human-readable gloss alongside the IPA. + + Returns 409 if a `(word, language)` pair already exists in the workspace. + Requires `editor` workspace role and the `dictionary:write` scope. Parameters ---------- word : str + The surface form of the word or phrase as it appears in a script. method : DictionaryMethod + Pronunciation method: `spelled` (phonetic respelling), `recorded` (reference audio clip), or `ipa` (IPA transcription). language : str + BCP-47 locale this entry applies to (e.g. `ko-kr`). Case-insensitive; stored lowercase. workspace_id : typing.Optional[str] description : typing.Optional[str] + Optional human-readable note about the entry (e.g. context, source). pronunciation : typing.Optional[str] + Phonetic respelling. Required when `method` is `spelled`. upload_id : typing.Optional[str] + ID of a completed staging upload (category `dictionary`). Required when `method` is `recorded`; consumed on create. ipa : typing.Optional[str] - User-provided IPA transcription. Persisted as-is. Auto-generation via phonemizer/LLM is a POD-256 follow-up. + IPA transcription of the word. Supplied by the caller; automatic generation is a planned enhancement. request_options : typing.Optional[RequestOptions] Request-specific configuration. @@ -803,19 +928,32 @@ async def search_dictionary_entries( request_options: typing.Optional[RequestOptions] = None, ) -> AsyncHttpResponse[ApiListResponseDictionaryOut]: """ - Cross-language search over dictionary entries. + Search dictionary entries by word text across one or more locales. + + Performs a case-insensitive substring match on the `word` field. Optionally + narrow to one or more BCP-47 locales by repeating `?language=` (OR logic). + Omitting `language` searches across all locales in the workspace. + + Use `GET /dictionary` (locale-scoped list) when you want the full entry list + for a specific locale; use this endpoint when you need to find how a word + is defined across languages or when the user is typing a search query. Parameters ---------- search : str + Substring to match against the `word` field (case-insensitive). sort : typing.Optional[SearchDictionaryEntriesApiV1DictionarySearchGetRequestSort] + Field to sort by. order : typing.Optional[SearchDictionaryEntriesApiV1DictionarySearchGetRequestOrder] + Sort direction. offset : typing.Optional[int] + Zero-based pagination offset. limit : typing.Optional[int] + Page size (max 50). language : typing.Optional[typing.Sequence[SearchDictionaryEntriesApiV1DictionarySearchGetRequestLanguageItem]] Repeat for OR, e.g. ?language=en-us&language=ko-kr @@ -880,7 +1018,12 @@ async def list_dictionary_languages( self, *, workspace_id: typing.Optional[str] = None, request_options: typing.Optional[RequestOptions] = None ) -> AsyncHttpResponse[ApiResponseListDictionaryLanguageOut]: """ - Return distinct languages with entry counts, ordered by count DESC, code ASC. + Return all locales that have at least one dictionary entry, with entry counts. + + Results are ordered by entry count descending, then BCP-47 locale code + ascending. Use this endpoint to populate a locale filter dropdown before + calling `GET /dictionary?language=`, rather than hard-coding the supported + locale list in your client. Parameters ---------- @@ -941,21 +1084,25 @@ async def suggest_pronunciation( request_options: typing.Optional[RequestOptions] = None, ) -> AsyncHttpResponse[ApiResponsePronunciationSuggestion]: """ - Return a deterministic FE-parity pronunciation fallback. + Generate a pronunciation suggestion for a word before saving it as a dictionary entry. - ``language`` is reserved for future per-locale rules; ``ipa`` is reserved - for a future generator and is always ``None`` in this version. + Returns a `pronunciation` string suitable for use as the `pronunciation` field + when creating a `spelled`-method dictionary entry. The suggestion is + deterministic (same word always returns the same result) and is intended as a + starting point for human review, not as a production-ready transcription. - Workspace scoping is enforced via ``get_current_workspace`` even though the - response is workspace-independent today: this keeps all ``/api/v1/`` - endpoints uniform (see CLAUDE.md §Workspace Scoping) and leaves room for - per-workspace dictionary overrides when the generator lands. + `language` is accepted to maintain a consistent request shape for future + per-locale phonetic rules; it does not affect the current output. `ipa` is + always `null` in this version — automatic IPA generation is a planned + enhancement. Parameters ---------- word : str + The word or phrase to generate a pronunciation suggestion for. language : str + BCP-47 locale of the word. Reserved for future per-locale phonetic rules; does not affect current output. workspace_id : typing.Optional[str] @@ -1026,7 +1173,20 @@ async def update_dictionary_entry( request_options: typing.Optional[RequestOptions] = None, ) -> AsyncHttpResponse[ApiResponseDictionaryOut]: """ - Update a dictionary entry scoped to the current workspace. + Update fields on an existing dictionary entry in the current workspace. + + Supports partial updates — only the fields included in the request body are + changed; omitted fields retain their current values. Passing `null` for + `word`, `method`, or `language` is rejected with 422, as these fields are + required on the stored entry. + + To replace the reference audio on a `recorded`-method entry, supply a new + `upload_id` pointing to a completed staging upload. The previous audio is + orphaned (not deleted from storage) and the new file is copied to permanent + storage atomically. + + Returns 409 if the new `(word, language)` combination already exists in the + workspace. Requires `editor` workspace role and the `dictionary:write` scope. Parameters ---------- @@ -1035,19 +1195,25 @@ async def update_dictionary_entry( workspace_id : typing.Optional[str] word : typing.Optional[str] + Updated surface form. Omit to leave unchanged. description : typing.Optional[str] + Updated human-readable note. Omit to leave unchanged. pronunciation : typing.Optional[str] + Updated phonetic respelling. Required when changing `method` to `spelled`. upload_id : typing.Optional[str] + New staging upload ID to replace the reference audio. Required when changing `method` to `recorded`. method : typing.Optional[DictionaryMethod] + Updated pronunciation method. Omit to leave unchanged. language : typing.Optional[str] + Updated BCP-47 locale. Omit to leave unchanged. ipa : typing.Optional[str] - User-provided IPA transcription. Persisted as-is. Auto-generation via phonemizer/LLM is a POD-256 follow-up. + Updated IPA transcription. Omit to leave unchanged; supply `null` explicitly to clear. request_options : typing.Optional[RequestOptions] Request-specific configuration. @@ -1114,7 +1280,13 @@ async def delete_dictionary_entry( request_options: typing.Optional[RequestOptions] = None, ) -> AsyncHttpResponse[ApiResponseDict]: """ - Soft-delete a dictionary entry scoped to the current workspace. + Delete a dictionary entry from the current workspace. + + The entry is removed from the workspace's dictionary and will no longer + influence synthesis output in subsequent workflow runs. The operation is not + reversible via the API — create a new entry to restore the pronunciation. + Returns an empty `data` object on success. Requires `editor` workspace role + and the `dictionary:write` scope. Parameters ---------- diff --git a/src/onepin/health/__init__.py b/src/onepin/health/__init__.py deleted file mode 100644 index 5cde020..0000000 --- a/src/onepin/health/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -# isort: skip_file - diff --git a/src/onepin/health/client.py b/src/onepin/health/client.py deleted file mode 100644 index 01bb179..0000000 --- a/src/onepin/health/client.py +++ /dev/null @@ -1,159 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper -from ..core.request_options import RequestOptions -from .raw_client import AsyncRawHealthClient, RawHealthClient - - -class HealthClient: - def __init__(self, *, client_wrapper: SyncClientWrapper): - self._raw_client = RawHealthClient(client_wrapper=client_wrapper) - - @property - def with_raw_response(self) -> RawHealthClient: - """ - Retrieves a raw implementation of this client that returns raw responses. - - Returns - ------- - RawHealthClient - """ - return self._raw_client - - def liveness(self, *, request_options: typing.Optional[RequestOptions] = None) -> typing.Any: - """ - Liveness probe — always returns 200. - - Parameters - ---------- - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - typing.Any - Successful Response - - Examples - -------- - from onepin import OnePinClient - - client = OnePinClient( - token="YOUR_TOKEN", - ) - client.health.liveness() - """ - _response = self._raw_client.liveness(request_options=request_options) - return _response.data - - def readiness(self, *, request_options: typing.Optional[RequestOptions] = None) -> typing.Any: - """ - Readiness probe — checks DB and Redis connectivity. - - Parameters - ---------- - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - typing.Any - Successful Response - - Examples - -------- - from onepin import OnePinClient - - client = OnePinClient( - token="YOUR_TOKEN", - ) - client.health.readiness() - """ - _response = self._raw_client.readiness(request_options=request_options) - return _response.data - - -class AsyncHealthClient: - def __init__(self, *, client_wrapper: AsyncClientWrapper): - self._raw_client = AsyncRawHealthClient(client_wrapper=client_wrapper) - - @property - def with_raw_response(self) -> AsyncRawHealthClient: - """ - Retrieves a raw implementation of this client that returns raw responses. - - Returns - ------- - AsyncRawHealthClient - """ - return self._raw_client - - async def liveness(self, *, request_options: typing.Optional[RequestOptions] = None) -> typing.Any: - """ - Liveness probe — always returns 200. - - Parameters - ---------- - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - typing.Any - Successful Response - - Examples - -------- - import asyncio - - from onepin import AsyncOnePinClient - - client = AsyncOnePinClient( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.health.liveness() - - - asyncio.run(main()) - """ - _response = await self._raw_client.liveness(request_options=request_options) - return _response.data - - async def readiness(self, *, request_options: typing.Optional[RequestOptions] = None) -> typing.Any: - """ - Readiness probe — checks DB and Redis connectivity. - - Parameters - ---------- - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - typing.Any - Successful Response - - Examples - -------- - import asyncio - - from onepin import AsyncOnePinClient - - client = AsyncOnePinClient( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.health.readiness() - - - asyncio.run(main()) - """ - _response = await self._raw_client.readiness(request_options=request_options) - return _response.data diff --git a/src/onepin/health/raw_client.py b/src/onepin/health/raw_client.py deleted file mode 100644 index 4bba4eb..0000000 --- a/src/onepin/health/raw_client.py +++ /dev/null @@ -1,186 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing -from json.decoder import JSONDecodeError - -from ..core.api_error import ApiError -from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper -from ..core.http_response import AsyncHttpResponse, HttpResponse -from ..core.parse_error import ParsingError -from ..core.pydantic_utilities import parse_obj_as -from ..core.request_options import RequestOptions -from pydantic import ValidationError - - -class RawHealthClient: - def __init__(self, *, client_wrapper: SyncClientWrapper): - self._client_wrapper = client_wrapper - - def liveness(self, *, request_options: typing.Optional[RequestOptions] = None) -> HttpResponse[typing.Any]: - """ - Liveness probe — always returns 200. - - Parameters - ---------- - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - HttpResponse[typing.Any] - Successful Response - """ - _response = self._client_wrapper.httpx_client.request( - "health", - method="GET", - request_options=request_options, - ) - try: - if _response is None or not _response.text.strip(): - return HttpResponse(response=_response, data=None) - if 200 <= _response.status_code < 300: - _data = typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ) - return HttpResponse(response=_response, data=_data) - _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 readiness(self, *, request_options: typing.Optional[RequestOptions] = None) -> HttpResponse[typing.Any]: - """ - Readiness probe — checks DB and Redis connectivity. - - Parameters - ---------- - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - HttpResponse[typing.Any] - Successful Response - """ - _response = self._client_wrapper.httpx_client.request( - "ready", - method="GET", - request_options=request_options, - ) - try: - if _response is None or not _response.text.strip(): - return HttpResponse(response=_response, data=None) - if 200 <= _response.status_code < 300: - _data = typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ) - return HttpResponse(response=_response, data=_data) - _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) - - -class AsyncRawHealthClient: - def __init__(self, *, client_wrapper: AsyncClientWrapper): - self._client_wrapper = client_wrapper - - async def liveness( - self, *, request_options: typing.Optional[RequestOptions] = None - ) -> AsyncHttpResponse[typing.Any]: - """ - Liveness probe — always returns 200. - - Parameters - ---------- - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AsyncHttpResponse[typing.Any] - Successful Response - """ - _response = await self._client_wrapper.httpx_client.request( - "health", - method="GET", - request_options=request_options, - ) - try: - if _response is None or not _response.text.strip(): - return AsyncHttpResponse(response=_response, data=None) - if 200 <= _response.status_code < 300: - _data = typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ) - return AsyncHttpResponse(response=_response, data=_data) - _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 readiness( - self, *, request_options: typing.Optional[RequestOptions] = None - ) -> AsyncHttpResponse[typing.Any]: - """ - Readiness probe — checks DB and Redis connectivity. - - Parameters - ---------- - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AsyncHttpResponse[typing.Any] - Successful Response - """ - _response = await self._client_wrapper.httpx_client.request( - "ready", - method="GET", - request_options=request_options, - ) - try: - if _response is None or not _response.text.strip(): - return AsyncHttpResponse(response=_response, data=None) - if 200 <= _response.status_code < 300: - _data = typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ) - return AsyncHttpResponse(response=_response, data=_data) - _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) diff --git a/src/onepin/nodes/client.py b/src/onepin/nodes/client.py index a3ec2c1..9294f1b 100644 --- a/src/onepin/nodes/client.py +++ b/src/onepin/nodes/client.py @@ -26,7 +26,18 @@ def with_raw_response(self) -> RawNodesClient: def list_nodes(self, *, request_options: typing.Optional[RequestOptions] = None) -> ApiListResponseNodePortsOut: """ - List all node types and their input/output port definitions. + List all available node types with their input/output port schemas. + + Returns the static structural definition for every node type registered in + the catalog — what ports each node exposes, their names, and expected data + shapes — without runtime-variable values such as available languages or the + TTS model catalog. This endpoint requires no `X-Workspace-Id` header and no + authentication, making it suitable for static documentation generation and + canvas layout tooling. + + For the full runtime configuration options a user would pick when wiring up + a specific node (available target languages, provider/model options, voice + picker URL), use `GET /api/v2/nodes/{node_type}` instead. Parameters ---------- @@ -58,18 +69,19 @@ def get_node_detail( request_options: typing.Optional[RequestOptions] = None, ) -> ApiResponseNodeDetailOut: """ - Return full node definition + runtime options for the canvas node-config UI. + Return full node definition and runtime configuration options for a node type. - **Deprecated (POD-612):** this version inlines the model catalog as - `options.models_by_provider`. Use `GET /api/v2/nodes/{node_type}`, which - replaces the inline tree with a `providers` HATEOAS href to the standalone - catalog `/api/v1/providers`. This endpoint is kept for one release while the - FE migrates, then removed. + **Deprecated:** use `GET /api/v2/nodes/{node_type}` instead. This v1 variant + inlines the full TTS model catalog under `options.models_by_provider`, which + creates a large response and couples clients to the catalog structure. The v2 + endpoint replaces that inline tree with a `providers` HATEOAS href pointing to + the standalone `/api/v1/providers` catalog, so the model list is fetched lazily + only when needed. - Unlike `GET /nodes` (which returns only port schemas), this endpoint returns the - actual runtime values a user picks: available target languages (from settings), - the TTS model catalog grouped by provider, and a HATEOAS link to the workspace- - scoped voices list. Requires `X-Workspace-Id` for a uniform FE contract. + Unlike `GET /nodes` (which returns only static port schemas), this endpoint + returns the runtime values a caller uses to configure a node: supported target + languages derived from deployment settings, the available model catalog, and a + HATEOAS link to the workspace-scoped voice list. Requires `X-Workspace-Id`. Parameters ---------- @@ -109,14 +121,18 @@ def get_node_detail_v2( request_options: typing.Optional[RequestOptions] = None, ) -> ApiResponseNodeDetailOut: """ - Return full node definition + runtime options (v2 — HATEOAS catalog href). + Return full node definition and runtime configuration options for a node type (v2). + + Extends `GET /api/v1/nodes/{node_type}` by replacing the large inline model + catalog (`options.models_by_provider`) with a `providers` HATEOAS href pointing + to `GET /api/v1/providers`. Clients follow that link to load the model list and + each model's configuration schema only when the user opens the relevant + configuration panel, rather than receiving it in every node-detail response. - POD-612: replaces the deprecated v1 ``options.models_by_provider`` inline tree - with a ``providers`` HATEOAS href to the standalone catalog - ``/api/v1/providers``. The FE follows that href to fetch each model's - ``config_schema`` lazily. The ``voices`` href (with its provider/model/language - filter enums) is unchanged, so the voice picker never needs the catalog call. - Requires ``X-Workspace-Id`` for a uniform FE contract. + The `voices` HATEOAS href (with its provider, model, and language filter + parameters) is unchanged from v1, so the voice picker does not require a + catalog call. Supported target languages are resolved from deployment settings + at request time. Requires `X-Workspace-Id` and the `catalog:read` scope. Parameters ---------- @@ -168,7 +184,18 @@ async def list_nodes( self, *, request_options: typing.Optional[RequestOptions] = None ) -> ApiListResponseNodePortsOut: """ - List all node types and their input/output port definitions. + List all available node types with their input/output port schemas. + + Returns the static structural definition for every node type registered in + the catalog — what ports each node exposes, their names, and expected data + shapes — without runtime-variable values such as available languages or the + TTS model catalog. This endpoint requires no `X-Workspace-Id` header and no + authentication, making it suitable for static documentation generation and + canvas layout tooling. + + For the full runtime configuration options a user would pick when wiring up + a specific node (available target languages, provider/model options, voice + picker URL), use `GET /api/v2/nodes/{node_type}` instead. Parameters ---------- @@ -208,18 +235,19 @@ async def get_node_detail( request_options: typing.Optional[RequestOptions] = None, ) -> ApiResponseNodeDetailOut: """ - Return full node definition + runtime options for the canvas node-config UI. + Return full node definition and runtime configuration options for a node type. - **Deprecated (POD-612):** this version inlines the model catalog as - `options.models_by_provider`. Use `GET /api/v2/nodes/{node_type}`, which - replaces the inline tree with a `providers` HATEOAS href to the standalone - catalog `/api/v1/providers`. This endpoint is kept for one release while the - FE migrates, then removed. + **Deprecated:** use `GET /api/v2/nodes/{node_type}` instead. This v1 variant + inlines the full TTS model catalog under `options.models_by_provider`, which + creates a large response and couples clients to the catalog structure. The v2 + endpoint replaces that inline tree with a `providers` HATEOAS href pointing to + the standalone `/api/v1/providers` catalog, so the model list is fetched lazily + only when needed. - Unlike `GET /nodes` (which returns only port schemas), this endpoint returns the - actual runtime values a user picks: available target languages (from settings), - the TTS model catalog grouped by provider, and a HATEOAS link to the workspace- - scoped voices list. Requires `X-Workspace-Id` for a uniform FE contract. + Unlike `GET /nodes` (which returns only static port schemas), this endpoint + returns the runtime values a caller uses to configure a node: supported target + languages derived from deployment settings, the available model catalog, and a + HATEOAS link to the workspace-scoped voice list. Requires `X-Workspace-Id`. Parameters ---------- @@ -267,14 +295,18 @@ async def get_node_detail_v2( request_options: typing.Optional[RequestOptions] = None, ) -> ApiResponseNodeDetailOut: """ - Return full node definition + runtime options (v2 — HATEOAS catalog href). - - POD-612: replaces the deprecated v1 ``options.models_by_provider`` inline tree - with a ``providers`` HATEOAS href to the standalone catalog - ``/api/v1/providers``. The FE follows that href to fetch each model's - ``config_schema`` lazily. The ``voices`` href (with its provider/model/language - filter enums) is unchanged, so the voice picker never needs the catalog call. - Requires ``X-Workspace-Id`` for a uniform FE contract. + Return full node definition and runtime configuration options for a node type (v2). + + Extends `GET /api/v1/nodes/{node_type}` by replacing the large inline model + catalog (`options.models_by_provider`) with a `providers` HATEOAS href pointing + to `GET /api/v1/providers`. Clients follow that link to load the model list and + each model's configuration schema only when the user opens the relevant + configuration panel, rather than receiving it in every node-detail response. + + The `voices` HATEOAS href (with its provider, model, and language filter + parameters) is unchanged from v1, so the voice picker does not require a + catalog call. Supported target languages are resolved from deployment settings + at request time. Requires `X-Workspace-Id` and the `catalog:read` scope. Parameters ---------- diff --git a/src/onepin/nodes/raw_client.py b/src/onepin/nodes/raw_client.py index a140cc7..55565fc 100644 --- a/src/onepin/nodes/raw_client.py +++ b/src/onepin/nodes/raw_client.py @@ -25,7 +25,18 @@ def list_nodes( self, *, request_options: typing.Optional[RequestOptions] = None ) -> HttpResponse[ApiListResponseNodePortsOut]: """ - List all node types and their input/output port definitions. + List all available node types with their input/output port schemas. + + Returns the static structural definition for every node type registered in + the catalog — what ports each node exposes, their names, and expected data + shapes — without runtime-variable values such as available languages or the + TTS model catalog. This endpoint requires no `X-Workspace-Id` header and no + authentication, making it suitable for static documentation generation and + canvas layout tooling. + + For the full runtime configuration options a user would pick when wiring up + a specific node (available target languages, provider/model options, voice + picker URL), use `GET /api/v2/nodes/{node_type}` instead. Parameters ---------- @@ -69,18 +80,19 @@ def get_node_detail( request_options: typing.Optional[RequestOptions] = None, ) -> HttpResponse[ApiResponseNodeDetailOut]: """ - Return full node definition + runtime options for the canvas node-config UI. + Return full node definition and runtime configuration options for a node type. - **Deprecated (POD-612):** this version inlines the model catalog as - `options.models_by_provider`. Use `GET /api/v2/nodes/{node_type}`, which - replaces the inline tree with a `providers` HATEOAS href to the standalone - catalog `/api/v1/providers`. This endpoint is kept for one release while the - FE migrates, then removed. + **Deprecated:** use `GET /api/v2/nodes/{node_type}` instead. This v1 variant + inlines the full TTS model catalog under `options.models_by_provider`, which + creates a large response and couples clients to the catalog structure. The v2 + endpoint replaces that inline tree with a `providers` HATEOAS href pointing to + the standalone `/api/v1/providers` catalog, so the model list is fetched lazily + only when needed. - Unlike `GET /nodes` (which returns only port schemas), this endpoint returns the - actual runtime values a user picks: available target languages (from settings), - the TTS model catalog grouped by provider, and a HATEOAS link to the workspace- - scoped voices list. Requires `X-Workspace-Id` for a uniform FE contract. + Unlike `GET /nodes` (which returns only static port schemas), this endpoint + returns the runtime values a caller uses to configure a node: supported target + languages derived from deployment settings, the available model catalog, and a + HATEOAS link to the workspace-scoped voice list. Requires `X-Workspace-Id`. Parameters ---------- @@ -153,14 +165,18 @@ def get_node_detail_v2( request_options: typing.Optional[RequestOptions] = None, ) -> HttpResponse[ApiResponseNodeDetailOut]: """ - Return full node definition + runtime options (v2 — HATEOAS catalog href). + Return full node definition and runtime configuration options for a node type (v2). + + Extends `GET /api/v1/nodes/{node_type}` by replacing the large inline model + catalog (`options.models_by_provider`) with a `providers` HATEOAS href pointing + to `GET /api/v1/providers`. Clients follow that link to load the model list and + each model's configuration schema only when the user opens the relevant + configuration panel, rather than receiving it in every node-detail response. - POD-612: replaces the deprecated v1 ``options.models_by_provider`` inline tree - with a ``providers`` HATEOAS href to the standalone catalog - ``/api/v1/providers``. The FE follows that href to fetch each model's - ``config_schema`` lazily. The ``voices`` href (with its provider/model/language - filter enums) is unchanged, so the voice picker never needs the catalog call. - Requires ``X-Workspace-Id`` for a uniform FE contract. + The `voices` HATEOAS href (with its provider, model, and language filter + parameters) is unchanged from v1, so the voice picker does not require a + catalog call. Supported target languages are resolved from deployment settings + at request time. Requires `X-Workspace-Id` and the `catalog:read` scope. Parameters ---------- @@ -234,7 +250,18 @@ async def list_nodes( self, *, request_options: typing.Optional[RequestOptions] = None ) -> AsyncHttpResponse[ApiListResponseNodePortsOut]: """ - List all node types and their input/output port definitions. + List all available node types with their input/output port schemas. + + Returns the static structural definition for every node type registered in + the catalog — what ports each node exposes, their names, and expected data + shapes — without runtime-variable values such as available languages or the + TTS model catalog. This endpoint requires no `X-Workspace-Id` header and no + authentication, making it suitable for static documentation generation and + canvas layout tooling. + + For the full runtime configuration options a user would pick when wiring up + a specific node (available target languages, provider/model options, voice + picker URL), use `GET /api/v2/nodes/{node_type}` instead. Parameters ---------- @@ -278,18 +305,19 @@ async def get_node_detail( request_options: typing.Optional[RequestOptions] = None, ) -> AsyncHttpResponse[ApiResponseNodeDetailOut]: """ - Return full node definition + runtime options for the canvas node-config UI. + Return full node definition and runtime configuration options for a node type. - **Deprecated (POD-612):** this version inlines the model catalog as - `options.models_by_provider`. Use `GET /api/v2/nodes/{node_type}`, which - replaces the inline tree with a `providers` HATEOAS href to the standalone - catalog `/api/v1/providers`. This endpoint is kept for one release while the - FE migrates, then removed. + **Deprecated:** use `GET /api/v2/nodes/{node_type}` instead. This v1 variant + inlines the full TTS model catalog under `options.models_by_provider`, which + creates a large response and couples clients to the catalog structure. The v2 + endpoint replaces that inline tree with a `providers` HATEOAS href pointing to + the standalone `/api/v1/providers` catalog, so the model list is fetched lazily + only when needed. - Unlike `GET /nodes` (which returns only port schemas), this endpoint returns the - actual runtime values a user picks: available target languages (from settings), - the TTS model catalog grouped by provider, and a HATEOAS link to the workspace- - scoped voices list. Requires `X-Workspace-Id` for a uniform FE contract. + Unlike `GET /nodes` (which returns only static port schemas), this endpoint + returns the runtime values a caller uses to configure a node: supported target + languages derived from deployment settings, the available model catalog, and a + HATEOAS link to the workspace-scoped voice list. Requires `X-Workspace-Id`. Parameters ---------- @@ -362,14 +390,18 @@ async def get_node_detail_v2( request_options: typing.Optional[RequestOptions] = None, ) -> AsyncHttpResponse[ApiResponseNodeDetailOut]: """ - Return full node definition + runtime options (v2 — HATEOAS catalog href). - - POD-612: replaces the deprecated v1 ``options.models_by_provider`` inline tree - with a ``providers`` HATEOAS href to the standalone catalog - ``/api/v1/providers``. The FE follows that href to fetch each model's - ``config_schema`` lazily. The ``voices`` href (with its provider/model/language - filter enums) is unchanged, so the voice picker never needs the catalog call. - Requires ``X-Workspace-Id`` for a uniform FE contract. + Return full node definition and runtime configuration options for a node type (v2). + + Extends `GET /api/v1/nodes/{node_type}` by replacing the large inline model + catalog (`options.models_by_provider`) with a `providers` HATEOAS href pointing + to `GET /api/v1/providers`. Clients follow that link to load the model list and + each model's configuration schema only when the user opens the relevant + configuration panel, rather than receiving it in every node-detail response. + + The `voices` HATEOAS href (with its provider, model, and language filter + parameters) is unchanged from v1, so the voice picker does not require a + catalog call. Supported target languages are resolved from deployment settings + at request time. Requires `X-Workspace-Id` and the `catalog:read` scope. Parameters ---------- diff --git a/src/onepin/provider_keys/__init__.py b/src/onepin/provider_keys/__init__.py deleted file mode 100644 index 5cde020..0000000 --- a/src/onepin/provider_keys/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -# isort: skip_file - diff --git a/src/onepin/provider_keys/client.py b/src/onepin/provider_keys/client.py deleted file mode 100644 index ef5d362..0000000 --- a/src/onepin/provider_keys/client.py +++ /dev/null @@ -1,305 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper -from ..core.request_options import RequestOptions -from ..types.api_response_provider_key_item_out import ApiResponseProviderKeyItemOut -from ..types.api_response_provider_keys_manifest_out import ApiResponseProviderKeysManifestOut -from ..types.provider_key_provider import ProviderKeyProvider -from .raw_client import AsyncRawProviderKeysClient, RawProviderKeysClient - -# this is used as the default value for optional parameters -OMIT = typing.cast(typing.Any, ...) - - -class ProviderKeysClient: - def __init__(self, *, client_wrapper: SyncClientWrapper): - self._raw_client = RawProviderKeysClient(client_wrapper=client_wrapper) - - @property - def with_raw_response(self) -> RawProviderKeysClient: - """ - Retrieves a raw implementation of this client that returns raw responses. - - Returns - ------- - RawProviderKeysClient - """ - return self._raw_client - - def list_provider_keys( - self, *, workspace_id: typing.Optional[str] = None, request_options: typing.Optional[RequestOptions] = None - ) -> ApiResponseProviderKeysManifestOut: - """ - List BYOK provider-key schemas and status for the current workspace. - - Parameters - ---------- - workspace_id : typing.Optional[str] - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ApiResponseProviderKeysManifestOut - Successful Response - - Examples - -------- - from onepin import OnePinClient - - client = OnePinClient( - token="YOUR_TOKEN", - ) - client.provider_keys.list_provider_keys() - """ - _response = self._raw_client.list_provider_keys(workspace_id=workspace_id, request_options=request_options) - return _response.data - - def put_provider_key( - self, - provider: ProviderKeyProvider, - *, - request: typing.Dict[str, typing.Any], - workspace_id: typing.Optional[str] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> ApiResponseProviderKeyItemOut: - """ - Create or replace a BYOK provider key for the current workspace. - - POD-301: gated by `byok_enabled` feature flag on the workspace owner's plan. - Free plan rejects with 403 FEATURE_NOT_IN_PLAN. - - Parameters - ---------- - provider : ProviderKeyProvider - - request : typing.Dict[str, typing.Any] - - workspace_id : typing.Optional[str] - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ApiResponseProviderKeyItemOut - Successful Response - - Examples - -------- - from onepin import OnePinClient - - client = OnePinClient( - token="YOUR_TOKEN", - ) - client.provider_keys.put_provider_key( - provider="elevenlabs", - request={"key": "value"}, - ) - """ - _response = self._raw_client.put_provider_key( - provider, request=request, workspace_id=workspace_id, request_options=request_options - ) - return _response.data - - def delete_provider_key( - self, - provider: ProviderKeyProvider, - *, - workspace_id: typing.Optional[str] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> ApiResponseProviderKeyItemOut: - """ - Delete a BYOK provider key for the current workspace idempotently. - - Parameters - ---------- - provider : ProviderKeyProvider - - workspace_id : typing.Optional[str] - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ApiResponseProviderKeyItemOut - Successful Response - - Examples - -------- - from onepin import OnePinClient - - client = OnePinClient( - token="YOUR_TOKEN", - ) - client.provider_keys.delete_provider_key( - provider="elevenlabs", - ) - """ - _response = self._raw_client.delete_provider_key( - provider, workspace_id=workspace_id, request_options=request_options - ) - return _response.data - - -class AsyncProviderKeysClient: - def __init__(self, *, client_wrapper: AsyncClientWrapper): - self._raw_client = AsyncRawProviderKeysClient(client_wrapper=client_wrapper) - - @property - def with_raw_response(self) -> AsyncRawProviderKeysClient: - """ - Retrieves a raw implementation of this client that returns raw responses. - - Returns - ------- - AsyncRawProviderKeysClient - """ - return self._raw_client - - async def list_provider_keys( - self, *, workspace_id: typing.Optional[str] = None, request_options: typing.Optional[RequestOptions] = None - ) -> ApiResponseProviderKeysManifestOut: - """ - List BYOK provider-key schemas and status for the current workspace. - - Parameters - ---------- - workspace_id : typing.Optional[str] - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ApiResponseProviderKeysManifestOut - Successful Response - - Examples - -------- - import asyncio - - from onepin import AsyncOnePinClient - - client = AsyncOnePinClient( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.provider_keys.list_provider_keys() - - - asyncio.run(main()) - """ - _response = await self._raw_client.list_provider_keys( - workspace_id=workspace_id, request_options=request_options - ) - return _response.data - - async def put_provider_key( - self, - provider: ProviderKeyProvider, - *, - request: typing.Dict[str, typing.Any], - workspace_id: typing.Optional[str] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> ApiResponseProviderKeyItemOut: - """ - Create or replace a BYOK provider key for the current workspace. - - POD-301: gated by `byok_enabled` feature flag on the workspace owner's plan. - Free plan rejects with 403 FEATURE_NOT_IN_PLAN. - - Parameters - ---------- - provider : ProviderKeyProvider - - request : typing.Dict[str, typing.Any] - - workspace_id : typing.Optional[str] - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ApiResponseProviderKeyItemOut - Successful Response - - Examples - -------- - import asyncio - - from onepin import AsyncOnePinClient - - client = AsyncOnePinClient( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.provider_keys.put_provider_key( - provider="elevenlabs", - request={"key": "value"}, - ) - - - asyncio.run(main()) - """ - _response = await self._raw_client.put_provider_key( - provider, request=request, workspace_id=workspace_id, request_options=request_options - ) - return _response.data - - async def delete_provider_key( - self, - provider: ProviderKeyProvider, - *, - workspace_id: typing.Optional[str] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> ApiResponseProviderKeyItemOut: - """ - Delete a BYOK provider key for the current workspace idempotently. - - Parameters - ---------- - provider : ProviderKeyProvider - - workspace_id : typing.Optional[str] - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ApiResponseProviderKeyItemOut - Successful Response - - Examples - -------- - import asyncio - - from onepin import AsyncOnePinClient - - client = AsyncOnePinClient( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.provider_keys.delete_provider_key( - provider="elevenlabs", - ) - - - asyncio.run(main()) - """ - _response = await self._raw_client.delete_provider_key( - provider, workspace_id=workspace_id, request_options=request_options - ) - return _response.data diff --git a/src/onepin/provider_keys/raw_client.py b/src/onepin/provider_keys/raw_client.py deleted file mode 100644 index d9ae15d..0000000 --- a/src/onepin/provider_keys/raw_client.py +++ /dev/null @@ -1,408 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing -from json.decoder import JSONDecodeError - -from ..core.api_error import ApiError -from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper -from ..core.http_response import AsyncHttpResponse, HttpResponse -from ..core.jsonable_encoder import encode_path_param -from ..core.parse_error import ParsingError -from ..core.pydantic_utilities import parse_obj_as -from ..core.request_options import RequestOptions -from ..errors.unprocessable_entity_error import UnprocessableEntityError -from ..types.api_response_provider_key_item_out import ApiResponseProviderKeyItemOut -from ..types.api_response_provider_keys_manifest_out import ApiResponseProviderKeysManifestOut -from ..types.provider_key_provider import ProviderKeyProvider -from pydantic import ValidationError - -# this is used as the default value for optional parameters -OMIT = typing.cast(typing.Any, ...) - - -class RawProviderKeysClient: - def __init__(self, *, client_wrapper: SyncClientWrapper): - self._client_wrapper = client_wrapper - - def list_provider_keys( - self, *, workspace_id: typing.Optional[str] = None, request_options: typing.Optional[RequestOptions] = None - ) -> HttpResponse[ApiResponseProviderKeysManifestOut]: - """ - List BYOK provider-key schemas and status for the current workspace. - - Parameters - ---------- - workspace_id : typing.Optional[str] - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - HttpResponse[ApiResponseProviderKeysManifestOut] - Successful Response - """ - _response = self._client_wrapper.httpx_client.request( - "api/v1/provider-keys", - method="GET", - 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( - ApiResponseProviderKeysManifestOut, - parse_obj_as( - type_=ApiResponseProviderKeysManifestOut, # 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 put_provider_key( - self, - provider: ProviderKeyProvider, - *, - request: typing.Dict[str, typing.Any], - workspace_id: typing.Optional[str] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> HttpResponse[ApiResponseProviderKeyItemOut]: - """ - Create or replace a BYOK provider key for the current workspace. - - POD-301: gated by `byok_enabled` feature flag on the workspace owner's plan. - Free plan rejects with 403 FEATURE_NOT_IN_PLAN. - - Parameters - ---------- - provider : ProviderKeyProvider - - request : typing.Dict[str, typing.Any] - - workspace_id : typing.Optional[str] - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - HttpResponse[ApiResponseProviderKeyItemOut] - Successful Response - """ - _response = self._client_wrapper.httpx_client.request( - f"api/v1/provider-keys/{encode_path_param(provider)}", - method="PUT", - json=request, - 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: - _data = typing.cast( - ApiResponseProviderKeyItemOut, - parse_obj_as( - type_=ApiResponseProviderKeyItemOut, # 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 delete_provider_key( - self, - provider: ProviderKeyProvider, - *, - workspace_id: typing.Optional[str] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> HttpResponse[ApiResponseProviderKeyItemOut]: - """ - Delete a BYOK provider key for the current workspace idempotently. - - Parameters - ---------- - provider : ProviderKeyProvider - - workspace_id : typing.Optional[str] - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - HttpResponse[ApiResponseProviderKeyItemOut] - Successful Response - """ - _response = self._client_wrapper.httpx_client.request( - f"api/v1/provider-keys/{encode_path_param(provider)}", - method="DELETE", - 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( - ApiResponseProviderKeyItemOut, - parse_obj_as( - type_=ApiResponseProviderKeyItemOut, # 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) - - -class AsyncRawProviderKeysClient: - def __init__(self, *, client_wrapper: AsyncClientWrapper): - self._client_wrapper = client_wrapper - - async def list_provider_keys( - self, *, workspace_id: typing.Optional[str] = None, request_options: typing.Optional[RequestOptions] = None - ) -> AsyncHttpResponse[ApiResponseProviderKeysManifestOut]: - """ - List BYOK provider-key schemas and status for the current workspace. - - Parameters - ---------- - workspace_id : typing.Optional[str] - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AsyncHttpResponse[ApiResponseProviderKeysManifestOut] - Successful Response - """ - _response = await self._client_wrapper.httpx_client.request( - "api/v1/provider-keys", - method="GET", - 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( - ApiResponseProviderKeysManifestOut, - parse_obj_as( - type_=ApiResponseProviderKeysManifestOut, # 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 put_provider_key( - self, - provider: ProviderKeyProvider, - *, - request: typing.Dict[str, typing.Any], - workspace_id: typing.Optional[str] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> AsyncHttpResponse[ApiResponseProviderKeyItemOut]: - """ - Create or replace a BYOK provider key for the current workspace. - - POD-301: gated by `byok_enabled` feature flag on the workspace owner's plan. - Free plan rejects with 403 FEATURE_NOT_IN_PLAN. - - Parameters - ---------- - provider : ProviderKeyProvider - - request : typing.Dict[str, typing.Any] - - workspace_id : typing.Optional[str] - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AsyncHttpResponse[ApiResponseProviderKeyItemOut] - Successful Response - """ - _response = await self._client_wrapper.httpx_client.request( - f"api/v1/provider-keys/{encode_path_param(provider)}", - method="PUT", - json=request, - 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: - _data = typing.cast( - ApiResponseProviderKeyItemOut, - parse_obj_as( - type_=ApiResponseProviderKeyItemOut, # 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 delete_provider_key( - self, - provider: ProviderKeyProvider, - *, - workspace_id: typing.Optional[str] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> AsyncHttpResponse[ApiResponseProviderKeyItemOut]: - """ - Delete a BYOK provider key for the current workspace idempotently. - - Parameters - ---------- - provider : ProviderKeyProvider - - workspace_id : typing.Optional[str] - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AsyncHttpResponse[ApiResponseProviderKeyItemOut] - Successful Response - """ - _response = await self._client_wrapper.httpx_client.request( - f"api/v1/provider-keys/{encode_path_param(provider)}", - method="DELETE", - 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( - ApiResponseProviderKeyItemOut, - parse_obj_as( - type_=ApiResponseProviderKeyItemOut, # 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) diff --git a/src/onepin/providers/client.py b/src/onepin/providers/client.py index 1a1a7e2..6d1c547 100644 --- a/src/onepin/providers/client.py +++ b/src/onepin/providers/client.py @@ -31,12 +31,17 @@ def list_catalog_providers( self, *, workspace_id: typing.Optional[str] = None, request_options: typing.Optional[RequestOptions] = None ) -> ApiListResponseCatalogProviderOut: """ - List TTS providers in the public catalog. + List all available speech synthesis providers in the catalog. - Returns the processing (TTS) provider catalog with a per-provider model count - and a HATEOAS link to each provider's models. Lean / customer-safe — cost, - credentials, and base URLs are never exposed. Requires `X-Workspace-Id` (or a - workspace-bound API key with the `catalog:read` scope), matching `/voices`. + Returns the full set of processing 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. + + This endpoint is the starting point for building a provider/model/voice + selection flow. The typical traversal is: list providers → follow `models` + link → follow `voices` link for the chosen model. Requires `X-Workspace-Id` + and the `catalog:read` scope. Parameters ---------- @@ -70,7 +75,12 @@ def get_catalog_provider( request_options: typing.Optional[RequestOptions] = None, ) -> ApiResponseCatalogProviderOut: """ - Get a single TTS provider by canonical name (e.g. `cartesia`). + Get a single speech synthesis provider by its canonical identifier. + + Returns the same shape as an item in `GET /providers` — display name, model + count, and a HATEOAS `models` link — but scoped to a single provider. Returns + 404 if the provider identifier is not recognized. The canonical identifier is + the lowercase slug returned in the `provider` field of the list response. Parameters ---------- @@ -110,7 +120,15 @@ def list_catalog_provider_models( request_options: typing.Optional[RequestOptions] = None, ) -> ApiListResponseCatalogModelOut: """ - List a provider's TTS models, each with its `config_schema` and live `voice_count`. + List all models available for a given provider. + + Returns each model's display name, content type, live `voice_count` (the + number of platform voices catalogued under that model), and a `controls` map + describing the canonical provider-agnostic parameters supported by the model + (e.g. speed, stability). Also includes `config_schema` for back-compat — new + integrations should prefer `controls` as the authoritative parameter + description. Each item includes a HATEOAS `voices` link to the paginated + voice list for that model. Returns 404 if the provider is not recognized. Parameters ---------- @@ -153,11 +171,24 @@ def list_catalog_provider_model_voices( request_options: typing.Optional[RequestOptions] = None, ) -> ApiCountedListResponseCatalogVoiceOut: """ - List platform voices catalogued under an exact `(provider, model)`. + List platform voices available for a specific provider and model. + + Returns a paginated list of voices from the platform catalog (system + workspace) that declare support for the given `model`. A voice can appear + under multiple models when its `supported_models` list includes more than + one entry; voices with no supported models are excluded from all model + listings. - Lean voice shape with a presigned `preview_url`. Platform catalog voices only - (System workspace); model-less voices are excluded. Paginated via `offset` / - `limit`. The flat `/voices?provider=&model=` endpoint remains for the picker. + Each voice includes gender, age, accent, supported locales, and a short-lived + presigned `preview_url` for the audio sample — do not cache these URLs across + sessions. The response `pagination.total` field reflects the total match count + for the provider/model pair. + + For the workspace voice picker (which merges platform and workspace-scoped + voices and supports favorite/similarity filtering), use `GET /voices` with + `?provider=` and `?model=` query parameters instead. + + Returns 404 if the provider or model is not recognized. Parameters ---------- @@ -166,8 +197,10 @@ def list_catalog_provider_model_voices( model : str offset : typing.Optional[int] + Zero-based pagination offset. limit : typing.Optional[int] + Page size (max 100). workspace_id : typing.Optional[str] @@ -205,7 +238,12 @@ def get_catalog_provider_model( request_options: typing.Optional[RequestOptions] = None, ) -> ApiResponseCatalogModelOut: """ - Get a single TTS model — `config_schema` + live `voice_count`. + Get a single model for a given provider. + + Returns the same shape as an item in `GET /providers/{provider}/models`, + including `controls` (canonical parameter map), `config_schema` (for + back-compat), live `voice_count`, and a HATEOAS `voices` link. Returns 404 + if the provider or model identifier is not recognized. Parameters ---------- @@ -260,12 +298,17 @@ async def list_catalog_providers( self, *, workspace_id: typing.Optional[str] = None, request_options: typing.Optional[RequestOptions] = None ) -> ApiListResponseCatalogProviderOut: """ - List TTS providers in the public catalog. + List all available speech synthesis providers in the catalog. - Returns the processing (TTS) provider catalog with a per-provider model count - and a HATEOAS link to each provider's models. Lean / customer-safe — cost, - credentials, and base URLs are never exposed. Requires `X-Workspace-Id` (or a - workspace-bound API key with the `catalog:read` scope), matching `/voices`. + Returns the full set of processing 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. + + This endpoint is the starting point for building a provider/model/voice + selection flow. The typical traversal is: list providers → follow `models` + link → follow `voices` link for the chosen model. Requires `X-Workspace-Id` + and the `catalog:read` scope. Parameters ---------- @@ -309,7 +352,12 @@ async def get_catalog_provider( request_options: typing.Optional[RequestOptions] = None, ) -> ApiResponseCatalogProviderOut: """ - Get a single TTS provider by canonical name (e.g. `cartesia`). + Get a single speech synthesis provider by its canonical identifier. + + Returns the same shape as an item in `GET /providers` — display name, model + count, and a HATEOAS `models` link — but scoped to a single provider. Returns + 404 if the provider identifier is not recognized. The canonical identifier is + the lowercase slug returned in the `provider` field of the list response. Parameters ---------- @@ -357,7 +405,15 @@ async def list_catalog_provider_models( request_options: typing.Optional[RequestOptions] = None, ) -> ApiListResponseCatalogModelOut: """ - List a provider's TTS models, each with its `config_schema` and live `voice_count`. + List all models available for a given provider. + + Returns each model's display name, content type, live `voice_count` (the + number of platform voices catalogued under that model), and a `controls` map + describing the canonical provider-agnostic parameters supported by the model + (e.g. speed, stability). Also includes `config_schema` for back-compat — new + integrations should prefer `controls` as the authoritative parameter + description. Each item includes a HATEOAS `voices` link to the paginated + voice list for that model. Returns 404 if the provider is not recognized. Parameters ---------- @@ -408,11 +464,24 @@ async def list_catalog_provider_model_voices( request_options: typing.Optional[RequestOptions] = None, ) -> ApiCountedListResponseCatalogVoiceOut: """ - List platform voices catalogued under an exact `(provider, model)`. + List platform voices available for a specific provider and model. + + Returns a paginated list of voices from the platform catalog (system + workspace) that declare support for the given `model`. A voice can appear + under multiple models when its `supported_models` list includes more than + one entry; voices with no supported models are excluded from all model + listings. - Lean voice shape with a presigned `preview_url`. Platform catalog voices only - (System workspace); model-less voices are excluded. Paginated via `offset` / - `limit`. The flat `/voices?provider=&model=` endpoint remains for the picker. + Each voice includes gender, age, accent, supported locales, and a short-lived + presigned `preview_url` for the audio sample — do not cache these URLs across + sessions. The response `pagination.total` field reflects the total match count + for the provider/model pair. + + For the workspace voice picker (which merges platform and workspace-scoped + voices and supports favorite/similarity filtering), use `GET /voices` with + `?provider=` and `?model=` query parameters instead. + + Returns 404 if the provider or model is not recognized. Parameters ---------- @@ -421,8 +490,10 @@ async def list_catalog_provider_model_voices( model : str offset : typing.Optional[int] + Zero-based pagination offset. limit : typing.Optional[int] + Page size (max 100). workspace_id : typing.Optional[str] @@ -468,7 +539,12 @@ async def get_catalog_provider_model( request_options: typing.Optional[RequestOptions] = None, ) -> ApiResponseCatalogModelOut: """ - Get a single TTS model — `config_schema` + live `voice_count`. + Get a single model for a given provider. + + Returns the same shape as an item in `GET /providers/{provider}/models`, + including `controls` (canonical parameter map), `config_schema` (for + back-compat), live `voice_count`, and a HATEOAS `voices` link. Returns 404 + if the provider or model identifier is not recognized. Parameters ---------- diff --git a/src/onepin/providers/raw_client.py b/src/onepin/providers/raw_client.py index 933636a..14bcd24 100644 --- a/src/onepin/providers/raw_client.py +++ b/src/onepin/providers/raw_client.py @@ -28,12 +28,17 @@ def list_catalog_providers( self, *, workspace_id: typing.Optional[str] = None, request_options: typing.Optional[RequestOptions] = None ) -> HttpResponse[ApiListResponseCatalogProviderOut]: """ - List TTS providers in the public catalog. + List all available speech synthesis providers in the catalog. - Returns the processing (TTS) provider catalog with a per-provider model count - and a HATEOAS link to each provider's models. Lean / customer-safe — cost, - credentials, and base URLs are never exposed. Requires `X-Workspace-Id` (or a - workspace-bound API key with the `catalog:read` scope), matching `/voices`. + Returns the full set of processing 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. + + This endpoint is the starting point for building a provider/model/voice + selection flow. The typical traversal is: list providers → follow `models` + link → follow `voices` link for the chosen model. Requires `X-Workspace-Id` + and the `catalog:read` scope. Parameters ---------- @@ -93,7 +98,12 @@ def get_catalog_provider( request_options: typing.Optional[RequestOptions] = None, ) -> HttpResponse[ApiResponseCatalogProviderOut]: """ - Get a single TTS provider by canonical name (e.g. `cartesia`). + Get a single speech synthesis provider by its canonical identifier. + + Returns the same shape as an item in `GET /providers` — display name, model + count, and a HATEOAS `models` link — but scoped to a single provider. Returns + 404 if the provider identifier is not recognized. The canonical identifier is + the lowercase slug returned in the `provider` field of the list response. Parameters ---------- @@ -166,7 +176,15 @@ def list_catalog_provider_models( request_options: typing.Optional[RequestOptions] = None, ) -> HttpResponse[ApiListResponseCatalogModelOut]: """ - List a provider's TTS models, each with its `config_schema` and live `voice_count`. + List all models available for a given provider. + + Returns each model's display name, content type, live `voice_count` (the + number of platform voices catalogued under that model), and a `controls` map + describing the canonical provider-agnostic parameters supported by the model + (e.g. speed, stability). Also includes `config_schema` for back-compat — new + integrations should prefer `controls` as the authoritative parameter + description. Each item includes a HATEOAS `voices` link to the paginated + voice list for that model. Returns 404 if the provider is not recognized. Parameters ---------- @@ -242,11 +260,24 @@ def list_catalog_provider_model_voices( request_options: typing.Optional[RequestOptions] = None, ) -> HttpResponse[ApiCountedListResponseCatalogVoiceOut]: """ - List platform voices catalogued under an exact `(provider, model)`. + List platform voices available for a specific provider and model. + + Returns a paginated list of voices from the platform catalog (system + workspace) that declare support for the given `model`. A voice can appear + under multiple models when its `supported_models` list includes more than + one entry; voices with no supported models are excluded from all model + listings. - Lean voice shape with a presigned `preview_url`. Platform catalog voices only - (System workspace); model-less voices are excluded. Paginated via `offset` / - `limit`. The flat `/voices?provider=&model=` endpoint remains for the picker. + Each voice includes gender, age, accent, supported locales, and a short-lived + presigned `preview_url` for the audio sample — do not cache these URLs across + sessions. The response `pagination.total` field reflects the total match count + for the provider/model pair. + + For the workspace voice picker (which merges platform and workspace-scoped + voices and supports favorite/similarity filtering), use `GET /voices` with + `?provider=` and `?model=` query parameters instead. + + Returns 404 if the provider or model is not recognized. Parameters ---------- @@ -255,8 +286,10 @@ def list_catalog_provider_model_voices( model : str offset : typing.Optional[int] + Zero-based pagination offset. limit : typing.Optional[int] + Page size (max 100). workspace_id : typing.Optional[str] @@ -330,7 +363,12 @@ def get_catalog_provider_model( request_options: typing.Optional[RequestOptions] = None, ) -> HttpResponse[ApiResponseCatalogModelOut]: """ - Get a single TTS model — `config_schema` + live `voice_count`. + Get a single model for a given provider. + + Returns the same shape as an item in `GET /providers/{provider}/models`, + including `controls` (canonical parameter map), `config_schema` (for + back-compat), live `voice_count`, and a HATEOAS `voices` link. Returns 404 + if the provider or model identifier is not recognized. Parameters ---------- @@ -406,12 +444,17 @@ async def list_catalog_providers( self, *, workspace_id: typing.Optional[str] = None, request_options: typing.Optional[RequestOptions] = None ) -> AsyncHttpResponse[ApiListResponseCatalogProviderOut]: """ - List TTS providers in the public catalog. + List all available speech synthesis providers in the catalog. - Returns the processing (TTS) provider catalog with a per-provider model count - and a HATEOAS link to each provider's models. Lean / customer-safe — cost, - credentials, and base URLs are never exposed. Requires `X-Workspace-Id` (or a - workspace-bound API key with the `catalog:read` scope), matching `/voices`. + Returns the full set of processing 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. + + This endpoint is the starting point for building a provider/model/voice + selection flow. The typical traversal is: list providers → follow `models` + link → follow `voices` link for the chosen model. Requires `X-Workspace-Id` + and the `catalog:read` scope. Parameters ---------- @@ -471,7 +514,12 @@ async def get_catalog_provider( request_options: typing.Optional[RequestOptions] = None, ) -> AsyncHttpResponse[ApiResponseCatalogProviderOut]: """ - Get a single TTS provider by canonical name (e.g. `cartesia`). + Get a single speech synthesis provider by its canonical identifier. + + Returns the same shape as an item in `GET /providers` — display name, model + count, and a HATEOAS `models` link — but scoped to a single provider. Returns + 404 if the provider identifier is not recognized. The canonical identifier is + the lowercase slug returned in the `provider` field of the list response. Parameters ---------- @@ -544,7 +592,15 @@ async def list_catalog_provider_models( request_options: typing.Optional[RequestOptions] = None, ) -> AsyncHttpResponse[ApiListResponseCatalogModelOut]: """ - List a provider's TTS models, each with its `config_schema` and live `voice_count`. + List all models available for a given provider. + + Returns each model's display name, content type, live `voice_count` (the + number of platform voices catalogued under that model), and a `controls` map + describing the canonical provider-agnostic parameters supported by the model + (e.g. speed, stability). Also includes `config_schema` for back-compat — new + integrations should prefer `controls` as the authoritative parameter + description. Each item includes a HATEOAS `voices` link to the paginated + voice list for that model. Returns 404 if the provider is not recognized. Parameters ---------- @@ -620,11 +676,24 @@ async def list_catalog_provider_model_voices( request_options: typing.Optional[RequestOptions] = None, ) -> AsyncHttpResponse[ApiCountedListResponseCatalogVoiceOut]: """ - List platform voices catalogued under an exact `(provider, model)`. + List platform voices available for a specific provider and model. + + Returns a paginated list of voices from the platform catalog (system + workspace) that declare support for the given `model`. A voice can appear + under multiple models when its `supported_models` list includes more than + one entry; voices with no supported models are excluded from all model + listings. - Lean voice shape with a presigned `preview_url`. Platform catalog voices only - (System workspace); model-less voices are excluded. Paginated via `offset` / - `limit`. The flat `/voices?provider=&model=` endpoint remains for the picker. + Each voice includes gender, age, accent, supported locales, and a short-lived + presigned `preview_url` for the audio sample — do not cache these URLs across + sessions. The response `pagination.total` field reflects the total match count + for the provider/model pair. + + For the workspace voice picker (which merges platform and workspace-scoped + voices and supports favorite/similarity filtering), use `GET /voices` with + `?provider=` and `?model=` query parameters instead. + + Returns 404 if the provider or model is not recognized. Parameters ---------- @@ -633,8 +702,10 @@ async def list_catalog_provider_model_voices( model : str offset : typing.Optional[int] + Zero-based pagination offset. limit : typing.Optional[int] + Page size (max 100). workspace_id : typing.Optional[str] @@ -708,7 +779,12 @@ async def get_catalog_provider_model( request_options: typing.Optional[RequestOptions] = None, ) -> AsyncHttpResponse[ApiResponseCatalogModelOut]: """ - Get a single TTS model — `config_schema` + live `voice_count`. + Get a single model for a given provider. + + Returns the same shape as an item in `GET /providers/{provider}/models`, + including `controls` (canonical parameter map), `config_schema` (for + back-compat), live `voice_count`, and a HATEOAS `voices` link. Returns 404 + if the provider or model identifier is not recognized. Parameters ---------- diff --git a/src/onepin/reference.md b/src/onepin/reference.md index ac55156..86832b8 100644 --- a/src/onepin/reference.md +++ b/src/onepin/reference.md @@ -1,6 +1,6 @@ # Reference -## health -
client.health.liveness() -> typing.Any +## auth +
client.auth.whoami() -> ApiResponseAuthWhoamiOut
@@ -12,7 +12,17 @@
-Liveness probe — always returns 200. +Return the resolved authentication context for the current credential. + +Useful for verifying that a Bearer JWT or API key is valid and discovering +which workspace and permission scopes it grants — call this first when +debugging authentication issues or bootstrapping an SDK integration. + +The `auth_kind` field indicates whether the credential is a session token +(`clerk`) or a programmatic key (`api_key`). For API keys, `workspace_id` +and `api_key_id` are always populated; for session tokens, `workspace_id` +reflects the `X-Workspace-Id` header value (if present) and `api_key_id` +is `null`. The `scopes` list is sorted and deduplicated.
@@ -35,7 +45,7 @@ client = OnePinClient( environment=OnePinClientEnvironment.PROD, ) -client.health.liveness() +client.auth.whoami() ``` @@ -63,7 +73,8 @@ client.health.liveness()
-
client.health.readiness() -> typing.Any +## dictionary +
client.dictionary.list_dictionary_entries(...) -> ApiListResponseDictionaryOut
@@ -75,7 +86,16 @@ client.health.liveness()
-Readiness probe — checks DB and Redis connectivity. +List dictionary entries for a single locale in the current workspace. + +Returns a paginated list of entries for the BCP-47 `language` locale +specified via the `?language=` query parameter (required, e.g. `ko-kr`). +Use `GET /dictionary/search` instead when you need to match by word text +across multiple locales, or `GET /dictionary/languages` to discover which +locales have entries before filtering here. + +`audio_url` on entries with `method=recorded` is a short-lived presigned URL +— do not cache it across sessions.
@@ -98,7 +118,9 @@ client = OnePinClient( environment=OnePinClientEnvironment.PROD, ) -client.health.readiness() +client.dictionary.list_dictionary_entries( + language="de-de", +) ``` @@ -114,130 +136,59 @@ client.health.readiness()
-**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. +**language:** `ListDictionaryEntriesApiV1DictionaryGetRequestLanguage` — BCP-47 language code, e.g. en-us, ko-kr
- -
- - - - -
- -## webhooks -
client.webhooks.clerk_webhook() -> ApiResponseDict -
-
- -#### 📝 Description - -
-
-Handle Clerk webhook events. Verifies svix signature. -
-
+**method:** `typing.Optional[typing.List[DictionaryMethod]]` — Filter by one or more entry methods. Repeat to OR: `?method=spelled&method=recorded`. Omit to return all methods. +
-#### 🔌 Usage - -
-
-
-```python -from onepin import OnePinClient -from onepin.environment import OnePinClientEnvironment - -client = OnePinClient( - token="", - environment=OnePinClientEnvironment.PROD, -) - -client.webhooks.clerk_webhook() - -``` -
-
+**sort:** `typing.Optional[ListDictionaryEntriesApiV1DictionaryGetRequestSort]` — Field to sort by. `uses_count` ranks the most-applied entries first, useful for auditing high-impact corrections. +
-#### ⚙️ Parameters - -
-
-
-**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. +**order:** `typing.Optional[ListDictionaryEntriesApiV1DictionaryGetRequestOrder]` — Sort direction.
-
-
- - -
-
-
-
client.webhooks.stripe_webhook() -> ApiResponseDict
-#### 📝 Description - -
-
+**offset:** `typing.Optional[int]` — Zero-based pagination offset. + +
+
-Handle Stripe webhook events. Verifies Stripe-Signature header. -
-
+**limit:** `typing.Optional[int]` — Page size (max 50). +
-#### 🔌 Usage -
-
-
- -```python -from onepin import OnePinClient -from onepin.environment import OnePinClientEnvironment - -client = OnePinClient( - token="", - environment=OnePinClientEnvironment.PROD, -) - -client.webhooks.stripe_webhook() - -``` -
-
+**workspace_id:** `typing.Optional[str]` +
-#### ⚙️ Parameters - -
-
-
@@ -253,8 +204,7 @@ client.webhooks.stripe_webhook()
-## auth -
client.auth.whoami() -> ApiResponseAuthWhoamiOut +
client.dictionary.create_dictionary_entry(...) -> ApiResponseDictionaryOut
@@ -266,7 +216,27 @@ client.webhooks.stripe_webhook()
-Return the resolved Clerk or API-key authentication context. +Create a pronunciation dictionary entry in the current workspace. + +Dictionary entries teach the synthesis pipeline how to pronounce words that +it would otherwise handle incorrectly — brand names, acronyms, technical +terms, proper nouns, and foreign loanwords. Each entry is scoped to a single +BCP-47 locale and is applied during workflow execution when that locale is +the synthesis target. + +Three methods are supported via the `method` field: + +- `spelled` — provide a phonetic respelling in `pronunciation` (e.g. + `"Poh-doh-nohs"`). `pronunciation` is required for this method. +- `recorded` — attach a reference audio clip by supplying an `upload_id` + from a completed `/uploads` staging upload with category `dictionary`. + The audio is copied to permanent storage on create; the upload slot is + consumed and cannot be reused for a different entry. +- `ipa` — supply an IPA transcription in `ipa`. `pronunciation` is optional + as a human-readable gloss alongside the IPA. + +Returns 409 if a `(word, language)` pair already exists in the workspace. +Requires `editor` workspace role and the `dictionary:write` scope.
@@ -289,7 +259,11 @@ client = OnePinClient( environment=OnePinClientEnvironment.PROD, ) -client.auth.whoami() +client.dictionary.create_dictionary_entry( + word="word", + method="spelled", + language="language", +) ``` @@ -305,71 +279,39 @@ client.auth.whoami()
-**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. +**word:** `str` — The surface form of the word or phrase as it appears in a script.
- -
- - - - -
-## api-keys -
client.api_keys.list_api_keys(...) -> ApiCountedListResponseApiKeyOut
-#### 📝 Description - -
-
+**method:** `DictionaryMethod` — Pronunciation method: `spelled` (phonetic respelling), `recorded` (reference audio clip), or `ipa` (IPA transcription). + +
+
-List API keys for the current workspace without secret material. -
-
+**language:** `str` — BCP-47 locale this entry applies to (e.g. `ko-kr`). Case-insensitive; stored lowercase. +
-#### 🔌 Usage -
-
-
- -```python -from onepin import OnePinClient -from onepin.environment import OnePinClientEnvironment - -client = OnePinClient( - token="", - environment=OnePinClientEnvironment.PROD, -) - -client.api_keys.list_api_keys() - -``` -
-
+**workspace_id:** `typing.Optional[str]` +
-#### ⚙️ Parameters -
-
-
- -**offset:** `typing.Optional[int]` +**description:** `typing.Optional[str]` — Optional human-readable note about the entry (e.g. context, source).
@@ -377,7 +319,7 @@ client.api_keys.list_api_keys()
-**limit:** `typing.Optional[int]` +**pronunciation:** `typing.Optional[str]` — Phonetic respelling. Required when `method` is `spelled`.
@@ -385,7 +327,7 @@ client.api_keys.list_api_keys()
-**status:** `typing.Optional[ApiKeyListStatus]` — API-key list status filter. Defaults to currently usable keys. `revoked` returns unavailable keys (`active=false` or `revoked_at` is set); `all` returns all workspace API-key metadata rows. +**upload_id:** `typing.Optional[str]` — ID of a completed staging upload (category `dictionary`). Required when `method` is `recorded`; consumed on create.
@@ -393,7 +335,7 @@ client.api_keys.list_api_keys()
-**workspace_id:** `typing.Optional[str]` +**ipa:** `typing.Optional[str]` — IPA transcription of the word. Supplied by the caller; automatic generation is a planned enhancement.
@@ -413,7 +355,7 @@ client.api_keys.list_api_keys()
-
client.api_keys.create_api_key(...) -> ApiResponseApiKeyCreatedOut +
client.dictionary.search_dictionary_entries(...) -> ApiListResponseDictionaryOut
@@ -425,7 +367,15 @@ client.api_keys.list_api_keys()
-Create a live API key and return its plaintext value exactly once. +Search dictionary entries by word text across one or more locales. + +Performs a case-insensitive substring match on the `word` field. Optionally +narrow to one or more BCP-47 locales by repeating `?language=` (OR logic). +Omitting `language` searches across all locales in the workspace. + +Use `GET /dictionary` (locale-scoped list) when you want the full entry list +for a specific locale; use this endpoint when you need to find how a word +is defined across languages or when the user is typing a search query.
@@ -448,8 +398,8 @@ client = OnePinClient( environment=OnePinClientEnvironment.PROD, ) -client.api_keys.create_api_key( - name="name", +client.dictionary.search_dictionary_entries( + search="search", ) ``` @@ -466,7 +416,7 @@ client.api_keys.create_api_key(
-**name:** `str` +**search:** `str` — Substring to match against the `word` field (case-insensitive).
@@ -474,7 +424,23 @@ client.api_keys.create_api_key(
-**workspace_id:** `typing.Optional[str]` +**sort:** `typing.Optional[SearchDictionaryEntriesApiV1DictionarySearchGetRequestSort]` — Field to sort by. + +
+
+ +
+
+ +**order:** `typing.Optional[SearchDictionaryEntriesApiV1DictionarySearchGetRequestOrder]` — Sort direction. + +
+
+ +
+
+ +**offset:** `typing.Optional[int]` — Zero-based pagination offset.
@@ -482,7 +448,7 @@ client.api_keys.create_api_key(
-**scopes:** `typing.Optional[typing.List[ApiKeyScope]]` +**limit:** `typing.Optional[int]` — Page size (max 50).
@@ -490,7 +456,7 @@ client.api_keys.create_api_key(
-**rate_limit_per_min:** `typing.Optional[int]` +**language:** `typing.Optional[typing.List[SearchDictionaryEntriesApiV1DictionarySearchGetRequestLanguageItem]]` — Repeat for OR, e.g. ?language=en-us&language=ko-kr
@@ -498,7 +464,7 @@ client.api_keys.create_api_key(
-**key_type:** `typing.Optional[str]` — Phase 1 supports live bearer keys only; test/public are reserved. +**workspace_id:** `typing.Optional[str]`
@@ -518,7 +484,7 @@ client.api_keys.create_api_key(
-
client.api_keys.get_api_key(...) -> ApiResponseApiKeyOut +
client.dictionary.list_dictionary_languages(...) -> ApiResponseListDictionaryLanguageOut
@@ -530,7 +496,12 @@ client.api_keys.create_api_key(
-Get one API-key metadata record for the current workspace. +Return all locales that have at least one dictionary entry, with entry counts. + +Results are ordered by entry count descending, then BCP-47 locale code +ascending. Use this endpoint to populate a locale filter dropdown before +calling `GET /dictionary?language=`, rather than hard-coding the supported +locale list in your client.
@@ -553,9 +524,7 @@ client = OnePinClient( environment=OnePinClientEnvironment.PROD, ) -client.api_keys.get_api_key( - key_id="key_id", -) +client.dictionary.list_dictionary_languages() ``` @@ -571,14 +540,6 @@ client.api_keys.get_api_key(
-**key_id:** `str` - -
-
- -
-
- **workspace_id:** `typing.Optional[str]`
@@ -599,7 +560,7 @@ client.api_keys.get_api_key(
-
client.api_keys.delete_api_key(...) -> ApiResponseApiKeyOut +
client.dictionary.suggest_pronunciation(...) -> ApiResponsePronunciationSuggestion
@@ -611,7 +572,17 @@ client.api_keys.get_api_key(
-Soft-revoke an API key for the current workspace. +Generate a pronunciation suggestion for a word before saving it as a dictionary entry. + +Returns a `pronunciation` string suitable for use as the `pronunciation` field +when creating a `spelled`-method dictionary entry. The suggestion is +deterministic (same word always returns the same result) and is intended as a +starting point for human review, not as a production-ready transcription. + +`language` is accepted to maintain a consistent request shape for future +per-locale phonetic rules; it does not affect the current output. `ipa` is +always `null` in this version — automatic IPA generation is a planned +enhancement.
@@ -634,8 +605,9 @@ client = OnePinClient( environment=OnePinClientEnvironment.PROD, ) -client.api_keys.delete_api_key( - key_id="key_id", +client.dictionary.suggest_pronunciation( + word="word", + language="language", ) ``` @@ -652,7 +624,15 @@ client.api_keys.delete_api_key(
-**key_id:** `str` +**word:** `str` — The word or phrase to generate a pronunciation suggestion for. + +
+
+ +
+
+ +**language:** `str` — BCP-47 locale of the word. Reserved for future per-locale phonetic rules; does not affect current output.
@@ -680,7 +660,7 @@ client.api_keys.delete_api_key(
-
client.api_keys.update_api_key(...) -> ApiResponseApiKeyOut +
client.dictionary.update_dictionary_entry(...) -> ApiResponseDictionaryOut
@@ -692,7 +672,20 @@ client.api_keys.delete_api_key(
-Update API-key metadata, scopes, rate limit, or active state. +Update fields on an existing dictionary entry in the current workspace. + +Supports partial updates — only the fields included in the request body are +changed; omitted fields retain their current values. Passing `null` for +`word`, `method`, or `language` is rejected with 422, as these fields are +required on the stored entry. + +To replace the reference audio on a `recorded`-method entry, supply a new +`upload_id` pointing to a completed staging upload. The previous audio is +orphaned (not deleted from storage) and the new file is copied to permanent +storage atomically. + +Returns 409 if the new `(word, language)` combination already exists in the +workspace. Requires `editor` workspace role and the `dictionary:write` scope.
@@ -715,8 +708,8 @@ client = OnePinClient( environment=OnePinClientEnvironment.PROD, ) -client.api_keys.update_api_key( - key_id="key_id", +client.dictionary.update_dictionary_entry( + entry_id="entry_id", ) ``` @@ -733,7 +726,7 @@ client.api_keys.update_api_key(
-**key_id:** `str` +**entry_id:** `str`
@@ -749,7 +742,31 @@ client.api_keys.update_api_key(
-**name:** `typing.Optional[str]` +**word:** `typing.Optional[str]` — Updated surface form. Omit to leave unchanged. + +
+
+ +
+
+ +**description:** `typing.Optional[str]` — Updated human-readable note. Omit to leave unchanged. + +
+
+ +
+
+ +**pronunciation:** `typing.Optional[str]` — Updated phonetic respelling. Required when changing `method` to `spelled`. + +
+
+ +
+
+ +**upload_id:** `typing.Optional[str]` — New staging upload ID to replace the reference audio. Required when changing `method` to `recorded`.
@@ -757,7 +774,7 @@ client.api_keys.update_api_key(
-**scopes:** `typing.Optional[typing.List[ApiKeyScope]]` +**method:** `typing.Optional[DictionaryMethod]` — Updated pronunciation method. Omit to leave unchanged.
@@ -765,7 +782,7 @@ client.api_keys.update_api_key(
-**rate_limit_per_min:** `typing.Optional[int]` +**language:** `typing.Optional[str]` — Updated BCP-47 locale. Omit to leave unchanged.
@@ -773,7 +790,7 @@ client.api_keys.update_api_key(
-**active:** `typing.Optional[bool]` +**ipa:** `typing.Optional[str]` — Updated IPA transcription. Omit to leave unchanged; supply `null` explicitly to clear.
@@ -793,7 +810,7 @@ client.api_keys.update_api_key(
-
client.api_keys.rotate_api_key(...) -> ApiResponseApiKeyRotateOut +
client.dictionary.delete_dictionary_entry(...) -> ApiResponseDict
@@ -805,7 +822,13 @@ client.api_keys.update_api_key(
-Rotate an API key by revoking the old row and creating a new key. +Delete a dictionary entry from the current workspace. + +The entry is removed from the workspace's dictionary and will no longer +influence synthesis output in subsequent workflow runs. The operation is not +reversible via the API — create a new entry to restore the pronunciation. +Returns an empty `data` object on success. Requires `editor` workspace role +and the `dictionary:write` scope.
@@ -828,8 +851,8 @@ client = OnePinClient( environment=OnePinClientEnvironment.PROD, ) -client.api_keys.rotate_api_key( - key_id="key_id", +client.dictionary.delete_dictionary_entry( + entry_id="entry_id", ) ``` @@ -846,7 +869,7 @@ client.api_keys.rotate_api_key(
-**key_id:** `str` +**entry_id:** `str`
@@ -874,8 +897,8 @@ client.api_keys.rotate_api_key(
-## dictionary -
client.dictionary.list_dictionary_entries(...) -> ApiListResponseDictionaryOut +## nodes +
client.nodes.list_nodes() -> ApiListResponseNodePortsOut
@@ -887,7 +910,18 @@ client.api_keys.rotate_api_key(
-List dictionary entries for a single language in the current workspace. +List all available node types with their input/output port schemas. + +Returns the static structural definition for every node type registered in +the catalog — what ports each node exposes, their names, and expected data +shapes — without runtime-variable values such as available languages or the +TTS model catalog. This endpoint requires no `X-Workspace-Id` header and no +authentication, making it suitable for static documentation generation and +canvas layout tooling. + +For the full runtime configuration options a user would pick when wiring up +a specific node (available target languages, provider/model options, voice +picker URL), use `GET /api/v2/nodes/{node_type}` instead.
@@ -910,9 +944,7 @@ client = OnePinClient( environment=OnePinClientEnvironment.PROD, ) -client.dictionary.list_dictionary_entries( - language="de-de", -) +client.nodes.list_nodes() ``` @@ -928,2030 +960,10 @@ client.dictionary.list_dictionary_entries(
-**language:** `ListDictionaryEntriesApiV1DictionaryGetRequestLanguage` — BCP-47 language code, e.g. en-us, ko-kr +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. -
-
- -
-
- -**method:** `typing.Optional[typing.List[DictionaryMethod]]` — Repeat for OR, e.g. ?method=spelled&method=recorded - -
-
- -
-
- -**sort:** `typing.Optional[ListDictionaryEntriesApiV1DictionaryGetRequestSort]` - -
-
- -
-
- -**order:** `typing.Optional[ListDictionaryEntriesApiV1DictionaryGetRequestOrder]` - -
-
- -
-
- -**offset:** `typing.Optional[int]` - -
-
- -
-
- -**limit:** `typing.Optional[int]` - -
-
- -
-
- -**workspace_id:** `typing.Optional[str]` - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
- -
- - - - -
- -
client.dictionary.create_dictionary_entry(...) -> ApiResponseDictionaryOut -
-
- -#### 📝 Description - -
-
- -
-
- -Create a new dictionary entry in the current workspace. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from onepin import OnePinClient -from onepin.environment import OnePinClientEnvironment - -client = OnePinClient( - token="", - environment=OnePinClientEnvironment.PROD, -) - -client.dictionary.create_dictionary_entry( - word="word", - method="spelled", - language="language", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**word:** `str` - -
-
- -
-
- -**method:** `DictionaryMethod` - -
-
- -
-
- -**language:** `str` - -
-
- -
-
- -**workspace_id:** `typing.Optional[str]` - -
-
- -
-
- -**description:** `typing.Optional[str]` - -
-
- -
-
- -**pronunciation:** `typing.Optional[str]` - -
-
- -
-
- -**upload_id:** `typing.Optional[str]` - -
-
- -
-
- -**ipa:** `typing.Optional[str]` — User-provided IPA transcription. Persisted as-is. Auto-generation via phonemizer/LLM is a POD-256 follow-up. - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.dictionary.search_dictionary_entries(...) -> ApiListResponseDictionaryOut -
-
- -#### 📝 Description - -
-
- -
-
- -Cross-language search over dictionary entries. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from onepin import OnePinClient -from onepin.environment import OnePinClientEnvironment - -client = OnePinClient( - token="", - environment=OnePinClientEnvironment.PROD, -) - -client.dictionary.search_dictionary_entries( - search="search", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**search:** `str` - -
-
- -
-
- -**sort:** `typing.Optional[SearchDictionaryEntriesApiV1DictionarySearchGetRequestSort]` - -
-
- -
-
- -**order:** `typing.Optional[SearchDictionaryEntriesApiV1DictionarySearchGetRequestOrder]` - -
-
- -
-
- -**offset:** `typing.Optional[int]` - -
-
- -
-
- -**limit:** `typing.Optional[int]` - -
-
- -
-
- -**language:** `typing.Optional[typing.List[SearchDictionaryEntriesApiV1DictionarySearchGetRequestLanguageItem]]` — Repeat for OR, e.g. ?language=en-us&language=ko-kr - -
-
- -
-
- -**workspace_id:** `typing.Optional[str]` - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.dictionary.list_dictionary_languages(...) -> ApiResponseListDictionaryLanguageOut -
-
- -#### 📝 Description - -
-
- -
-
- -Return distinct languages with entry counts, ordered by count DESC, code ASC. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from onepin import OnePinClient -from onepin.environment import OnePinClientEnvironment - -client = OnePinClient( - token="", - environment=OnePinClientEnvironment.PROD, -) - -client.dictionary.list_dictionary_languages() - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**workspace_id:** `typing.Optional[str]` - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.dictionary.suggest_pronunciation(...) -> ApiResponsePronunciationSuggestion -
-
- -#### 📝 Description - -
-
- -
-
- -Return a deterministic FE-parity pronunciation fallback. - -``language`` is reserved for future per-locale rules; ``ipa`` is reserved -for a future generator and is always ``None`` in this version. - -Workspace scoping is enforced via ``get_current_workspace`` even though the -response is workspace-independent today: this keeps all ``/api/v1/`` -endpoints uniform (see CLAUDE.md §Workspace Scoping) and leaves room for -per-workspace dictionary overrides when the generator lands. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from onepin import OnePinClient -from onepin.environment import OnePinClientEnvironment - -client = OnePinClient( - token="", - environment=OnePinClientEnvironment.PROD, -) - -client.dictionary.suggest_pronunciation( - word="word", - language="language", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**word:** `str` - -
-
- -
-
- -**language:** `str` - -
-
- -
-
- -**workspace_id:** `typing.Optional[str]` - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.dictionary.update_dictionary_entry(...) -> ApiResponseDictionaryOut -
-
- -#### 📝 Description - -
-
- -
-
- -Update a dictionary entry scoped to the current workspace. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from onepin import OnePinClient -from onepin.environment import OnePinClientEnvironment - -client = OnePinClient( - token="", - environment=OnePinClientEnvironment.PROD, -) - -client.dictionary.update_dictionary_entry( - entry_id="entry_id", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**entry_id:** `str` - -
-
- -
-
- -**workspace_id:** `typing.Optional[str]` - -
-
- -
-
- -**word:** `typing.Optional[str]` - -
-
- -
-
- -**description:** `typing.Optional[str]` - -
-
- -
-
- -**pronunciation:** `typing.Optional[str]` - -
-
- -
-
- -**upload_id:** `typing.Optional[str]` - -
-
- -
-
- -**method:** `typing.Optional[DictionaryMethod]` - -
-
- -
-
- -**language:** `typing.Optional[str]` - -
-
- -
-
- -**ipa:** `typing.Optional[str]` — User-provided IPA transcription. Persisted as-is. Auto-generation via phonemizer/LLM is a POD-256 follow-up. - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.dictionary.delete_dictionary_entry(...) -> ApiResponseDict -
-
- -#### 📝 Description - -
-
- -
-
- -Soft-delete a dictionary entry scoped to the current workspace. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from onepin import OnePinClient -from onepin.environment import OnePinClientEnvironment - -client = OnePinClient( - token="", - environment=OnePinClientEnvironment.PROD, -) - -client.dictionary.delete_dictionary_entry( - entry_id="entry_id", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**entry_id:** `str` - -
-
- -
-
- -**workspace_id:** `typing.Optional[str]` - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -## nodes -
client.nodes.list_nodes() -> ApiListResponseNodePortsOut -
-
- -#### 📝 Description - -
-
- -
-
- -List all node types and their input/output port definitions. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from onepin import OnePinClient -from onepin.environment import OnePinClientEnvironment - -client = OnePinClient( - token="", - environment=OnePinClientEnvironment.PROD, -) - -client.nodes.list_nodes() - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.nodes.get_node_detail(...) -> ApiResponseNodeDetailOut -
-
- -#### 📝 Description - -
-
- -
-
- -Return full node definition + runtime options for the canvas node-config UI. - -**Deprecated (POD-612):** this version inlines the model catalog as -`options.models_by_provider`. Use `GET /api/v2/nodes/{node_type}`, which -replaces the inline tree with a `providers` HATEOAS href to the standalone -catalog `/api/v1/providers`. This endpoint is kept for one release while the -FE migrates, then removed. - -Unlike `GET /nodes` (which returns only port schemas), this endpoint returns the -actual runtime values a user picks: available target languages (from settings), -the TTS model catalog grouped by provider, and a HATEOAS link to the workspace- -scoped voices list. Requires `X-Workspace-Id` for a uniform FE contract. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from onepin import OnePinClient -from onepin.environment import OnePinClientEnvironment - -client = OnePinClient( - token="", - environment=OnePinClientEnvironment.PROD, -) - -client.nodes.get_node_detail( - node_type="node_type", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**node_type:** `str` - -
-
- -
-
- -**workspace_id:** `typing.Optional[str]` - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.nodes.get_node_detail_v2(...) -> ApiResponseNodeDetailOut -
-
- -#### 📝 Description - -
-
- -
-
- -Return full node definition + runtime options (v2 — HATEOAS catalog href). - -POD-612: replaces the deprecated v1 ``options.models_by_provider`` inline tree -with a ``providers`` HATEOAS href to the standalone catalog -``/api/v1/providers``. The FE follows that href to fetch each model's -``config_schema`` lazily. The ``voices`` href (with its provider/model/language -filter enums) is unchanged, so the voice picker never needs the catalog call. -Requires ``X-Workspace-Id`` for a uniform FE contract. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from onepin import OnePinClient -from onepin.environment import OnePinClientEnvironment - -client = OnePinClient( - token="", - environment=OnePinClientEnvironment.PROD, -) - -client.nodes.get_node_detail_v2( - node_type="node_type", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**node_type:** `str` - -
-
- -
-
- -**workspace_id:** `typing.Optional[str]` - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -## provider-keys -
client.provider_keys.list_provider_keys(...) -> ApiResponseProviderKeysManifestOut -
-
- -#### 📝 Description - -
-
- -
-
- -List BYOK provider-key schemas and status for the current workspace. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from onepin import OnePinClient -from onepin.environment import OnePinClientEnvironment - -client = OnePinClient( - token="", - environment=OnePinClientEnvironment.PROD, -) - -client.provider_keys.list_provider_keys() - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**workspace_id:** `typing.Optional[str]` - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.provider_keys.put_provider_key(...) -> ApiResponseProviderKeyItemOut -
-
- -#### 📝 Description - -
-
- -
-
- -Create or replace a BYOK provider key for the current workspace. - -POD-301: gated by `byok_enabled` feature flag on the workspace owner's plan. -Free plan rejects with 403 FEATURE_NOT_IN_PLAN. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from onepin import OnePinClient -from onepin.environment import OnePinClientEnvironment - -client = OnePinClient( - token="", - environment=OnePinClientEnvironment.PROD, -) - -client.provider_keys.put_provider_key( - provider="elevenlabs", - request={ - "key": "value" - }, -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**provider:** `ProviderKeyProvider` - -
-
- -
-
- -**request:** `typing.Dict[str, typing.Any]` — Provider-specific credential payload. The provider is the path parameter and must not be in body. Use GET /provider-keys data.providers[].credentials_schema for the matching provider as the canonical request schema. - -
-
- -
-
- -**workspace_id:** `typing.Optional[str]` - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.provider_keys.delete_provider_key(...) -> ApiResponseProviderKeyItemOut -
-
- -#### 📝 Description - -
-
- -
-
- -Delete a BYOK provider key for the current workspace idempotently. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from onepin import OnePinClient -from onepin.environment import OnePinClientEnvironment - -client = OnePinClient( - token="", - environment=OnePinClientEnvironment.PROD, -) - -client.provider_keys.delete_provider_key( - provider="elevenlabs", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**provider:** `ProviderKeyProvider` - -
-
- -
-
- -**workspace_id:** `typing.Optional[str]` - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -## providers -
client.providers.list_catalog_providers(...) -> ApiListResponseCatalogProviderOut -
-
- -#### 📝 Description - -
-
- -
-
- -List TTS providers in the public catalog. - -Returns the processing (TTS) provider catalog with a per-provider model count -and a HATEOAS link to each provider's models. Lean / customer-safe — cost, -credentials, and base URLs are never exposed. Requires `X-Workspace-Id` (or a -workspace-bound API key with the `catalog:read` scope), matching `/voices`. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from onepin import OnePinClient -from onepin.environment import OnePinClientEnvironment - -client = OnePinClient( - token="", - environment=OnePinClientEnvironment.PROD, -) - -client.providers.list_catalog_providers() - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**workspace_id:** `typing.Optional[str]` - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.providers.get_catalog_provider(...) -> ApiResponseCatalogProviderOut -
-
- -#### 📝 Description - -
-
- -
-
- -Get a single TTS provider by canonical name (e.g. `cartesia`). -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from onepin import OnePinClient -from onepin.environment import OnePinClientEnvironment - -client = OnePinClient( - token="", - environment=OnePinClientEnvironment.PROD, -) - -client.providers.get_catalog_provider( - provider="provider", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**provider:** `str` - -
-
- -
-
- -**workspace_id:** `typing.Optional[str]` - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.providers.list_catalog_provider_models(...) -> ApiListResponseCatalogModelOut -
-
- -#### 📝 Description - -
-
- -
-
- -List a provider's TTS models, each with its `config_schema` and live `voice_count`. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from onepin import OnePinClient -from onepin.environment import OnePinClientEnvironment - -client = OnePinClient( - token="", - environment=OnePinClientEnvironment.PROD, -) - -client.providers.list_catalog_provider_models( - provider="provider", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**provider:** `str` - -
-
- -
-
- -**workspace_id:** `typing.Optional[str]` - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.providers.list_catalog_provider_model_voices(...) -> ApiCountedListResponseCatalogVoiceOut -
-
- -#### 📝 Description - -
-
- -
-
- -List platform voices catalogued under an exact `(provider, model)`. - -Lean voice shape with a presigned `preview_url`. Platform catalog voices only -(System workspace); model-less voices are excluded. Paginated via `offset` / -`limit`. The flat `/voices?provider=&model=` endpoint remains for the picker. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from onepin import OnePinClient -from onepin.environment import OnePinClientEnvironment - -client = OnePinClient( - token="", - environment=OnePinClientEnvironment.PROD, -) - -client.providers.list_catalog_provider_model_voices( - provider="provider", - model="model", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**provider:** `str` - -
-
- -
-
- -**model:** `str` - -
-
- -
-
- -**offset:** `typing.Optional[int]` - -
-
- -
-
- -**limit:** `typing.Optional[int]` - -
-
- -
-
- -**workspace_id:** `typing.Optional[str]` - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.providers.get_catalog_provider_model(...) -> ApiResponseCatalogModelOut -
-
- -#### 📝 Description - -
-
- -
-
- -Get a single TTS model — `config_schema` + live `voice_count`. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from onepin import OnePinClient -from onepin.environment import OnePinClientEnvironment - -client = OnePinClient( - token="", - environment=OnePinClientEnvironment.PROD, -) - -client.providers.get_catalog_provider_model( - provider="provider", - model="model", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**provider:** `str` - -
-
- -
-
- -**model:** `str` - -
-
- -
-
- -**workspace_id:** `typing.Optional[str]` - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -## templates -
client.templates.list(...) -> ApiListResponseTemplateOut -
-
- -#### 📝 Description - -
-
- -
-
- -List live published templates across workspaces (gallery). - -Authenticated but not workspace-scoped — the gallery is cross-workspace -by design (published rows from any workspace). Does not require -`X-Workspace-Id`, so a freshly signed-up user without a workspace can -still browse templates. - -Dual-auth: Clerk JWT or `op_live_*` API key (scope `templates:read`). -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from onepin import OnePinClient -from onepin.environment import OnePinClientEnvironment - -client = OnePinClient( - token="", - environment=OnePinClientEnvironment.PROD, -) - -client.templates.list() - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**category:** `typing.Optional[typing.List[TemplateCategory]]` — Repeat for OR, e.g. ?category=media&category=creative - -
-
- -
-
- -**search:** `typing.Optional[str]` - -
-
- -
-
- -**sort:** `typing.Optional[ListTemplatesRequestSort]` - -
-
- -
-
- -**offset:** `typing.Optional[int]` - -
-
- -
-
- -**limit:** `typing.Optional[int]` - -
-
- -
-
- -**favorites_only:** `typing.Optional[bool]` - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.templates.create_template(...) -> ApiResponseTemplateOut -
-
- -#### 📝 Description - -
-
- -
-
- -Create a new workflow template in the current workspace. - -The caller supplies a full `WorkflowDefinition` (graph + execution). -Save-time validation (`validate_definition_save`) mirrors the -`/api/v1/workflows` contract — duplicate node/edge IDs, port mismatches, -and other structural errors fail at write time rather than surfacing -when a caller later clones the template into a workflow. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from onepin import OnePinClient -from onepin.environment import OnePinClientEnvironment - -client = OnePinClient( - token="", - environment=OnePinClientEnvironment.PROD, -) - -client.templates.create_template( - name="name", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**name:** `str` - -
-
- -
-
- -**workspace_id:** `typing.Optional[str]` - -
-
- -
-
- -**description:** `typing.Optional[str]` - -
-
- -
-
- -**category:** `typing.Optional[TemplateCategory]` - -
-
- -
-
- -**definition:** `typing.Optional[WorkflowDefinitionInput]` - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.templates.get(...) -> ApiResponseTemplateOut -
-
- -#### 📝 Description - -
-
- -
-
- -Fetch a template by id if visible (own, public, or starter). - -Dual-auth: Clerk JWT or `op_live_*` API key (scope `templates:read`). -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from onepin import OnePinClient -from onepin.environment import OnePinClientEnvironment - -client = OnePinClient( - token="", - environment=OnePinClientEnvironment.PROD, -) - -client.templates.get( - template_id="template_id", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**template_id:** `str` - -
-
- -
-
- -**workspace_id:** `typing.Optional[str]` - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.templates.delete_template(...) -> ApiResponseDict -
-
- -#### 📝 Description - -
-
- -
-
- -Soft-delete a template. Owner only; starter templates are read-only. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from onepin import OnePinClient -from onepin.environment import OnePinClientEnvironment - -client = OnePinClient( - token="", - environment=OnePinClientEnvironment.PROD, -) - -client.templates.delete_template( - template_id="template_id", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**template_id:** `str` - -
-
- -
-
- -**workspace_id:** `typing.Optional[str]` - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
+
+
@@ -2960,7 +972,7 @@ client.templates.delete_template(
-
client.templates.update_template(...) -> ApiResponseTemplateOut +
client.nodes.get_node_detail(...) -> ApiResponseNodeDetailOut
@@ -2972,123 +984,19 @@ client.templates.delete_template(
-Update a template. Owner only; starter templates are read-only. - -`definition` is full-replace (matches `WorkflowUpdate` — the FE sends the -entire graph back on save). Other fields are partial via `exclude_unset`. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from onepin import OnePinClient -from onepin.environment import OnePinClientEnvironment - -client = OnePinClient( - token="", - environment=OnePinClientEnvironment.PROD, -) - -client.templates.update_template( - template_id="template_id", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**template_id:** `str` - -
-
- -
-
- -**workspace_id:** `typing.Optional[str]` - -
-
- -
-
- -**name:** `typing.Optional[str]` - -
-
- -
-
- -**description:** `typing.Optional[str]` - -
-
- -
-
- -**category:** `typing.Optional[TemplateCategory]` - -
-
- -
-
- -**definition:** `typing.Optional[WorkflowDefinitionInput]` — Full-replace on PATCH. Omit to keep the stored value. Explicit `null` is rejected — there is no 'empty graph' use-case worth the ambiguity. The union with `null` here only makes omission easy for FE clients; see `reject_null_definition` for the runtime guard. - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - - - -
- -
client.templates.estimate_template(...) -> ApiResponseTemplateEstimateResponse -
-
- -#### 📝 Description - -
-
+Return full node definition and runtime configuration options for a node type. -
-
+**Deprecated:** use `GET /api/v2/nodes/{node_type}` instead. This v1 variant +inlines the full TTS model catalog under `options.models_by_provider`, which +creates a large response and couples clients to the catalog structure. The v2 +endpoint replaces that inline tree with a `providers` HATEOAS href pointing to +the standalone `/api/v1/providers` catalog, so the model list is fetched lazily +only when needed. -Return per-1,000-character pricing for a visible template snapshot. +Unlike `GET /nodes` (which returns only static port schemas), this endpoint +returns the runtime values a caller uses to configure a node: supported target +languages derived from deployment settings, the available model catalog, and a +HATEOAS link to the workspace-scoped voice list. Requires `X-Workspace-Id`.
@@ -3111,8 +1019,8 @@ client = OnePinClient( environment=OnePinClientEnvironment.PROD, ) -client.templates.estimate_template( - template_id="template_id", +client.nodes.get_node_detail( + node_type="node_type", ) ``` @@ -3129,7 +1037,7 @@ client.templates.estimate_template(
-**template_id:** `str` +**node_type:** `str`
@@ -3157,7 +1065,7 @@ client.templates.estimate_template(
-
client.templates.clone(...) -> ApiResponseWorkflowOut +
client.nodes.get_node_detail_v2(...) -> ApiResponseNodeDetailOut
@@ -3169,16 +1077,18 @@ client.templates.estimate_template(
-Clone a template into a workflow in the caller's workspace. - -Dual-auth: Clerk JWT or `op_live_*` API key (scope `workflows:write`). +Return full node definition and runtime configuration options for a node type (v2). -Same-workspace clones use the live `definition` (owner authoring). -Cross-workspace clones use the `published_definition` snapshot to avoid -leaking unpublished draft edits. +Extends `GET /api/v1/nodes/{node_type}` by replacing the large inline model +catalog (`options.models_by_provider`) with a `providers` HATEOAS href pointing +to `GET /api/v1/providers`. Clients follow that link to load the model list and +each model's configuration schema only when the user opens the relevant +configuration panel, rather than receiving it in every node-detail response. -Resolved name: explicit `body.name` (stripped) OR fallback to -`"{source_name} (Copy)"`. +The `voices` HATEOAS href (with its provider, model, and language filter +parameters) is unchanged from v1, so the voice picker does not require a +catalog call. Supported target languages are resolved from deployment settings +at request time. Requires `X-Workspace-Id` and the `catalog:read` scope.
@@ -3201,8 +1111,8 @@ client = OnePinClient( environment=OnePinClientEnvironment.PROD, ) -client.templates.clone( - template_id="template_id", +client.nodes.get_node_detail_v2( + node_type="node_type", ) ``` @@ -3219,15 +1129,7 @@ client.templates.clone(
-**template_id:** `str` - -
-
- -
-
- -**workspace_id:** `typing.Optional[str]` +**node_type:** `str`
@@ -3235,7 +1137,7 @@ client.templates.clone(
-**name:** `typing.Optional[str]` +**workspace_id:** `typing.Optional[str]`
@@ -3255,7 +1157,8 @@ client.templates.clone(
-
client.templates.favorite_template(...) -> ApiResponseTemplateOut +## providers +
client.providers.list_catalog_providers(...) -> ApiListResponseCatalogProviderOut
@@ -3267,15 +1170,17 @@ client.templates.clone(
-Mark a visible template as a favorite for the current user. +List all available speech synthesis providers in the catalog. -Authenticated but not workspace-scoped — favorites are cross-workspace by -design for public/starter templates and for the caller's own private -templates when they still belong to that template's workspace. +Returns the full set of processing 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. -Returns 404 when the template does not exist or is not visible to the -caller. This POST intentionally enumerates success/failure for the toggle -UX, while DELETE stays non-enumerating. +This endpoint is the starting point for building a provider/model/voice +selection flow. The typical traversal is: list providers → follow `models` +link → follow `voices` link for the chosen model. Requires `X-Workspace-Id` +and the `catalog:read` scope.
@@ -3298,9 +1203,7 @@ client = OnePinClient( environment=OnePinClientEnvironment.PROD, ) -client.templates.favorite_template( - template_id="template_id", -) +client.providers.list_catalog_providers() ``` @@ -3316,7 +1219,7 @@ client.templates.favorite_template(
-**template_id:** `str` +**workspace_id:** `typing.Optional[str]`
@@ -3336,7 +1239,7 @@ client.templates.favorite_template(
-
client.templates.unfavorite_template(...) -> ApiResponseDict +
client.providers.get_catalog_provider(...) -> ApiResponseCatalogProviderOut
@@ -3348,10 +1251,12 @@ client.templates.favorite_template(
-Remove a template favorite for the current user. +Get a single speech synthesis provider by its canonical identifier. -Deletion is intentionally non-enumerating: authenticated callers receive a -successful empty response whether the row or template exists. +Returns the same shape as an item in `GET /providers` — display name, model +count, and a HATEOAS `models` link — but scoped to a single provider. Returns +404 if the provider identifier is not recognized. The canonical identifier is +the lowercase slug returned in the `provider` field of the list response.
@@ -3374,8 +1279,8 @@ client = OnePinClient( environment=OnePinClientEnvironment.PROD, ) -client.templates.unfavorite_template( - template_id="template_id", +client.providers.get_catalog_provider( + provider="provider", ) ``` @@ -3392,7 +1297,15 @@ client.templates.unfavorite_template(
-**template_id:** `str` +**provider:** `str` + +
+
+ +
+
+ +**workspace_id:** `typing.Optional[str]`
@@ -3412,8 +1325,7 @@ client.templates.unfavorite_template(
-## voices -
client.voices.list(...) -> ApiCountedListResponseVoiceOut +
client.providers.list_catalog_provider_models(...) -> ApiListResponseCatalogModelOut
@@ -3425,25 +1337,15 @@ client.templates.unfavorite_template(
-List TTS voices available to the current workspace. - -Every filter accepts repeat-key OR semantics: -`?gender=female&gender=neutral&category=narration&source=platform&source=workspace`. -Filters combine across fields with AND; within a field, values OR. - -`language` uses Postgres `?|` (exists-any) against `voices.supported_languages`. -Platform voices with NULL `supported_languages` (catalog gaps) are treated -as general-use and match every locale filter. User-uploaded / cloned voices -with NULL stay excluded — NULL there means "language unknown" pending the -clone flow's language detection. +List all models available for a given provider. -Multi-sort: `sort` and `order` are parallel lists. `?sort=uses_count&sort=name&order=desc&order=asc` -orders primarily by uses_count DESC, secondarily by name ASC. When `order` -is shorter than `sort`, missing entries default per-field: -`name=asc, created_at=desc, uses_count=desc`. When `sort` is omitted, list -defaults to newest-first (or most-recently-favorited-first if -`favorites_only=true`). Every sort path appends `Voice.id ASC` as a -deterministic tiebreaker for pagination stability. +Returns each model's display name, content type, live `voice_count` (the +number of platform voices catalogued under that model), and a `controls` map +describing the canonical provider-agnostic parameters supported by the model +(e.g. speed, stability). Also includes `config_schema` for back-compat — new +integrations should prefer `controls` as the authoritative parameter +description. Each item includes a HATEOAS `voices` link to the paginated +voice list for that model. Returns 404 if the provider is not recognized.
@@ -3466,7 +1368,9 @@ client = OnePinClient( environment=OnePinClientEnvironment.PROD, ) -client.voices.list() +client.providers.list_catalog_provider_models( + provider="provider", +) ``` @@ -3482,7 +1386,7 @@ client.voices.list()
-**offset:** `typing.Optional[int]` +**provider:** `str`
@@ -3490,7 +1394,7 @@ client.voices.list()
-**limit:** `typing.Optional[int]` +**workspace_id:** `typing.Optional[str]`
@@ -3498,71 +1402,90 @@ client.voices.list()
-**favorites_only:** `typing.Optional[bool]` +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration.
+ +
-
-
-**source:** `typing.Optional[typing.List[ListVoicesRequestSourceItem]]` — Repeat for OR across scopes -
+
+
client.providers.list_catalog_provider_model_voices(...) -> ApiCountedListResponseCatalogVoiceOut
-**gender:** `typing.Optional[typing.List[VoiceGender]]` — Repeat for OR - -
-
+#### 📝 Description
-**age:** `typing.Optional[typing.List[VoiceAge]]` — Repeat for OR - -
-
-
-**category:** `typing.Optional[typing.List[VoiceCategory]]` — Repeat for OR - +List platform voices available for a specific provider and model. + +Returns a paginated list of voices from the platform catalog (system +workspace) that declare support for the given `model`. A voice can appear +under multiple models when its `supported_models` list includes more than +one entry; voices with no supported models are excluded from all model +listings. + +Each voice includes gender, age, accent, supported locales, and a short-lived +presigned `preview_url` for the audio sample — do not cache these URLs across +sessions. The response `pagination.total` field reflects the total match count +for the provider/model pair. + +For the workspace voice picker (which merges platform and workspace-scoped +voices and supports favorite/similarity filtering), use `GET /voices` with +`?provider=` and `?model=` query parameters instead. + +Returns 404 if the provider or model is not recognized. +
+
+#### 🔌 Usage +
-**accent:** `typing.Optional[typing.List[VoiceAccent]]` — Repeat for OR - -
-
-
-**search:** `typing.Optional[str]` - +```python +from onepin import OnePinClient +from onepin.environment import OnePinClientEnvironment + +client = OnePinClient( + token="", + environment=OnePinClientEnvironment.PROD, +) + +client.providers.list_catalog_provider_model_voices( + provider="provider", + model="model", +) + +```
+ + + +#### ⚙️ Parameters
-**sort:** `typing.Optional[typing.List[ListVoicesRequestSortItem]]` — Repeat for multi-sort. Pairs with `order` index-wise. - -
-
-
-**order:** `typing.Optional[typing.List[ListVoicesRequestOrderItem]]` — Parallel to sort[]; shorter is padded with per-field defaults. +**provider:** `str`
@@ -3570,7 +1493,7 @@ client.voices.list()
-**provider:** `typing.Optional[typing.List[ListVoicesRequestProviderItem]]` — Repeat for OR, e.g. ?provider=elevenlabs&provider=rime +**model:** `str`
@@ -3578,7 +1501,7 @@ client.voices.list()
-**model:** `typing.Optional[typing.List[str]]` — Repeat for OR. Filters platform voices by TTS model, e.g. ?model=arcana&model=sonic-2 +**offset:** `typing.Optional[int]` — Zero-based pagination offset.
@@ -3586,7 +1509,7 @@ client.voices.list()
-**language:** `typing.Optional[typing.List[ListVoicesRequestLanguageItem]]` — Repeat for OR, e.g. ?language=en-us&language=ko-kr +**limit:** `typing.Optional[int]` — Page size (max 100).
@@ -3614,7 +1537,7 @@ client.voices.list()
-
client.voices.get(...) -> ApiResponseVoiceOut +
client.providers.get_catalog_provider_model(...) -> ApiResponseCatalogModelOut
@@ -3626,7 +1549,12 @@ client.voices.list()
-Get a voice by ID, scoped to caller workspace + platform voices. +Get a single model for a given provider. + +Returns the same shape as an item in `GET /providers/{provider}/models`, +including `controls` (canonical parameter map), `config_schema` (for +back-compat), live `voice_count`, and a HATEOAS `voices` link. Returns 404 +if the provider or model identifier is not recognized.
@@ -3649,8 +1577,9 @@ client = OnePinClient( environment=OnePinClientEnvironment.PROD, ) -client.voices.get( - voice_id="voice_id", +client.providers.get_catalog_provider_model( + provider="provider", + model="model", ) ``` @@ -3667,7 +1596,15 @@ client.voices.get(
-**voice_id:** `str` +**provider:** `str` + +
+
+ +
+
+ +**model:** `str`
@@ -3695,7 +1632,8 @@ client.voices.get(
-
client.voices.similar(...) -> ApiListResponseVoiceSimilarOut +## templates +
client.templates.list(...) -> ApiListResponseTemplateOut
@@ -3707,7 +1645,18 @@ client.voices.get(
-Return voices nearest to a reference voice embedding. +Browse the public template gallery across all workspaces. + +Returns only templates that have an active published snapshot (`is_public=true`, +`published_definition` set, not unpublished). Results come from any workspace — +the gallery is intentionally cross-workspace so callers can discover shared +starting points regardless of their own workspace membership. + +Does not require `X-Workspace-Id`, so callers without a workspace (e.g. during +onboarding) can still browse. The response reflects the published snapshot for +each row, not any unpublished draft edits. + +Dual-auth: Bearer JWT or API key (scope `templates:read`).
@@ -3730,9 +1679,7 @@ client = OnePinClient( environment=OnePinClientEnvironment.PROD, ) -client.voices.similar( - voice_id="voice_id", -) +client.templates.list() ``` @@ -3748,7 +1695,7 @@ client.voices.similar(
-**voice_id:** `str` +**category:** `typing.Optional[typing.List[TemplateCategory]]` — Filter by category. Repeat the parameter for OR logic, e.g. `?category=media&category=creative`.
@@ -3756,7 +1703,7 @@ client.voices.similar(
-**limit:** `typing.Optional[int]` +**search:** `typing.Optional[str]` — Full-text search over template name and description.
@@ -3764,7 +1711,7 @@ client.voices.similar(
-**language:** `typing.Optional[typing.List[str]]` — Repeat for OR, e.g. ?language=en-us&language=ko-kr +**sort:** `typing.Optional[ListTemplatesRequestSort]` — Sort order: `recent` (last published), `popular` (most cloned), or `name` (alphabetical).
@@ -3772,7 +1719,23 @@ client.voices.similar(
-**workspace_id:** `typing.Optional[str]` +**offset:** `typing.Optional[int]` — Zero-based offset for page navigation. + +
+
+ +
+
+ +**limit:** `typing.Optional[int]` — Maximum number of templates to return (1–100). + +
+
+ +
+
+ +**favorites_only:** `typing.Optional[bool]`
@@ -3792,7 +1755,7 @@ client.voices.similar(
-
client.voices.favorite_voice(...) -> ApiResponseVoiceOut +
client.templates.create_template(...) -> ApiResponseTemplateOut
@@ -3804,7 +1767,19 @@ client.voices.similar(
-Mark a voice as a workspace favorite. +Create a reusable workflow template in the current workspace. + +Templates are workspace-private on creation (`is_public=false`, `is_starter=false`). +The full `WorkflowDefinition` (graph + execution config) is validated at write +time — structural errors (duplicate node/edge IDs, port mismatches, etc.) surface +here rather than when a caller later clones the template into a workflow. + +Use this to capture a workflow configuration you intend to reuse or share. To +make a template available in the public gallery, an admin must mark it public +via the admin API. To create a runnable workflow from an existing template, +use `POST /templates/{id}/clone` instead. + +Requires workspace `editor` role or higher.
@@ -3827,8 +1802,8 @@ client = OnePinClient( environment=OnePinClientEnvironment.PROD, ) -client.voices.favorite_voice( - voice_id="voice_id", +client.templates.create_template( + name="name", ) ``` @@ -3845,7 +1820,7 @@ client.voices.favorite_voice(
-**voice_id:** `str` +**name:** `str` — Display name for the template (1–200 characters, not blank).
@@ -3861,72 +1836,15 @@ client.voices.favorite_voice(
-**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. +**description:** `typing.Optional[str]` — Optional human-readable description shown in the gallery (max 2,000 characters).
- -
- - - - -
- -
client.voices.unfavorite_voice(...) -> ApiResponseDict -
-
- -#### 📝 Description - -
-
- -
-
- -Remove a voice from workspace favorites. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from onepin import OnePinClient -from onepin.environment import OnePinClientEnvironment - -client = OnePinClient( - token="", - environment=OnePinClientEnvironment.PROD, -) - -client.voices.unfavorite_voice( - voice_id="voice_id", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
-**voice_id:** `str` +**category:** `typing.Optional[TemplateCategory]` — Optional category tag used for gallery filtering.
@@ -3934,7 +1852,7 @@ client.voices.unfavorite_voice(
-**workspace_id:** `typing.Optional[str]` +**definition:** `typing.Optional[WorkflowDefinitionInput]` — Full workflow definition (graph + execution config). Validated at write time — structural errors are rejected with 422.
@@ -3954,8 +1872,7 @@ client.voices.unfavorite_voice(
-## workspace -
client.workspace.get_workspace_settings(...) -> ApiResponseWorkspaceSettingsOut +
client.templates.get(...) -> ApiResponseTemplateOut
@@ -3967,15 +1884,16 @@ client.voices.unfavorite_voice(
-Return workspace-level settings (default_language, theme). +Fetch a single template by ID. -Settings are workspace-scoped: every member of the workspace sees the -same defaults. Per-user UI preferences (e.g. personal dark-mode) belong -in a future user_settings endpoint. +Returns the template if it is visible to the caller: templates owned by the +caller's workspace are returned with the live draft definition; public/starter +templates from other workspaces are returned with the published snapshot. -Path-based authorization (via get_workspace_from_path_for_auth_context) prevents the -header-vs-path bypass class — the {ws_id} URL segment is the source of -truth for which workspace is being read. +Returns 404 for templates that exist but are not visible to the caller (not +owned, not public, not a starter) — same response as for a missing ID. + +Dual-auth: Bearer JWT or API key (scope `templates:read`).
@@ -3998,8 +1916,8 @@ client = OnePinClient( environment=OnePinClientEnvironment.PROD, ) -client.workspace.get_workspace_settings( - ws_id="ws_id", +client.templates.get( + template_id="template_id", ) ``` @@ -4016,7 +1934,15 @@ client.workspace.get_workspace_settings(
-**ws_id:** `str` +**template_id:** `str` + +
+
+ +
+
+ +**workspace_id:** `typing.Optional[str]`
@@ -4036,8 +1962,7 @@ client.workspace.get_workspace_settings(
-## workspace-aggregates -
client.workspace_aggregates.workspace_runs_stats(...) -> ApiResponseWorkspaceRunsStatsOut +
client.templates.delete_template(...) -> ApiResponseDict
@@ -4049,7 +1974,18 @@ client.workspace.get_workspace_settings(
-Aggregate workflow-run counts grouped by raw RunStatus across the workspace. +Delete a template owned by the caller's workspace. + +The delete is a soft delete — the record is hidden from the gallery and +all visibility checks immediately, but is not physically removed. Any +workflows previously cloned from this template are unaffected; clone +creates an independent copy of the definition at clone time. + +Restrictions: +- Only the owning workspace may delete its templates (403 otherwise). +- Platform starter templates (`is_starter=true`) cannot be deleted (403). + +Requires workspace `editor` role or higher.
@@ -4072,7 +2008,9 @@ client = OnePinClient( environment=OnePinClientEnvironment.PROD, ) -client.workspace_aggregates.workspace_runs_stats() +client.templates.delete_template( + template_id="template_id", +) ``` @@ -4088,15 +2026,7 @@ client.workspace_aggregates.workspace_runs_stats()
-**from:** `typing.Optional[datetime.datetime]` — Filter runs by created_at >= this ISO datetime. - -
-
- -
-
- -**to:** `typing.Optional[datetime.datetime]` — Filter runs by created_at <= this ISO datetime. +**template_id:** `str`
@@ -4124,7 +2054,7 @@ client.workspace_aggregates.workspace_runs_stats()
-
client.workspace_aggregates.workspace_workflows_stats(...) -> ApiResponseWorkspaceWorkflowsStatsOut +
client.templates.update_template(...) -> ApiResponseTemplateOut
@@ -4136,7 +2066,20 @@ client.workspace_aggregates.workspace_runs_stats()
-Aggregate workflow counts grouped by derived WorkflowListStatus across the workspace. +Update a template owned by the caller's workspace. + +All fields are optional (omit to keep the stored value). When `definition` +is supplied it is a full replace — send the complete graph, not a partial +diff. Structural validation runs on write, same as `POST /templates`. + +Restrictions: +- Only the owning workspace may update its templates (403 otherwise). +- Platform starter templates (`is_starter=true`) are read-only via this + endpoint regardless of workspace ownership (403). +- Updates apply only to the draft/live definition; the published gallery + snapshot is not updated until an admin republishes. + +Requires workspace `editor` role or higher.
@@ -4159,7 +2102,9 @@ client = OnePinClient( environment=OnePinClientEnvironment.PROD, ) -client.workspace_aggregates.workspace_workflows_stats() +client.templates.update_template( + template_id="template_id", +) ``` @@ -4175,7 +2120,7 @@ client.workspace_aggregates.workspace_workflows_stats()
-**from:** `typing.Optional[datetime.datetime]` — Filter workflows by created_at >= this ISO datetime. +**template_id:** `str`
@@ -4183,7 +2128,7 @@ client.workspace_aggregates.workspace_workflows_stats()
-**to:** `typing.Optional[datetime.datetime]` — Filter workflows by created_at <= this ISO datetime. +**workspace_id:** `typing.Optional[str]`
@@ -4191,7 +2136,31 @@ client.workspace_aggregates.workspace_workflows_stats()
-**workspace_id:** `typing.Optional[str]` +**name:** `typing.Optional[str]` + +
+
+ +
+
+ +**description:** `typing.Optional[str]` + +
+
+ +
+
+ +**category:** `typing.Optional[TemplateCategory]` + +
+
+ +
+
+ +**definition:** `typing.Optional[WorkflowDefinitionInput]` — Full-replace on PATCH. Omit to keep the stored value. Explicit `null` is rejected — there is no 'empty graph' use-case worth the ambiguity. The union with `null` here only makes omission easy for FE clients; see `reject_null_definition` for the runtime guard.
@@ -4211,8 +2180,7 @@ client.workspace_aggregates.workspace_workflows_stats()
-## workspace-members -
client.workspace_members.list_members(...) -> ApiListResponseWorkspaceMemberOut +
client.templates.estimate_template(...) -> ApiResponseTemplateEstimateResponse
@@ -4224,7 +2192,28 @@ client.workspace_aggregates.workspace_workflows_stats()
-List active members and pending invites for a workspace. +Estimate the credit cost of running a workflow built from this template. + +Returns a per-unit pricing guide expressed in credits per +`unit_chars` input characters (default 1,000). Because the template does not +contain the caller's actual script, the estimate uses a synthetic fixed-length +input to compute a reproducible per-unit rate. Multiply by your expected +character count to project total cost. + +The response distinguishes variable costs (scale with script length, e.g. +synthesis) from fixed costs (apply once per run regardless of length). A +node-level breakdown is included so callers can see which processing steps +drive the cost. + +Results are cached against the template definition and current pricing rates. +`cache_status` indicates whether this response was served from cache (`hit`), +computed fresh (`miss`), or recomputed because the definition or rates changed +(`stale`). + +Visibility rules match `GET /templates/{id}` — own-workspace templates use the +draft definition; cross-workspace templates use the published snapshot. + +Dual-auth: Bearer JWT or API key (scope `templates:read`).
@@ -4247,8 +2236,8 @@ client = OnePinClient( environment=OnePinClientEnvironment.PROD, ) -client.workspace_members.list_members( - ws_id="ws_id", +client.templates.estimate_template( + template_id="template_id", ) ``` @@ -4265,7 +2254,15 @@ client.workspace_members.list_members(
-**ws_id:** `str` +**template_id:** `str` + +
+
+ +
+
+ +**workspace_id:** `typing.Optional[str]`
@@ -4285,7 +2282,7 @@ client.workspace_members.list_members(
-
client.workspace_members.create_invite(...) -> ApiResponseWorkspaceInviteOut +
client.templates.clone(...) -> ApiResponseWorkflowOut
@@ -4297,10 +2294,24 @@ client.workspace_members.list_members(
-Invite a user to the workspace via email. +Create a runnable workflow in the caller's workspace from a template. -POD-301: gated by `seats` plan limit on the workspace owner's plan. -Active members + pending invites both count against the cap. +This is the primary way to use a template: it produces a new `Workflow` +owned by the caller's workspace, ready to accept scripts and run jobs. + +Use `body.name` to set the workflow name; omit it (or send blank/whitespace) +to get the default `"{template name} (Copy)"`. + +Cross-workspace clones (gallery/starter templates) copy the published +snapshot so unpublished draft edits made by the template owner never leak to +other workspaces. Same-workspace clones copy the live draft definition. + +Use `GET /templates/{id}/estimate` first to preview credit costs before +committing to a clone and run. Use `POST /workflows/{id}/duplicate` to copy +an existing workflow rather than starting from a template. + +Requires workspace `editor` role or higher. +Dual-auth: Bearer JWT or API key (scope `workflows:write`).
@@ -4323,10 +2334,8 @@ client = OnePinClient( environment=OnePinClientEnvironment.PROD, ) -client.workspace_members.create_invite( - ws_id="ws_id", - email="email", - role="admin", +client.templates.clone( + template_id="template_id", ) ``` @@ -4343,7 +2352,7 @@ client.workspace_members.create_invite(
-**ws_id:** `str` +**template_id:** `str`
@@ -4351,7 +2360,7 @@ client.workspace_members.create_invite(
-**email:** `str` +**workspace_id:** `typing.Optional[str]`
@@ -4359,7 +2368,7 @@ client.workspace_members.create_invite(
-**role:** `WorkspaceRole` +**name:** `typing.Optional[str]`
@@ -4379,7 +2388,7 @@ client.workspace_members.create_invite(
-
client.workspace_members.remove_member(...) -> ApiResponseDict +
client.templates.favorite_template(...) -> ApiResponseTemplateOut
@@ -4391,7 +2400,17 @@ client.workspace_members.create_invite(
-Remove an active member. Admin only. +Add a template to the current user's favorites. + +Favorites are per-user, not per-workspace — the same favorite list is +visible regardless of which workspace the caller is currently acting in. +Any template visible to the caller (own workspace, public, or starter) can +be favorited. + +Returns 404 when the template does not exist or is not visible to the caller. +Calling this endpoint on an already-favorited template is idempotent (returns +200 with the template). Use `DELETE /templates/{id}/favorite` to remove. +Does not require `X-Workspace-Id`.
@@ -4414,9 +2433,8 @@ client = OnePinClient( environment=OnePinClientEnvironment.PROD, ) -client.workspace_members.remove_member( - ws_id="ws_id", - member_id="member_id", +client.templates.favorite_template( + template_id="template_id", ) ``` @@ -4433,15 +2451,7 @@ client.workspace_members.remove_member(
-**ws_id:** `str` - -
-
- -
-
- -**member_id:** `str` +**template_id:** `str`
@@ -4461,7 +2471,7 @@ client.workspace_members.remove_member(
-
client.workspace_members.update_member_role(...) -> ApiResponseDict +
client.templates.unfavorite_template(...) -> ApiResponseDict
@@ -4473,7 +2483,10 @@ client.workspace_members.remove_member(
-Change a member's role. Admin only. +Remove a template from the current user's favorites. + +Idempotent and non-enumerating: returns an empty success response whether +or not the favorite or the template exists. Does not require `X-Workspace-Id`.
@@ -4496,10 +2509,8 @@ client = OnePinClient( environment=OnePinClientEnvironment.PROD, ) -client.workspace_members.update_member_role( - ws_id="ws_id", - member_id="member_id", - role="admin", +client.templates.unfavorite_template( + template_id="template_id", ) ``` @@ -4516,23 +2527,7 @@ client.workspace_members.update_member_role(
-**ws_id:** `str` - -
-
- -
-
- -**member_id:** `str` - -
-
- -
-
- -**request:** `WorkspaceMemberRoleUpdate` +**template_id:** `str`
@@ -4552,7 +2547,8 @@ client.workspace_members.update_member_role(
-
client.workspace_members.revoke_invite(...) -> ApiResponseDict +## voices +
client.voices.list(...) -> ApiCountedListResponseVoiceOut
@@ -4564,7 +2560,25 @@ client.workspace_members.update_member_role(
-Revoke a pending invite. Admin only. +List TTS voices available to the current workspace. + +Every filter accepts repeat-key OR semantics: +`?gender=female&gender=neutral&category=narration&source=platform&source=workspace`. +Filters combine across fields with AND; within a field, values OR. + +`language` matches a voice when any of its declared locales matches any +requested value. Platform voices with no declared locales (catalog gaps) +are treated as general-use and match every language filter. User-uploaded +/ cloned voices with no declared locales are excluded — that state means +"language unknown" pending the clone flow's language detection. + +Multi-sort: `sort` and `order` are parallel lists. `?sort=uses_count&sort=name&order=desc&order=asc` +orders primarily by uses_count DESC, secondarily by name ASC. When `order` +is shorter than `sort`, missing entries default per-field: +`name=asc, created_at=desc, uses_count=desc`. When `sort` is omitted, list +defaults to newest-first (or most-recently-favorited-first if +`favorites_only=true`). Every sort path appends `Voice.id ASC` as a +deterministic tiebreaker for pagination stability.
@@ -4587,10 +2601,7 @@ client = OnePinClient( environment=OnePinClientEnvironment.PROD, ) -client.workspace_members.revoke_invite( - ws_id="ws_id", - invite_id="invite_id", -) +client.voices.list() ``` @@ -4606,7 +2617,7 @@ client.workspace_members.revoke_invite(
-**ws_id:** `str` +**offset:** `typing.Optional[int]` — Number of results to skip for pagination.
@@ -4614,7 +2625,7 @@ client.workspace_members.revoke_invite(
-**invite_id:** `str` +**limit:** `typing.Optional[int]` — Maximum number of results to return (1–100).
@@ -4622,74 +2633,87 @@ client.workspace_members.revoke_invite(
-**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. +**favorites_only:** `typing.Optional[bool]` — When true, return only voices in the workspace's favorites list.
- -
+
+
+**source:** `typing.Optional[typing.List[ListVoicesRequestSourceItem]]` — Repeat for OR across scopes: `platform` for system-provided voices, `workspace` for workspace-owned voices. +
-
-
client.workspace_members.update_invite_role(...) -> ApiResponseWorkspaceInviteOut
-#### 📝 Description +**gender:** `typing.Optional[typing.List[VoiceGender]]` — Repeat for OR + +
+
+**age:** `typing.Optional[typing.List[VoiceAge]]` — Repeat for OR + +
+
+
-Update role on a pending invite. Admin only. -
-
+**category:** `typing.Optional[typing.List[VoiceCategory]]` — Repeat for OR + -#### 🔌 Usage -
+**accent:** `typing.Optional[typing.List[VoiceAccent]]` — Repeat for OR + +
+
+
-```python -from onepin import OnePinClient -from onepin.environment import OnePinClientEnvironment - -client = OnePinClient( - token="", - environment=OnePinClientEnvironment.PROD, -) +**search:** `typing.Optional[str]` — Full-text search against voice name, description, and tags. + +
+
-client.workspace_members.update_invite_role( - ws_id="ws_id", - invite_id="invite_id", - role="admin", -) +
+
-``` +**sort:** `typing.Optional[typing.List[ListVoicesRequestSortItem]]` — Repeat for multi-sort. Pairs with `order` index-wise. +
+ +
+
+ +**order:** `typing.Optional[typing.List[ListVoicesRequestOrderItem]]` — Parallel to sort[]; shorter is padded with per-field defaults. +
-#### ⚙️ Parameters -
+**provider:** `typing.Optional[typing.List[ListVoicesRequestProviderItem]]` — Repeat for OR, e.g. ?provider=elevenlabs&provider=rime + +
+
+
-**ws_id:** `str` +**model:** `typing.Optional[typing.List[str]]` — Repeat for OR. Filters platform voices by TTS model, e.g. ?model=arcana&model=sonic-2
@@ -4697,7 +2721,7 @@ client.workspace_members.update_invite_role(
-**invite_id:** `str` +**language:** `typing.Optional[typing.List[ListVoicesRequestLanguageItem]]` — Repeat for OR, e.g. ?language=en-us&language=ko-kr
@@ -4705,7 +2729,7 @@ client.workspace_members.update_invite_role(
-**request:** `WorkspaceMemberRoleUpdate` +**workspace_id:** `typing.Optional[str]`
@@ -4725,7 +2749,7 @@ client.workspace_members.update_invite_role(
-
client.workspace_members.accept_invite(...) -> ApiResponseDict +
client.voices.get_voice_facets(...) -> ApiResponseVoiceFacetsOut
@@ -4737,10 +2761,32 @@ client.workspace_members.update_invite_role(
-Accept a workspace invite. Authenticated; validates email match. +Filter-bar options (chips) for the voice browser, one list per dimension. + +Returns `providers`, `models`, `languages` (data-driven) plus `genders`, +`ages`, `categories`, `accents` (fixed enums) as `VoiceFacetItem[]` so the FE +builds the whole filter bar — with per-chip count badges — in a single request +instead of hardcoding option lists (mirrors `GET /dictionary/languages`). Each +item is `{value, label, count}`: `value` is passed straight back to +`GET /voices`; `label` is the display name for providers/models and `null` +elsewhere (the FE owns language + enum labels); `count` is the number of +matching voices. For `languages`/`models`, `count` counts only voices that +explicitly declare the value — "general-use" platform voices (no declared +locales/models) that `GET /voices` matches against every language/model filter +are not counted, so a chip's count can be lower than the `GET /voices` result. + +Accepts the SAME filters as `GET /voices` (tab scope `source`/`favorites_only`, +plus `provider`/`model`/`language`/`gender`/`age`/`category`/`accent`/`search`). +`count` is context-aware (faceted search): each dimension's counts apply every +OTHER active filter but exclude that dimension's own selection — e.g. with +`provider=elevenlabs` the language counts are scoped to ElevenLabs, while the +provider chips still show every provider so the caller can switch. -POD-301: re-checks `seats` against owner's plan at accept time — owner may -have downgraded since invite sent. +Count-0 policy: data-driven dimensions omit count-0 values (only present ones, +each a valid `GET /voices` filter — providers/models restricted to the enabled +catalog, languages to the supported-locale allowlist, so a chip never 422s). +Enum dimensions always return the full enum in natural order, count-0 included, +for the FE to grey out.
@@ -4763,9 +2809,7 @@ client = OnePinClient( environment=OnePinClientEnvironment.PROD, ) -client.workspace_members.accept_invite( - token="token", -) +client.voices.get_voice_facets() ``` @@ -4781,7 +2825,7 @@ client.workspace_members.accept_invite(
-**token:** `str` +**favorites_only:** `typing.Optional[bool]` — Favorites tab scope
@@ -4789,71 +2833,71 @@ client.workspace_members.accept_invite(
-**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. +**source:** `typing.Optional[typing.List[GetVoiceFacetsApiV1VoicesFacetsGetRequestSourceItem]]` — Tab scope — repeat for OR, same values as GET /voices (e.g. platform, workspace)
+ +
+
+ +**gender:** `typing.Optional[typing.List[VoiceGender]]` — Repeat for OR +
+
+
+**age:** `typing.Optional[typing.List[VoiceAge]]` — Repeat for OR +
-
-## workspaces -
client.workspaces.list_workspaces(...) -> ApiListResponseWorkspaceOut
-#### 📝 Description +**category:** `typing.Optional[typing.List[VoiceCategory]]` — Repeat for OR + +
+
+**accent:** `typing.Optional[typing.List[VoiceAccent]]` — Repeat for OR + +
+
+
-List workspaces the current user is a member of. -
-
+**search:** `typing.Optional[str]` + -#### 🔌 Usage - -
-
-
-```python -from onepin import OnePinClient -from onepin.environment import OnePinClientEnvironment - -client = OnePinClient( - token="", - environment=OnePinClientEnvironment.PROD, -) - -client.workspaces.list_workspaces() - -``` -
-
+**provider:** `typing.Optional[typing.List[str]]` — Repeat for OR, e.g. ?provider=elevenlabs&provider=rime +
-#### ⚙️ Parameters -
+**model:** `typing.Optional[typing.List[str]]` — Repeat for OR. Filters platform voices by TTS model, e.g. ?model=arcana&model=sonic-2 + +
+
+
-**offset:** `typing.Optional[int]` +**language:** `typing.Optional[typing.List[str]]` — Repeat for OR, e.g. ?language=en-us&language=ko-kr
@@ -4861,7 +2905,7 @@ client.workspaces.list_workspaces()
-**limit:** `typing.Optional[int]` +**workspace_id:** `typing.Optional[str]`
@@ -4881,7 +2925,7 @@ client.workspaces.list_workspaces()
-
client.workspaces.create_workspace(...) -> ApiResponseWorkspaceOut +
client.voices.get(...) -> ApiResponseVoiceOut
@@ -4893,11 +2937,13 @@ client.workspaces.list_workspaces()
-Create a new workspace owned by the current user. +Fetch a single voice by its ID. -POD-301: gated by `workspaces_per_owner` plan limit. Free=1, Creator=1, -Studio=2, Enterprise=bespoke. Owner soft-deletes don't free up quota until -purge — keeps the gate honest against rapid create/delete cycles. +Returns both platform (system-wide) voices and voices that belong to the +caller's workspace. Returns 404 when the voice does not exist or is not +accessible to the caller's workspace. The `sample_url` field is a +time-limited presigned URL valid for 1 hour; regenerate it by calling this +endpoint again rather than caching it long-term.
@@ -4920,8 +2966,8 @@ client = OnePinClient( environment=OnePinClientEnvironment.PROD, ) -client.workspaces.create_workspace( - name="name", +client.voices.get( + voice_id="voice_id", ) ``` @@ -4938,15 +2984,7 @@ client.workspaces.create_workspace(
-**name:** `str` - -
-
- -
-
- -**slug:** `typing.Optional[str]` +**voice_id:** `str`
@@ -4954,7 +2992,7 @@ client.workspaces.create_workspace(
-**color_idx:** `typing.Optional[int]` +**workspace_id:** `typing.Optional[str]`
@@ -4974,7 +3012,7 @@ client.workspaces.create_workspace(
-
client.workspaces.slug_available(...) -> ApiResponseSlugAvailabilityOut +
client.voices.similar(...) -> ApiListResponseVoiceSimilarOut
@@ -4986,17 +3024,16 @@ client.workspaces.create_workspace(
-POD-557: admin-only live availability check for a workspace slug. - -Declared before `/{workspace_id}` so the literal path wins over the UUID -route. Auth is hard 4xx (missing/invalid X-Workspace-Id -> 400, not a member --> 404, not admin -> 403, missing ?slug -> 422). Slug content is soft 200 -`{available, reason?: invalid|reserved|taken}`, self-excluded against the -X-Workspace-Id workspace's own current slug. Global across tenants. +Return voices acoustically similar to a reference voice. -Advisory only — a point-in-time snapshot. A concurrent request can claim the -slug between this check and the caller's POST/PATCH, so callers must still -handle 409 WORKSPACE_SLUG_TAKEN on the write path. +Results are ranked by semantic similarity score (descending) and include the +reference voice's workspace voices and all platform voices. Each result +includes a `similarity_score` between 0 and 1. Optionally filter by one or +more `language` BCP-47 codes (repeat the parameter for OR semantics); up to +16 language values are accepted. Returns 503 when the reference voice has no +embedding yet — retry after the indicated `Retry-After` interval. Prefer this +endpoint over `GET /voices` with manual filtering when building a +"voices like this" recommendation UI.
@@ -5019,8 +3056,8 @@ client = OnePinClient( environment=OnePinClientEnvironment.PROD, ) -client.workspaces.slug_available( - slug="slug", +client.voices.similar( + voice_id="voice_id", ) ``` @@ -5037,7 +3074,23 @@ client.workspaces.slug_available(
-**slug:** `str` — Candidate slug; normalized (strip().lower()) before checks. +**voice_id:** `str` + +
+
+ +
+
+ +**limit:** `typing.Optional[int]` — Number of similar voices to return (1–50). + +
+
+ +
+
+ +**language:** `typing.Optional[typing.List[str]]` — Repeat for OR, e.g. ?language=en-us&language=ko-kr
@@ -5065,7 +3118,7 @@ client.workspaces.slug_available(
-
client.workspaces.get_workspace(...) -> ApiResponseWorkspaceOut +
client.voices.favorite_voice(...) -> ApiResponseVoiceOut
@@ -5077,7 +3130,12 @@ client.workspaces.slug_available(
-Get a workspace the current user is a member of. +Add a voice to the current workspace's favorites. + +Favorites are workspace-scoped, not per-user: all members of the workspace +see the same favorited set. Idempotent — favoriting a voice that is already +favorited succeeds without error. Returns the voice with `is_favorite=true`. +Requires the caller to have at least editor role in the workspace.
@@ -5100,8 +3158,8 @@ client = OnePinClient( environment=OnePinClientEnvironment.PROD, ) -client.workspaces.get_workspace( - workspace_id="workspace_id", +client.voices.favorite_voice( + voice_id="voice_id", ) ``` @@ -5118,7 +3176,15 @@ client.workspaces.get_workspace(
-**workspace_id:** `str` +**voice_id:** `str` + +
+
+ +
+
+ +**workspace_id:** `typing.Optional[str]`
@@ -5138,7 +3204,7 @@ client.workspaces.get_workspace(
-
client.workspaces.delete_workspace(...) -> ApiResponseDict +
client.voices.unfavorite_voice(...) -> ApiResponseDict
@@ -5150,7 +3216,11 @@ client.workspaces.get_workspace(
-Soft-delete a workspace and cascade soft-delete to its resources. +Remove a voice from the current workspace's favorites. + +Idempotent — removing a voice that is not currently favorited succeeds +without error. Requires the caller to have at least editor role in the +workspace.
@@ -5173,8 +3243,8 @@ client = OnePinClient( environment=OnePinClientEnvironment.PROD, ) -client.workspaces.delete_workspace( - workspace_id="workspace_id", +client.voices.unfavorite_voice( + voice_id="voice_id", ) ``` @@ -5191,7 +3261,15 @@ client.workspaces.delete_workspace(
-**workspace_id:** `str` +**voice_id:** `str` + +
+
+ +
+
+ +**workspace_id:** `typing.Optional[str]`
@@ -5211,7 +3289,8 @@ client.workspaces.delete_workspace(
-
client.workspaces.update_workspace(...) -> ApiResponseWorkspaceOut +## workspace +
client.workspace.get_workspace_settings(...) -> ApiResponseWorkspaceSettingsOut
@@ -5223,7 +3302,16 @@ client.workspaces.delete_workspace(
-Update workspace name, color, and/or slug. Admin only. +Return workspace-level settings for the specified workspace. + +Currently exposes `default_language` (the locale used as the default for +new workflow nodes) and `theme` (the workspace's display color theme). +Settings are workspace-scoped: all members of the workspace share the +same values. Per-user preferences (e.g. personal dark mode) are outside +the scope of this endpoint. + +If settings have not yet been explicitly configured for the workspace, +defaults are returned (and persisted) on first access.
@@ -5246,8 +3334,8 @@ client = OnePinClient( environment=OnePinClientEnvironment.PROD, ) -client.workspaces.update_workspace( - workspace_id="workspace_id", +client.workspace.get_workspace_settings( + ws_id="ws_id", ) ``` @@ -5264,31 +3352,7 @@ client.workspaces.update_workspace(
-**workspace_id:** `str` - -
-
- -
-
- -**name:** `typing.Optional[str]` - -
-
- -
-
- -**slug:** `typing.Optional[str]` - -
-
- -
-
- -**color_idx:** `typing.Optional[int]` +**ws_id:** `str`
@@ -5308,8 +3372,8 @@ client.workspaces.update_workspace(
-## uploads -
client.uploads.create(...) -> ApiResponseUploadCreateResponse +## workspace-members +
client.workspace_members.list_members(...) -> ApiListResponseWorkspaceMemberOut
@@ -5321,7 +3385,21 @@ client.workspaces.update_workspace(
-Request a presigned URL for uploading a file. +List active members and pending invites for a workspace. + +Returns a unified list combining confirmed members (status `active`) and +outstanding invites that have not yet been accepted or revoked (status +`invited`). Pending invites appear with `user_id: null` and only the +`email` and `role` fields populated. + +The list is sorted: the requesting user appears first, then admins by +join date, then other members by join date. No pagination — the full +roster is returned in a single response. + +Roles: +- `admin`: can manage members, invites, workspace settings, and all content. +- `editor`: can create, edit, and run workflows; cannot manage members. +- `viewer`: read-only access to workspace content and run history.
@@ -5344,9 +3422,8 @@ client = OnePinClient( environment=OnePinClientEnvironment.PROD, ) -client.uploads.create( - filename="filename", - category="script", +client.workspace_members.list_members( + ws_id="ws_id", ) ``` @@ -5363,23 +3440,7 @@ client.uploads.create(
-**filename:** `str` - -
-
- -
-
- -**category:** `UploadRequestCategory` - -
-
- -
-
- -**workspace_id:** `typing.Optional[str]` +**ws_id:** `str`
@@ -5399,7 +3460,7 @@ client.uploads.create(
-
client.uploads.confirm(...) -> ApiResponseUploadOut +
client.workspace_members.create_invite(...) -> ApiResponseWorkspaceInviteOut
@@ -5411,12 +3472,24 @@ client.uploads.create(
-Confirm upload and move file to final location. +Invite a user to the workspace by email. Admin only. + +Creates a pending invite and sends an invitation email to the specified +address. The invitee does not need to have an existing account — they can +sign up after receiving the invite. The invite includes a role +(`admin`, `editor`, or `viewer`) that the invitee will receive upon +accepting. -POD-301: gates the file size against `storage_bytes_per_workspace` whenever -the upload binds to a workspace-scoped resource (header OR derived from -context_id). Records the storage_charge event in the same transaction as the -upload row update. +Invites expire after 14 days. Only one pending invite per email address +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}`.
@@ -5439,10 +3512,10 @@ client = OnePinClient( environment=OnePinClientEnvironment.PROD, ) -client.uploads.confirm( - upload_id="upload_id", - context_type="workflow", - context_id="context_id", +client.workspace_members.create_invite( + ws_id="ws_id", + email="email", + role="admin", ) ``` @@ -5459,15 +3532,7 @@ client.uploads.confirm(
-**upload_id:** `str` - -
-
- -
-
- -**context_type:** `UploadConfirmRequestContextType` +**ws_id:** `str`
@@ -5475,7 +3540,7 @@ client.uploads.confirm(
-**context_id:** `str` +**email:** `str` — Email address to invite. Normalized to lowercase. Returns 409 if this address is already an active member or has a pending invite.
@@ -5483,7 +3548,7 @@ client.uploads.confirm(
-**workspace_id:** `typing.Optional[str]` +**role:** `WorkspaceRole` — Role to grant when the invite is accepted: `admin`, `editor`, or `viewer`.
@@ -5503,7 +3568,7 @@ client.uploads.confirm(
-
client.uploads.delete(...) -> ApiResponseDict +
client.workspace_members.remove_member(...) -> ApiResponseDict
@@ -5515,17 +3580,18 @@ client.uploads.confirm(
-Delete an upload and its S3 object. +Remove an active member from the workspace. Admin only. + +The removed member immediately loses access to all workspace resources. +They receive an email notification informing them they have been removed. -DB record is deleted first (committed on response). S3 cleanup runs -after the response via a background task so the file is only removed -once the DB commit succeeds. +Two protections prevent accidental lockouts: +- The workspace owner cannot be removed. +- The last remaining admin cannot be removed (returns 409). -POD-301: if the upload was confirmed against a workspace-scoped resource, -release the bytes back to that workspace's storage counter. Without this, -storage_bytes_used drifts upward forever and customers stay capped after -deleting files. Read upload state BEFORE delete_for_user — the row is gone -after that call. +Removing a member does not affect their account or other workspaces. To +block an invited but not-yet-accepted user instead, revoke the invite via +`DELETE /workspaces/{ws_id}/invites/{invite_id}`.
@@ -5548,8 +3614,9 @@ client = OnePinClient( environment=OnePinClientEnvironment.PROD, ) -client.uploads.delete( - upload_id="upload_id", +client.workspace_members.remove_member( + ws_id="ws_id", + member_id="member_id", ) ``` @@ -5566,7 +3633,7 @@ client.uploads.delete(
-**upload_id:** `str` +**ws_id:** `str`
@@ -5574,7 +3641,7 @@ client.uploads.delete(
-**workspace_id:** `typing.Optional[str]` +**member_id:** `str`
@@ -5594,8 +3661,7 @@ client.uploads.delete(
-## usage -
client.usage.usage_summary(...) -> ApiResponseUsageSummaryOut +
client.workspace_members.update_member_role(...) -> ApiResponseDict
@@ -5607,7 +3673,18 @@ client.uploads.delete(
-Return workspace usage totals plus tab-specific aggregate activity buckets. +Change a workspace member's role. Admin only. + +Updates the role of an active member to `admin`, `editor`, or `viewer`. +The operation is idempotent — setting a member to their current role +succeeds silently (no error, no duplicate email notification). + +Two protections prevent accidental lockouts: +- The workspace owner's role cannot be changed. +- The last remaining admin cannot be demoted (returns 409). + +When the role actually changes, the affected member receives an email +notification describing their new permissions.
@@ -5630,7 +3707,11 @@ client = OnePinClient( environment=OnePinClientEnvironment.PROD, ) -client.usage.usage_summary() +client.workspace_members.update_member_role( + ws_id="ws_id", + member_id="member_id", + role="admin", +) ``` @@ -5646,15 +3727,7 @@ client.usage.usage_summary()
-**range:** `typing.Optional[UsageSummaryApiV1UsageSummaryGetRequestRange]` — Rolling local calendar-day range. - -
-
- -
-
- -**activity_view:** `typing.Optional[UsageSummaryApiV1UsageSummaryGetRequestActivityView]` — Activity chart view: daily=7 local days, weekly=12 Monday-start weeks, monthly=12 months. +**ws_id:** `str`
@@ -5662,7 +3735,7 @@ client.usage.usage_summary()
-**timezone:** `typing.Optional[str]` — IANA timezone for local day bucketing. +**member_id:** `str`
@@ -5670,7 +3743,7 @@ client.usage.usage_summary()
-**workspace_id:** `typing.Optional[str]` +**request:** `WorkspaceMemberRoleUpdate`
@@ -5690,7 +3763,7 @@ client.usage.usage_summary()
-
client.usage.usage_by_language(...) -> ApiResponseUsageByLanguageOut +
client.workspace_members.revoke_invite(...) -> ApiResponseDict
@@ -5702,11 +3775,15 @@ client.usage.usage_summary()
-Return workspace generated-audio usage grouped by language. +Cancel a pending invite so the invitee can no longer accept it. Admin only. + +The invite token is invalidated immediately. If the invitee attempts to +accept after revocation, they receive a 410 Gone. The invite is removed +from the pending list returned by `GET /workspaces/{ws_id}/members`. -``share`` is a 0..1 fraction. When ``activity_view`` is supplied, rows use -that tab's local-calendar period; otherwise they preserve legacy ``range`` -behavior. +Revoking an invite that is already accepted, revoked, or expired returns +404. To remove an already-accepted member, use +`DELETE /workspaces/{ws_id}/members/{member_id}`.
@@ -5729,7 +3806,10 @@ client = OnePinClient( environment=OnePinClientEnvironment.PROD, ) -client.usage.usage_by_language() +client.workspace_members.revoke_invite( + ws_id="ws_id", + invite_id="invite_id", +) ``` @@ -5740,28 +3820,12 @@ client.usage.usage_by_language() #### ⚙️ Parameters
-
- -
-
- -**range:** `typing.Optional[UsageByLanguageApiV1UsageByLanguageGetRequestRange]` — Rolling local calendar-day range. - -
-
- -
-
- -**activity_view:** `typing.Optional[UsageByLanguageApiV1UsageByLanguageGetRequestActivityView]` — Optional activity view period to align language rows with the selected Usage tab. When supplied, range is ignored and range in the response is null. - -
-
+
-**timezone:** `typing.Optional[str]` — IANA timezone for local day bucketing. +**ws_id:** `str`
@@ -5769,7 +3833,7 @@ client.usage.usage_by_language()
-**workspace_id:** `typing.Optional[str]` +**invite_id:** `str`
@@ -5789,7 +3853,7 @@ client.usage.usage_by_language()
-
client.usage.usage_activity(...) -> ApiListResponseUsageActivityOut +
client.workspace_members.update_invite_role(...) -> ApiResponseWorkspaceInviteOut
@@ -5801,7 +3865,12 @@ client.usage.usage_by_language()
-Return the workspace usage activity feed with stable action filters and cursor pagination. +Change the role on a pending (not yet accepted) invite. Admin only. + +Updates the role the invitee will receive when they accept. Only pending +invites can be updated — attempting to update an accepted, revoked, or +expired invite returns 409. The invitee is not notified of the role +change; the updated role takes effect when they accept.
@@ -5824,7 +3893,11 @@ client = OnePinClient( environment=OnePinClientEnvironment.PROD, ) -client.usage.usage_activity() +client.workspace_members.update_invite_role( + ws_id="ws_id", + invite_id="invite_id", + role="admin", +) ``` @@ -5840,7 +3913,7 @@ client.usage.usage_activity()
-**range:** `typing.Optional[UsageActivityApiV1UsageActivityGetRequestRange]` — Rolling local calendar-day range. +**ws_id:** `str`
@@ -5848,7 +3921,7 @@ client.usage.usage_activity()
-**type:** `typing.Optional[UsageActivityAction]` — Filter by usage activity type. +**invite_id:** `str`
@@ -5856,7 +3929,7 @@ client.usage.usage_activity()
-**user_id:** `typing.Optional[str]` — Filter by actor user id. +**request:** `WorkspaceMemberRoleUpdate`
@@ -5864,31 +3937,91 @@ client.usage.usage_activity()
-**limit:** `typing.Optional[int]` +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration.
+ +
+ + + +
+ +
client.workspace_members.accept_invite(...) -> ApiResponseDict
-**cursor:** `typing.Optional[str]` — Opaque pagination cursor. - +#### 📝 Description + +
+
+ +
+
+ +Accept a workspace invite using the token from the invitation email. + +The `token` path parameter comes from the invitation link sent to the +invitee's email. The caller must be authenticated and their verified email +address must match the address the invite was sent to (403 if it does not). + +On success the caller is added to the workspace with the role specified in +the invite, and `workspace_id` is returned so the caller can immediately +begin using that workspace. If the caller is already a member of the +workspace (e.g. accepted via a different device), the accept is idempotent +and returns the same `workspace_id`. + +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.
+
+
+ +#### 🔌 Usage
-**timezone:** `typing.Optional[str]` — IANA timezone for local day bucketing. - +
+
+ +```python +from onepin import OnePinClient +from onepin.environment import OnePinClientEnvironment + +client = OnePinClient( + token="", + environment=OnePinClientEnvironment.PROD, +) + +client.workspace_members.accept_invite( + token="token", +) + +``` +
+
+#### ⚙️ Parameters +
-**workspace_id:** `typing.Optional[str]` +
+
+ +**token:** `str`
@@ -5908,8 +4041,8 @@ client.usage.usage_activity()
-## billing -
client.billing.list_plans() -> ApiListResponseCustomerPlanResponse +## workspaces +
client.workspaces.list_workspaces(...) -> ApiListResponseWorkspaceOut
@@ -5921,12 +4054,12 @@ client.usage.usage_activity()
-List subscription plans and features (public, no authentication). +List all workspaces the current user is a member of. -Public so the marketing site (Framer) can render live pricing without a -Clerk session. Returns the same active, non-custom plan catalog as before -(name, price, interval, limits, localized ``plan_details``). Honors -``X-Language`` / ``Accept-Language`` for ``plan_details`` (defaults ``en``). +Returns workspaces where the caller has any role (admin, editor, or +viewer), including workspaces they own and workspaces they joined via +invite. Results are paginated; omits soft-deleted workspaces and the +internal system workspace.
@@ -5949,7 +4082,7 @@ client = OnePinClient( environment=OnePinClientEnvironment.PROD, ) -client.billing.list_plans() +client.workspaces.list_workspaces() ``` @@ -5965,6 +4098,22 @@ client.billing.list_plans()
+**offset:** `typing.Optional[int]` + +
+
+ +
+
+ +**limit:** `typing.Optional[int]` + +
+
+ +
+
+ **request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration.
@@ -5977,7 +4126,7 @@ client.billing.list_plans()
-
client.billing.preview_plan_change(...) -> ApiResponseCustomerPlanChangePreviewResponse +
client.workspaces.create_workspace(...) -> ApiResponseWorkspaceOut
@@ -5989,7 +4138,21 @@ client.billing.list_plans()
-Preview the cost of changing to the given plan. +Create a new workspace owned by the current user. + +Workspaces are the top-level container for all resources (workflows, +voices, dictionary entries, members). Every resource is scoped to exactly +one workspace via the `X-Workspace-Id` header on subsequent requests. + +The authenticated user becomes the workspace owner and is automatically +added as an `admin` member. An optional `slug` (1–50 characters, +lowercase kebab-case) can be supplied for a human-readable workspace +identifier; if omitted, one is auto-generated from `name`. Returns 409 +if the slug is already taken, 422 if the slug format is invalid or uses +a reserved word. + +The number of workspaces a user may own is plan-gated. Attempting to +exceed the limit returns 402.
@@ -6012,8 +4175,8 @@ client = OnePinClient( environment=OnePinClientEnvironment.PROD, ) -client.billing.preview_plan_change( - plan_id="plan_id", +client.workspaces.create_workspace( + name="name", ) ``` @@ -6030,7 +4193,23 @@ client.billing.preview_plan_change(
-**plan_id:** `str` +**name:** `str` — Human-readable workspace name (1–200 characters, non-blank). + +
+
+ +
+
+ +**slug:** `typing.Optional[str]` — Optional URL-safe identifier (lowercase kebab-case, 1–50 characters). Auto-generated from `name` if omitted. Returns 409 if taken, 422 if invalid or reserved. + +
+
+ +
+
+ +**color_idx:** `typing.Optional[int]` — Index into the workspace color palette (0–6).
@@ -6050,7 +4229,7 @@ client.billing.preview_plan_change(
-
client.billing.create_checkout(...) -> ApiResponseCheckoutResponse +
client.workspaces.slug_available(...) -> ApiResponseSlugAvailabilityOut
@@ -6062,7 +4241,23 @@ client.billing.preview_plan_change(
-Create a Stripe Checkout session for the given plan. +Check whether a slug is available for the current workspace. Admin only. + +Returns `{ available: true }` if the slug is valid, not reserved, and +not already claimed by another workspace. When unavailable, `reason` +indicates why: `invalid` (format/length), `reserved` (blocked word), or +`taken` (already in use globally). The workspace's own current slug is +self-excluded, so an admin can safely check their existing slug without +receiving `taken`. + +This is an advisory point-in-time check — a concurrent `POST /workspaces` +or `PATCH /workspaces/{id}` from another session can claim the slug +between this response and the caller's write. Always handle 409 +`WORKSPACE_SLUG_TAKEN` on `create_workspace` and `update_workspace`. + +Requires the `X-Workspace-Id` header (the workspace being renamed) and +admin role in that workspace. Missing/invalid header returns 400; not a +member returns 404; not admin returns 403.
@@ -6085,9 +4280,8 @@ client = OnePinClient( environment=OnePinClientEnvironment.PROD, ) -client.billing.create_checkout( - plan_id="plan_id", - return_url="return_url", +client.workspaces.slug_available( + slug="slug", ) ``` @@ -6104,7 +4298,7 @@ client.billing.create_checkout(
-**plan_id:** `str` +**slug:** `str` — Candidate slug; normalized (strip().lower()) before checks.
@@ -6112,7 +4306,7 @@ client.billing.create_checkout(
-**return_url:** `str` +**workspace_id:** `typing.Optional[str]`
@@ -6132,8 +4326,7 @@ client.billing.create_checkout(
-## users -
client.users.get_current_subscription() -> ApiResponseUnionCustomerSubscriptionResponseNoneType +
client.workspaces.get_workspace(...) -> ApiResponseWorkspaceOut
@@ -6145,7 +4338,12 @@ client.billing.create_checkout(
-Get the current user's active subscription. +Fetch a single workspace by ID. + +Returns the workspace if the current user is an active member (any role). +Returns 404 if the workspace does not exist, has been deleted, or the +caller is not a member — the two cases are intentionally indistinguishable +to prevent workspace enumeration.
@@ -6168,7 +4366,9 @@ client = OnePinClient( environment=OnePinClientEnvironment.PROD, ) -client.users.get_current_subscription() +client.workspaces.get_workspace( + workspace_id="workspace_id", +) ``` @@ -6184,6 +4384,14 @@ client.users.get_current_subscription()
+**workspace_id:** `str` + +
+
+ +
+
+ **request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration.
@@ -6196,7 +4404,7 @@ client.users.get_current_subscription()
-
client.users.subscribe(...) -> ApiResponseCustomerSubscriptionResponse +
client.workspaces.delete_workspace(...) -> ApiResponseDict
@@ -6208,7 +4416,16 @@ client.users.get_current_subscription()
-Create a subscription using the default payment method. +Delete a workspace and all of its resources. Owner only. + +Soft-deletes the workspace and cascades to all owned resources (workflows, +voices, dictionary entries, members, etc.). The workspace and its contents +become inaccessible via the API immediately. Data is retained for the GDPR +retention period before permanent purge. + +Only the workspace owner (the user who created it) can delete it; admin +members who are not the owner receive 404. Returns 404 if the workspace +does not exist or the caller is not the owner.
@@ -6231,8 +4448,8 @@ client = OnePinClient( environment=OnePinClientEnvironment.PROD, ) -client.users.subscribe( - plan_id="plan_id", +client.workspaces.delete_workspace( + workspace_id="workspace_id", ) ``` @@ -6249,7 +4466,7 @@ client.users.subscribe(
-**plan_id:** `str` +**workspace_id:** `str`
@@ -6269,7 +4486,7 @@ client.users.subscribe(
-
client.users.cancel_subscription() -> ApiResponseCustomerSubscriptionResponse +
client.workspaces.update_workspace(...) -> ApiResponseWorkspaceOut
@@ -6281,7 +4498,17 @@ client.users.subscribe(
-Cancel the current user's subscription at period end. +Update a workspace's name, color palette index, and/or slug. Admin only. + +All fields are optional — supply only the fields you want to change. +`slug` follows the same validation rules as on create (lowercase +kebab-case, 1–50 characters, no reserved words). Returns 409 if the new +slug is already claimed by another workspace, 422 if the slug format is +invalid or reserved. Re-setting the workspace's current slug to itself +never returns 409. + +Only workspace admins may call this endpoint; other members receive 404 +(same as not-found, to avoid leaking membership details to non-members).
@@ -6304,7 +4531,9 @@ client = OnePinClient( environment=OnePinClientEnvironment.PROD, ) -client.users.cancel_subscription() +client.workspaces.update_workspace( + workspace_id="workspace_id", +) ``` @@ -6320,6 +4549,54 @@ client.users.cancel_subscription()
+**workspace_id:** `str` + +
+
+ +
+
+ +**name:** `typing.Optional[str]` — New workspace name. Omit to leave unchanged. + +
+
+ +
+
+ +**slug:** `typing.Optional[str]` — New slug (lowercase kebab-case, 1–50 characters). Omit to leave unchanged. Returns 409 if taken, 422 if invalid or reserved. + +
+
+ +
+
+ +**color_idx:** `typing.Optional[int]` — New color palette index (0–6). Omit to leave unchanged. + +
+
+ +
+
+ +**routing_price_sensitivity:** `typing.Optional[float]` — New voice-selection price/quality balance (0.0 = pure quality, 1.0 = pure price, 0.5 = balanced). Omit to leave unchanged. + +
+
+ +
+
+ +**routing_llm_fit:** `typing.Optional[bool]` — New setting for whether automatic voice selection also weighs content fit. Omit to leave unchanged. + +
+
+ +
+
+ **request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration.
@@ -6332,7 +4609,8 @@ client.users.cancel_subscription()
-
client.users.change_plan(...) -> ApiResponseCustomerSubscriptionResponse +## uploads +
client.uploads.create(...) -> ApiResponseUploadCreateResponse
@@ -6344,7 +4622,27 @@ client.users.cancel_subscription()
-Switch the current user's subscription to a different plan. +Request a presigned URL to upload a file to object storage (step 1 of 2). + +The two-step upload flow: +1. `POST /uploads` — register the file and receive a short-lived `upload_url`. + PUT your file bytes directly to that URL (do not send them to this API). +2. `POST /uploads/{id}` — confirm the upload completed and bind the file to a + resource (e.g. a workflow). The file is moved to its final location and the + upload record transitions from `pending` to `uploaded`. + +`category` controls which file formats are accepted: +- `script` — text-based formats (txt, srt, csv, json, xliff, docx) +- `dictionary` — audio formats (mp3, wav, m4a, ogg, webm) + +The presigned URL expires within a short window (see `upload_url` TTL in the +response). If the URL expires before the PUT completes, discard this upload +record and start over with a fresh `POST /uploads` call. + +`X-Workspace-Id` is optional but recommended for workspace-scoped storage +quota tracking. API keys with a bound workspace attach automatically. + +Dual-auth: Bearer JWT or API key (scope `uploads:write`).
@@ -6367,8 +4665,9 @@ client = OnePinClient( environment=OnePinClientEnvironment.PROD, ) -client.users.change_plan( - new_plan_id="new_plan_id", +client.uploads.create( + filename="filename", + category="script", ) ``` @@ -6385,7 +4684,23 @@ client.users.change_plan(
-**new_plan_id:** `str` +**filename:** `str` — Original filename including extension (e.g. `script.txt`). Must include a file extension. + +
+
+ +
+
+ +**category:** `UploadRequestCategory` — File category. Determines which formats are accepted: `script` for text formats (txt, srt, csv, json, xliff, docx); `dictionary` for audio formats (mp3, wav, m4a, ogg, webm). + +
+
+ +
+
+ +**workspace_id:** `typing.Optional[str]`
@@ -6405,7 +4720,7 @@ client.users.change_plan(
-
client.users.cancel_scheduled_change() -> ApiResponseCustomerSubscriptionResponse +
client.uploads.confirm(...) -> ApiResponseUploadOut
@@ -6417,7 +4732,27 @@ client.users.change_plan(
-Cancel a scheduled plan downgrade. +Confirm a completed upload and bind it to a resource (step 2 of 2). + +Call this after successfully PUTting your file to the presigned URL returned +by `POST /uploads`. Provide `context_type` and `context_id` to associate the +file with an existing resource (currently `workflow` is the supported context +type). The file is moved to its final location and `status` transitions from +`pending` to `uploaded`. + +This endpoint is idempotent: if the upload was already confirmed, the current +state is returned without re-processing. + +Storage quota is checked against the workspace at confirm time. If confirming +would exceed the workspace storage limit, a 402 is returned and the file +remains in its staging location (the upload record stays `pending` so you can +delete the staging file and try a smaller file). + +Binding to a workspace-scoped resource requires the caller to be a member of +that workspace. Workspace is inferred from the resource when `X-Workspace-Id` +is omitted. + +Dual-auth: Bearer JWT or API key (scope `uploads:write`).
@@ -6440,7 +4775,11 @@ client = OnePinClient( environment=OnePinClientEnvironment.PROD, ) -client.users.cancel_scheduled_change() +client.uploads.confirm( + upload_id="upload_id", + context_type="workflow", + context_id="context_id", +) ``` @@ -6456,6 +4795,38 @@ client.users.cancel_scheduled_change()
+**upload_id:** `str` + +
+
+ +
+
+ +**context_type:** `UploadConfirmRequestContextType` — Type of resource this upload is being attached to. Currently only `workflow` is supported. + +
+
+ +
+
+ +**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. + +
+
+ +
+
+ +**workspace_id:** `typing.Optional[str]` + +
+
+ +
+
+ **request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration.
@@ -6468,7 +4839,7 @@ client.users.cancel_scheduled_change()
-
client.users.list_payment_methods() -> ApiResponseListPaymentMethodResponse +
client.uploads.delete(...) -> ApiResponseDict
@@ -6480,7 +4851,21 @@ client.users.cancel_scheduled_change()
-List the current user's saved payment methods. +Delete an upload and its associated file. + +Permanently removes the upload record and schedules the stored file for +deletion. The record is removed first; the file is cleaned up asynchronously +after the response so storage removal only happens after a successful commit. + +If the upload was previously confirmed against a workspace-scoped resource, +the consumed storage bytes are released back to the workspace quota, keeping +the workspace storage counter accurate. + +Callers can delete uploads in any state (`pending` or `uploaded`). Deleting +a `pending` upload (e.g. after an expired presigned URL) is the correct way +to clean up an abandoned upload attempt. + +Dual-auth: Bearer JWT or API key (scope `uploads:write`).
@@ -6503,7 +4888,9 @@ client = OnePinClient( environment=OnePinClientEnvironment.PROD, ) -client.users.list_payment_methods() +client.uploads.delete( + upload_id="upload_id", +) ``` @@ -6519,6 +4906,22 @@ client.users.list_payment_methods()
+**upload_id:** `str` + +
+
+ +
+
+ +**workspace_id:** `typing.Optional[str]` + +
+
+ +
+
+ **request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration.
@@ -6531,7 +4934,8 @@ client.users.list_payment_methods()
-
client.users.add_payment_method() -> ApiResponseSetupIntentResponse +## usage +
client.usage.usage_summary(...) -> ApiResponseUsageSummaryOut
@@ -6543,7 +4947,24 @@ client.users.list_payment_methods()
-Create a Stripe SetupIntent to add a new payment method. +Return aggregated usage totals and activity chart data for the workspace. + +Combines credit consumption, character and line counts, and workflow run +statistics for the requested rolling window (`range`) with a chart-ready +activity series (`activity`) bucketed by `activity_view`. + +The `credits.used` field reflects the authenticated user's own billing-period +consumption; all other aggregate fields (characters, lines, runs, daily +buckets, activity buckets) are workspace-scoped across all members. + +Date boundaries are computed in the supplied `timezone` (IANA, e.g. +`America/New_York`) so "today" and "this week" align with the caller's local +calendar. Defaults to UTC. + +Use `GET /usage/by-language` for a language-level breakdown, or +`GET /usage/activity` for the event-by-event feed. + +Dual-auth: Bearer JWT or API key (scope `workspace:read`).
@@ -6566,7 +4987,7 @@ client = OnePinClient( environment=OnePinClientEnvironment.PROD, ) -client.users.add_payment_method() +client.usage.usage_summary() ``` @@ -6582,6 +5003,38 @@ client.users.add_payment_method()
+**range:** `typing.Optional[UsageSummaryApiV1UsageSummaryGetRequestRange]` — Rolling local calendar-day range. + +
+
+ +
+
+ +**activity_view:** `typing.Optional[UsageSummaryApiV1UsageSummaryGetRequestActivityView]` — Activity chart view: daily=7 local days, weekly=12 Monday-start weeks, monthly=12 months. + +
+
+ +
+
+ +**timezone:** `typing.Optional[str]` — IANA timezone for local day bucketing. + +
+
+ +
+
+ +**workspace_id:** `typing.Optional[str]` + +
+
+ +
+
+ **request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration.
@@ -6594,7 +5047,7 @@ client.users.add_payment_method()
-
client.users.delete_payment_method(...) -> ApiResponseDict +
client.usage.usage_by_language(...) -> ApiResponseUsageByLanguageOut
@@ -6606,7 +5059,21 @@ client.users.add_payment_method()
-Detach a payment method from the current user's account. +Return workspace audio generation usage broken down by language. + +Each row represents one locale with its share of total credit and character +consumption. `share` is a 0..1 fraction of workspace-wide usage for the +period; multiply by 100 for a percentage. + +Period selection: supply `activity_view` to align the language rows with +the same period shown on the Usage dashboard chart (daily = last 7 local +days, weekly = last 12 Monday-start weeks, monthly = last 12 months). When +`activity_view` is provided, `range` is ignored and `range` in the response +is `null`. Omit `activity_view` to use the rolling `range` window instead. + +Date boundaries are computed in the supplied `timezone` (IANA). Defaults to UTC. + +Dual-auth: Bearer JWT or API key (scope `workspace:read`).
@@ -6629,9 +5096,7 @@ client = OnePinClient( environment=OnePinClientEnvironment.PROD, ) -client.users.delete_payment_method( - payment_method_id="payment_method_id", -) +client.usage.usage_by_language() ``` @@ -6647,7 +5112,31 @@ client.users.delete_payment_method(
-**payment_method_id:** `str` +**range:** `typing.Optional[UsageByLanguageApiV1UsageByLanguageGetRequestRange]` — Rolling local calendar-day range. + +
+
+ +
+
+ +**activity_view:** `typing.Optional[UsageByLanguageApiV1UsageByLanguageGetRequestActivityView]` — Optional activity view period to align language rows with the selected Usage tab. When supplied, range is ignored and range in the response is null. + +
+
+ +
+
+ +**timezone:** `typing.Optional[str]` — IANA timezone for local day bucketing. + +
+
+ +
+
+ +**workspace_id:** `typing.Optional[str]`
@@ -6667,7 +5156,7 @@ client.users.delete_payment_method(
-
client.users.set_default_payment_method(...) -> ApiResponseDict +
client.usage.usage_activity(...) -> ApiListResponseUsageActivityOut
@@ -6679,7 +5168,25 @@ client.users.delete_payment_method(
-Set a payment method as the default for the current user. +Return the workspace activity feed as a cursor-paginated event list. + +Each item represents a discrete workspace event (workflow run, voice generated, +template applied, member invited, API key created, settings changed). Events are +ordered newest-first within the requested rolling window. + +Filtering: +- `type` narrows to a single action kind (e.g. `workflow_run`). +- `user_id` restricts to events triggered by a specific workspace member; + returns 404 if the user is not a member of this workspace. +- Both filters can be combined. + +Pagination: pass the `cursor` value from a previous response to retrieve the +next page. An absent or null `cursor` in the response means no further pages +exist. Page size is controlled by `limit` (1–100, default 20). + +Date boundaries are computed in the supplied `timezone` (IANA). Defaults to UTC. + +Dual-auth: Bearer JWT or API key (scope `workspace:read`).
@@ -6702,9 +5209,7 @@ client = OnePinClient( environment=OnePinClientEnvironment.PROD, ) -client.users.set_default_payment_method( - payment_method_id="payment_method_id", -) +client.usage.usage_activity() ``` @@ -6720,7 +5225,7 @@ client.users.set_default_payment_method(
-**payment_method_id:** `str` +**range:** `typing.Optional[UsageActivityApiV1UsageActivityGetRequestRange]` — Rolling local calendar-day range.
@@ -6728,69 +5233,51 @@ client.users.set_default_payment_method(
-**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. +**type:** `typing.Optional[UsageActivityAction]` — Filter by usage activity type.
- -
+
+
+**user_id:** `typing.Optional[str]` — Filter by actor user id. +
-
-
client.users.get_my_credits() -> ApiResponseBalanceResponse
-#### 📝 Description - -
-
+**limit:** `typing.Optional[int]` + +
+
-Return the current user's credit balance + monthly grant + period anchor. - -Free-tier users have no Subscription row; the response falls back to the -canonical FREE Plan (1000 credits/mo, calendar-month boundary). -
-
+**cursor:** `typing.Optional[str]` — Opaque pagination cursor. +
-#### 🔌 Usage - -
-
-
-```python -from onepin import OnePinClient -from onepin.environment import OnePinClientEnvironment - -client = OnePinClient( - token="", - environment=OnePinClientEnvironment.PROD, -) - -client.users.get_my_credits() - -``` -
-
+**timezone:** `typing.Optional[str]` — IANA timezone for local day bucketing. +
-#### ⚙️ Parameters -
+**workspace_id:** `typing.Optional[str]` + +
+
+
@@ -6806,7 +5293,8 @@ client.users.get_my_credits()
-
client.users.get_my_plan_limits() -> ApiResponsePlanLimits +## users +
client.users.get_my_credits() -> ApiResponseBalanceResponse
@@ -6818,7 +5306,16 @@ client.users.get_my_credits()
-Return the typed plan limits for the current user (FE plan-card UI consumer). +Return the caller's current credit balance and billing period details. + +`balance` is the authoritative gate value: use it to decide whether to +attempt a workflow run. `remaining` is a display convenience derived from +settled ledger entries and may temporarily exceed `balance` while a workflow +run holds an open reserve. `used` reflects credits consumed in the current +billing period. `plan_grant` is the total monthly credit allowance for the +caller's plan, enabling a "X / Y used" display. `period_start` and +`period_end` mark the boundaries of the current billing window; free-tier +callers use a calendar-month boundary.
@@ -6841,7 +5338,7 @@ client = OnePinClient( environment=OnePinClientEnvironment.PROD, ) -client.users.get_my_plan_limits() +client.users.get_my_credits() ``` @@ -6869,7 +5366,7 @@ client.users.get_my_plan_limits()
-
client.users.list_invoices(...) -> ApiResponseInvoiceListResponse +
client.users.get_my_plan_limits() -> ApiResponsePlanLimits
@@ -6881,7 +5378,14 @@ client.users.get_my_plan_limits()
-List invoices for the current user. +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.
@@ -6904,7 +5408,7 @@ client = OnePinClient( environment=OnePinClientEnvironment.PROD, ) -client.users.list_invoices() +client.users.get_my_plan_limits() ``` @@ -6920,22 +5424,6 @@ client.users.list_invoices()
-**limit:** `typing.Optional[int]` - -
-
- -
-
- -**starting_after:** `typing.Optional[str]` - -
-
- -
-
- **request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration.
@@ -6960,7 +5448,11 @@ client.users.list_invoices()
-Get the current user's email notification preferences. +Return the caller's current email notification settings. + +Each boolean field corresponds to a notification category. `true` means the +caller will receive that email; `false` means they have opted out. Use +`PATCH /me/notification-preferences` to change individual preferences.
@@ -7023,7 +5515,11 @@ client.users.get_current_notification_preferences()
-Partially update the current user's email notification preferences. +Partially update the caller's email notification preferences. + +Send only the fields you want to change; omitted fields are left unchanged. +All provided fields must be boolean — explicit `null` values are rejected +with a 422. Returns the full updated preference object.
@@ -7062,7 +5558,7 @@ client.users.update_current_notification_preferences()
-**completed_generation_email:** `typing.Optional[bool]` +**completed_generation_email:** `typing.Optional[bool]` — Set to true to enable or false to disable completion emails. Omit to leave unchanged.
@@ -7070,7 +5566,7 @@ client.users.update_current_notification_preferences()
-**failed_generation_email:** `typing.Optional[bool]` +**failed_generation_email:** `typing.Optional[bool]` — Set to true to enable or false to disable failure emails. Omit to leave unchanged.
@@ -7102,12 +5598,13 @@ client.users.update_current_notification_preferences()
-List templates the current user created in the current workspace. +List workflow templates created by the caller in the current workspace. -Scoped on both `(workspace_id, created_by)` so when workspaces become -multi-user this endpoint keeps returning only the caller's own rows — -other workspace members' public/starter templates surface via the -gallery endpoint (`GET /api/v1/templates`) instead. +Returns only templates owned by the caller; templates shared by other +workspace members or platform starter templates are not included — use +`GET /api/v1/templates` for the full gallery. Supports offset-based +pagination via `offset` / `limit`. Combine `category`, `search`, and +`favorites_only` to narrow results; multiple `category` values are OR'd.
@@ -7154,7 +5651,7 @@ client.users.list_my_templates()
-**search:** `typing.Optional[str]` +**search:** `typing.Optional[str]` — Full-text search against template name and description.
@@ -7162,7 +5659,7 @@ client.users.list_my_templates()
-**sort:** `typing.Optional[ListMyTemplatesApiV1UsersMeTemplatesGetRequestSort]` +**sort:** `typing.Optional[ListMyTemplatesApiV1UsersMeTemplatesGetRequestSort]` — Sort order: `recent` (last updated), `name` (A–Z), or `uses` (most used).
@@ -7170,7 +5667,7 @@ client.users.list_my_templates()
-**offset:** `typing.Optional[int]` +**offset:** `typing.Optional[int]` — Number of results to skip for pagination.
@@ -7178,7 +5675,7 @@ client.users.list_my_templates()
-**limit:** `typing.Optional[int]` +**limit:** `typing.Optional[int]` — Maximum number of results to return (1–100).
@@ -7229,18 +5726,37 @@ client.users.list_my_templates() List workflows in the current workspace. -Multi-sort: `sort` and `order` are parallel lists. +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. + +**Status filter:** `status` narrows by the UI-derived state of the workflow's +most recent run. `completed` matches only workflows whose latest run succeeded +(completed-only), and `failed` matches only workflows whose latest run failed — +the two buckets are disjoint. A workflow whose latest run was `cancelled` matches +neither bucket and surfaces only in the unfiltered list. `running` matches active +(running or paused) workflows. `draft` matches workflows with no runs yet. +`paused` is accepted but currently returns no results. + +**Multi-sort:** `sort` and `order` are parallel query lists. `?sort=runs_count&sort=name&order=desc&order=asc` orders primarily by -runs_count DESC, secondarily by name ASC. When `order` is shorter than -`sort`, missing entries default per-field: -`name=asc, updated_at=desc, runs_count=desc`. When `sort` is omitted, -list defaults to `updated_at DESC`. Every sort path appends -`Workflow.id ASC` as a deterministic tiebreaker for pagination stability. +`runs_count DESC`, then by `name ASC`. When `order` has fewer entries than +`sort`, missing positions use per-field defaults (`name=asc`, +`updated_at=desc`, `runs_count=desc`). Omitting `sort` defaults to +`updated_at DESC`. A stable `id ASC` tiebreaker is always appended so +offset/limit pagination is consistent when sort keys tie. + +**Date range:** `last_run_after` / `last_run_before` filter by the time of +the most recent run. Both must be ISO 8601 with a UTC offset; a naive +datetime returns 422. An inverted range (`after > before`) also returns 422. + +**Failure history:** `has_failed_run` is orthogonal to `status`. `status` +keys off the latest run only; `has_failed_run=true` matches workflows with a +`failed` run *anywhere* in their history, so a workflow whose latest run +completed still matches if an earlier run failed. It composes with the other +filters (AND). `cancelled` runs do not count as failures. -`paused` is accepted but currently returns an empty result because -backend pause state is not implemented. `last_run_status` is the raw -RunStatus value, including values like `pending` or `cancelled`, and -pagination `total` is the filtered total for the current query. +`pagination.total` reflects the filtered count for the current query.
@@ -7279,7 +5795,7 @@ client.workflows.list()
-**status:** `typing.Optional[WorkflowListStatus]` — UI workflow status filter. `completed` means FINISHED — it matches workflows whose latest run is completed, failed, or cancelled, so it overlaps `failed` on failed runs. `paused` is accepted for forward compatibility and currently returns no rows. +**status:** `typing.Optional[WorkflowListStatus]` — UI workflow status filter. `completed` matches workflows whose latest run succeeded (completed-only); `failed` matches failed-only — the two are disjoint. A workflow whose latest run was cancelled matches neither and appears only in the unfiltered list. `paused` is accepted for forward compatibility and currently returns no rows.
@@ -7311,7 +5827,7 @@ client.workflows.list()
-**last_run_after:** `typing.Optional[datetime.datetime]` — Filter workflows whose last_run_at is at or after this ISO datetime. +**last_run_after:** `typing.Optional[datetime.datetime]` — Filter workflows whose last_run_at is at or after this ISO datetime (ISO 8601 with UTC offset required).
@@ -7319,7 +5835,7 @@ client.workflows.list()
-**last_run_before:** `typing.Optional[datetime.datetime]` — Filter workflows whose last_run_at is at or before this ISO datetime. +**last_run_before:** `typing.Optional[datetime.datetime]` — Filter workflows whose last_run_at is at or before this ISO datetime (ISO 8601 with UTC offset required).
@@ -7327,7 +5843,7 @@ client.workflows.list()
-**offset:** `typing.Optional[int]` +**has_failed_run:** `typing.Optional[bool]` — Filter by failure history — ORTHOGONAL to `status` (which is latest-run based). `true` returns only workflows with at least one run that ended in `failed` state anywhere in their history; a workflow whose latest run succeeded still matches if an earlier run failed. `false` returns only workflows that have never had a failed run. Composes (ANDs) with `status`/`search`/date filters. `cancelled` runs are not treated as failures.
@@ -7335,7 +5851,15 @@ client.workflows.list()
-**limit:** `typing.Optional[int]` +**offset:** `typing.Optional[int]` — Zero-based pagination offset. + +
+
+ +
+
+ +**limit:** `typing.Optional[int]` — Maximum items to return (1–100).
@@ -7375,7 +5899,16 @@ client.workflows.list()
-Create a workflow. +Create a new workflow in the current workspace. + +Validates the workflow `definition` (graph structure, node types, edge +connectivity) before persisting. Returns 422 with structured details if +the definition fails validation. Requires at least `editor` role in the +workspace; viewers cannot create workflows. + +The `definition` contains a `graph` (nodes and edges) and an `execution` +block (ordered step list and execution params). Omitting `definition` +creates a workflow with an empty graph that can be edited later.
@@ -7416,7 +5949,7 @@ client.workflows.create_workflow(
-**name:** `str` +**name:** `str` — Human-readable workflow name (1–200 characters, non-blank).
@@ -7432,7 +5965,7 @@ client.workflows.create_workflow(
-**description:** `typing.Optional[str]` +**description:** `typing.Optional[str]` — Optional description shown in the workflow list (max 5000 characters).
@@ -7440,7 +5973,7 @@ client.workflows.create_workflow(
-**definition:** `typing.Optional[WorkflowDefinitionInput]` +**definition:** `typing.Optional[WorkflowDefinitionInput]` — Graph and execution config. Omit to create an empty workflow.
@@ -7472,7 +6005,16 @@ client.workflows.create_workflow(
-Fetch a workflow by id. +Fetch a single workflow by ID. + +Returns the full workflow including its `definition` (graph nodes/edges and +execution config), aggregate run stats, and the latest run status. The +`definition` is returned with any backwards-compatible config migrations +applied, so node configs always reflect the current schema even if the +workflow was saved with an older version. + +Use `GET /workflows` to list multiple workflows without fetching their +full definitions.
@@ -7553,7 +6095,13 @@ client.workflows.get(
-Update a workflow. +Replace a workflow's name, description, and definition (full update). + +All fields in the request body are required. The `definition` is +validated before persisting; an invalid graph returns 422. Existing runs +are not affected — each run captures a `definition_snapshot` at start +time. Requires at least `editor` role. Use `PATCH` to update only +specific fields without supplying the full definition.
@@ -7604,7 +6152,7 @@ client.workflows.update_workflow(
-**name:** `str` +**name:** `str` — Human-readable workflow name (1–200 characters, non-blank).
@@ -7612,7 +6160,7 @@ client.workflows.update_workflow(
-**definition:** `WorkflowDefinitionInput` +**definition:** `WorkflowDefinitionInput` — Full replacement graph and execution config. Must be valid.
@@ -7628,7 +6176,7 @@ client.workflows.update_workflow(
-**description:** `typing.Optional[str]` +**description:** `typing.Optional[str]` — Optional description (max 5000 characters). Pass null to clear.
@@ -7660,7 +6208,12 @@ client.workflows.update_workflow(
-Soft-delete a workflow. +Delete a workflow and hide it from all list and get endpoints. + +The workflow is soft-deleted: its data (including runs and their outputs) +is retained for audit and GDPR-purge purposes but is no longer accessible +via the API. Subsequent `GET`, `PUT`, `PATCH`, or run requests on the +same ID return 404. Requires at least `editor` role.
@@ -7741,7 +6294,14 @@ client.workflows.delete_workflow(
-Partially update a workflow. Only fields present in the body are applied. +Partially update a workflow — only supplied fields are changed. + +Any combination of `name`, `description`, and `definition` may be +included; omitted fields are left unchanged. At least one field must be +present (empty body returns 422). If `definition` is provided it is fully +validated; an invalid graph returns 422. Requires at least `editor` role. + +Use `PUT` when replacing the full workflow definition in one operation.
@@ -7798,7 +6358,7 @@ client.workflows.patch_workflow(
-**name:** `typing.Optional[str]` +**name:** `typing.Optional[str]` — New workflow name (1–200 characters). Omit to leave unchanged.
@@ -7806,7 +6366,7 @@ client.workflows.patch_workflow(
-**description:** `typing.Optional[str]` +**description:** `typing.Optional[str]` — New description (max 5000 characters). Omit to leave unchanged; pass null to clear.
@@ -7814,7 +6374,7 @@ client.workflows.patch_workflow(
-**definition:** `typing.Optional[WorkflowDefinitionInput]` +**definition:** `typing.Optional[WorkflowDefinitionInput]` — Replacement graph and execution config. Omit to leave unchanged; must be valid if supplied.
@@ -7847,6 +6407,10 @@ client.workflows.patch_workflow(
List confirmed uploads attached to a workflow. + +Returns only uploads that have been confirmed (fully transferred and +committed to the workflow). In-progress or abandoned uploads are excluded. +Each item includes a short-lived download URL for the uploaded file.
@@ -7895,7 +6459,7 @@ client.workflows.list_workflow_uploads(
-**offset:** `typing.Optional[int]` +**offset:** `typing.Optional[int]` — Zero-based pagination offset.
@@ -7903,7 +6467,7 @@ client.workflows.list_workflow_uploads(
-**limit:** `typing.Optional[int]` +**limit:** `typing.Optional[int]` — Maximum items to return (1–100).
@@ -7943,7 +6507,12 @@ client.workflows.list_workflow_uploads(
-Estimate workflow credits without creating a run. +Estimate the credit cost of running a workflow without creating a run. + +Computes a breakdown of expected credits per node type based on the +workflow's current definition. No run is created, no credits are charged, +and no side effects occur. Equivalent to `POST /runs/preview`; prefer that +path in new integrations as it is co-located with the run lifecycle.
@@ -8024,7 +6593,12 @@ client.workflows.estimate_workflow(
-Estimate workflow run credits without creating a run. +Dry-run credit estimate for a workflow — no run is created. + +Returns a per-node-type credit breakdown based on the workflow's current +definition. No run is enqueued, no credits are charged, and the workflow +state is not modified. Use this before calling `POST /runs` to confirm +the expected cost. Equivalent to `POST /estimate`.
@@ -8105,12 +6679,22 @@ client.workflows.preview_run(
-Aggregate run-status counts plus pass_rate and average_duration_seconds. +Aggregate run statistics for a workflow over an optional date window. + +Returns per-status counts (`completed`, `failed`, `cancelled`, `pending`, +`running`, `paused`) plus two derived metrics: + +- `pass_rate`: `completed / (completed + failed + cancelled)`. `null` when + there are no terminal runs in the window. +- `average_duration_seconds`: mean of `completed_at - started_at` over + successfully completed runs only. `null` when no runs have completed. -``pass_rate = completed / (completed + failed + cancelled)``; -``None`` when no terminal runs. -``average_duration_seconds = mean(completed_at - started_at)`` over -completed runs only; ``None`` when there are zero completed runs. +**Date range:** `from` / `to` filter by `created_at`. Both must be ISO 8601 +with a UTC offset; a naive datetime returns 422. An inverted range +(`from > to`) also returns 422. Omit both to aggregate over all runs. + +Use `GET /runs` with `?status=` filters for individual run details; this +endpoint is best for dashboard-style health metrics.
@@ -8159,7 +6743,7 @@ client.workflows.runs_summary(
-**from:** `typing.Optional[datetime.datetime]` — Filter runs by created_at >= this ISO datetime. +**from:** `typing.Optional[datetime.datetime]` — Filter runs by created_at >= this ISO datetime (ISO 8601 with UTC offset required).
@@ -8167,7 +6751,7 @@ client.workflows.runs_summary(
-**to:** `typing.Optional[datetime.datetime]` — Filter runs by created_at <= this ISO datetime. +**to:** `typing.Optional[datetime.datetime]` — Filter runs by created_at <= this ISO datetime (ISO 8601 with UTC offset required).
@@ -8207,7 +6791,23 @@ client.workflows.runs_summary(
-List steps for a workflow run. +List per-node execution steps for a workflow run. + +Returns one entry per node execution attempt, ordered by execution sequence. +Each step includes the node type, status, iteration number (for nodes that +are retried), start/completion timestamps, and the node's `result` output. + +For audio output nodes, `result` is hydrated with short-lived `playback_url` +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. + +For a higher-level view with aggregated metrics (pass rates, audio duration +by language), use `GET /runs/{run_id}/overview`. For paginated, grouped +script+audio rows suitable for a data table, use `GET /runs/{run_id}/data`.
@@ -8298,6 +6898,21 @@ client.workflows.get_run_steps(
Fetch server-computed overview aggregates for a workflow run. + +Returns structured metric sections (e.g. audio duration totals, validation +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. + +This endpoint is best suited for a summary/results view after a run +completes. It differs from the other run sub-resources as follows: + +- `GET /runs/{run_id}` — full run record including the raw definition snapshot. +- `GET /runs/{run_id}/status` — volatile status fields only; for polling. +- `GET /runs/{run_id}/steps` — flat per-node step log with audio playback URLs. +- `GET /runs/{run_id}/data` — paginated script+audio rows for a data table. +- `GET /runs/{run_id}/overview` (this endpoint) — pre-aggregated metrics and + node state map for a dashboard/overview panel.
@@ -8387,10 +7002,26 @@ client.workflows.get_run_overview(
-Fetch normalized grouped rows/cards for the run detail Data tab. +Paginated script-and-audio data rows for a completed workflow run. -`pagination.total` is search-scoped and language-independent; language -filters only card lists, so returned rows may contain empty `cards`. +Returns grouped rows where each row represents one source script line. +Within each row, `cards` contain the per-language audio outputs, per-card +validation scores (word accuracy, naturalness), and short-lived audio +`playback_url` values (valid for 15 minutes). + +**Filtering:** +- `search` narrows which rows are returned based on their source script text. +- `language` narrows the `cards` list within each returned row to a single + locale. Rows with no matching cards are still returned (with empty `cards`), + and `pagination.total` always reflects the search-filtered row count + regardless of `language`. + +**Pagination:** `pagination.total` is scoped to the `search` filter only. + +Response includes a `partial` field indicating whether any data is still +being computed (e.g. audio not yet generated, validation not yet scored). +This endpoint sets `Cache-Control: no-store` because playback URLs are +short-lived and data may change while a run is still in progress.
@@ -8448,7 +7079,7 @@ client.workflows.get_run_data(
-**search:** `typing.Optional[str]` — Case-insensitive search over visible grouped source/script text. +**search:** `typing.Optional[str]` — Case-insensitive search over the source/script text of each row.
@@ -8456,7 +7087,7 @@ client.workflows.get_run_data(
-**language:** `typing.Optional[str]` — Exact full-locale card filter. `_` is normalized to `-`; filtering cards preserves row visibility and pagination.total remains language-independent, so rows may return empty cards. +**language:** `typing.Optional[str]` — Exact full-locale code to filter cards within each row (e.g. `en-US`). `_` is normalized to `-`. Filtering is card-level only — rows remain visible even when all their cards are filtered out, and `pagination.total` is unaffected.
@@ -8464,7 +7095,7 @@ client.workflows.get_run_data(
-**offset:** `typing.Optional[int]` +**offset:** `typing.Optional[int]` — Zero-based pagination offset.
@@ -8472,7 +7103,7 @@ client.workflows.get_run_data(
-**limit:** `typing.Optional[int]` +**limit:** `typing.Optional[int]` — Maximum rows to return (1–100).
@@ -8512,7 +7143,20 @@ client.workflows.get_run_data(
-Create a temporary download URL for a workflow run export. +Create a temporary download URL for a complete workflow run export. + +Returns a pre-signed URL pointing to a ZIP archive containing all audio +output files produced by the run. The URL is valid for 15 minutes +(`expires_at`). The archive is generated on first request and cached for +subsequent calls; re-calling this endpoint before expiry returns a new +URL for the same cached archive. + +Only available for runs in `completed` status — returns 409 for runs that +are still active or ended in `failed`/`cancelled`. Returns 404 if the run +produced no audio files. + +To download output from a single output node rather than the whole run, +use `GET /runs/{run_id}/nodes/{node_id}/download`.
@@ -8602,7 +7246,20 @@ client.workflows.download_run(
-Create a temporary download URL for a node-level workflow export. +Create a temporary download URL for a single output node's audio export. + +Returns a pre-signed URL for a ZIP archive containing the audio files +produced by one specific output node within the run. Useful when a +workflow has multiple output nodes and the caller wants only one node's +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 +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 +archive, use `GET /runs/{run_id}/download` instead.
@@ -8701,10 +7358,17 @@ client.workflows.download_run_node(
-Pause a workflow run. +Pause an active workflow run at the next safe checkpoint. + +For a running run, the current wave of parallel nodes is allowed to finish +before the run parks (in-flight work is preserved, not abandoned). For a +pending run that has not yet started, it parks immediately. The run +transitions to `paused` status once drained; during the drain period, +`status` remains `running` with `pause_requested_at` set. -A running run finishes its current wave (preserving in-flight work) and parks at the -next wave boundary; a pending run parks immediately. Fire-and-forget and idempotent. +The operation is idempotent: pausing an already-paused run returns it +unchanged. A paused run can be resumed via `POST /runs/{run_id}/resume` +or permanently stopped via `POST /runs/{run_id}/cancel`.
@@ -8796,8 +7460,15 @@ client.workflows.pause_run( Resume a paused workflow run from its last completed wave. -Best-effort: if the workflow already has another active run, or the caller is at their -concurrent-run limit, the run stays paused and a 409 explains why (retry later). +Transitions the run from `paused` back to `running` and schedules +execution to continue from where it left off — no nodes that already +completed are re-executed. + +Returns 409 if the workspace already has another active run for this +workflow, or if the caller is at the concurrent-run limit. In that case +the run stays `paused` and the caller can retry later. Only runs in +`paused` status can be resumed; attempting to resume a `running`, +`completed`, `failed`, or `cancelled` run returns 409. @@ -8887,7 +7558,12 @@ client.workflows.resume_run(
-Duplicate a workflow. +Create a copy of an existing workflow in the same workspace. + +The new workflow inherits the source's `name` (suffixed with " (Copy)"), +`description`, and `definition`. Runs from the original workflow are not +copied — the duplicate starts with zero runs. Requires at least `editor` +role. Returns 201 with the new workflow on success.
@@ -8969,12 +7645,21 @@ client.workflows.duplicate_workflow(
-List runs for a workflow. +List runs for a workflow, newest first by default. + +Returns a counted, paginated list of runs for the specified workflow. +Each item includes the run's status, step progress (`total_steps`, +`finished_steps`), credit usage, and the actor who triggered it. + +**Status filter:** Pass `?status=completed,failed` to OR-match multiple +statuses. Values must be the raw lowercase RunStatus strings. Unknown values +or empty tokens (e.g. `a,,b`) return 422. -Tiebreaker is always ``id ASC`` so offset/limit pagination is stable when -primary sort keys tie. ``status`` accepts comma-separated raw RunStatus -values; unknown values return 422. ``search`` matches the triggering -user's display name (full name, falling back to email). +**Pagination:** `pagination.total` reflects the filtered count. Sort +tiebreaks always append `id ASC` for stable offset/limit pagination. + +For aggregate statistics (pass rate, average duration) across all runs, +use `GET /runs/summary` instead.
@@ -9023,7 +7708,7 @@ client.workflows.runs.list(
-**offset:** `typing.Optional[int]` +**offset:** `typing.Optional[int]` — Zero-based pagination offset.
@@ -9031,7 +7716,7 @@ client.workflows.runs.list(
-**limit:** `typing.Optional[int]` +**limit:** `typing.Optional[int]` — Maximum items to return (1–100).
@@ -9039,7 +7724,7 @@ client.workflows.runs.list(
-**status:** `typing.Optional[str]` — Comma-separated raw RunStatus values (e.g. `completed,failed`). Values are case-sensitive lowercase. Multiple values OR-match. Empty tokens (e.g. `a,,b`) and unknown values return 422. +**status:** `typing.Optional[str]` — Comma-separated raw RunStatus values (e.g. `completed,failed`). Values are case-sensitive lowercase: `pending`, `running`, `completed`, `failed`, `cancelled`, `paused`. Multiple values OR-match. Empty tokens (e.g. `a,,b`) and unknown values return 422.
@@ -9047,7 +7732,7 @@ client.workflows.runs.list(
-**search:** `typing.Optional[str]` — Case-insensitive search over triggering user's display name and email. +**search:** `typing.Optional[str]` — Case-insensitive search over the triggering user's display name (falls back to email).
@@ -9055,7 +7740,7 @@ client.workflows.runs.list(
-**sort:** `typing.Optional[ListRunsRequestSort]` — Sort field: created_at | started_at | completed_at | status. +**sort:** `typing.Optional[ListRunsRequestSort]` — Sort field: `created_at` | `started_at` | `completed_at` | `status`. Defaults to `created_at`.
@@ -9063,7 +7748,7 @@ client.workflows.runs.list(
-**order:** `typing.Optional[ListRunsRequestOrder]` — asc or desc. +**order:** `typing.Optional[ListRunsRequestOrder]` — `asc` or `desc`. Defaults to `desc`.
@@ -9103,7 +7788,20 @@ client.workflows.runs.list(
-Start a workflow run. +Start a new execution of a workflow (202 Accepted). + +Enqueues the workflow for asynchronous execution and returns the newly +created run in `pending` or `running` status. The run progresses through +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. + +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. + +Returns 409 if the workspace is at its concurrent-run limit or another +run for this workflow is already active.
@@ -9184,11 +7882,18 @@ client.workflows.runs.start(
-Fetch a workflow run by id. +Fetch full detail for a single workflow run. -Includes `definition_snapshot` — the graph/execution config captured -when the run started, returned raw (no config migrations applied) so -it reflects the workflow exactly as it existed for this run. +Returns all run fields plus `definition_snapshot` — the graph and +execution config captured at the moment the run started. The snapshot is +returned raw (no config migrations applied), so it faithfully represents +the workflow as it existed for this specific execution even if the +workflow definition has since been edited. + +This is the heaviest run endpoint. For progress polling, use the lighter +`GET /runs/{run_id}/status` which omits the snapshot. For aggregated +visual metrics, use `GET /runs/{run_id}/overview`. For the per-node step +log with audio playback URLs, use `GET /runs/{run_id}/steps`.
@@ -9278,12 +7983,23 @@ client.workflows.runs.get(
-Lightweight run status for polling. +Lightweight run status for progress polling. + +Returns only the volatile, frequently-changing fields: `status`, step +counts (`total_steps`, `finished_steps`), timestamps (`started_at`, +`completed_at`, `paused_at`, `pause_requested_at`), `usage_summary`, +`error`, and `has_export`. The definition snapshot is intentionally +omitted to keep response size small. -Returns only the volatile run state (status, step counts, timestamps, -usage_summary, error, has_export) — no graph snapshot. Use this for -progress polling; call `GET /runs/{run_id}` once to load the immutable -definition snapshot. +Recommended polling pattern: call `GET /runs/{run_id}` once after +starting a run to load the immutable definition snapshot and initial +metadata, then poll this endpoint until `status` reaches a terminal value +(`completed`, `failed`, or `cancelled`). `has_export` becoming `true` +signals that a download is ready via `GET /runs/{run_id}/download`. + +The transient `pausing` state is observable here: `status == "running"` +with `pause_requested_at` set means a pause is in progress but the +current wave has not yet finished draining.
@@ -9373,7 +8089,19 @@ client.workflows.runs.status(
-Cancel a running workflow run. +Cancel an active workflow run. + +Immediately marks the run as `cancelled` and stops any further processing. +In-flight work at the current node may be abandoned mid-execution. The +operation is idempotent: cancelling an already-cancelled run returns the +run unchanged without error. + +Only runs in `pending`, `running`, or `paused` status can be cancelled. +Runs that have already reached a terminal state (`completed`, `failed`, +`cancelled`) return 409. + +Unlike `pause`, cancel is permanent — a cancelled run cannot be resumed. +Use `pause` if you intend to continue the run later.
diff --git a/src/onepin/templates/client.py b/src/onepin/templates/client.py index bab92d2..5e47f61 100644 --- a/src/onepin/templates/client.py +++ b/src/onepin/templates/client.py @@ -45,27 +45,35 @@ def list( request_options: typing.Optional[RequestOptions] = None, ) -> ApiListResponseTemplateOut: """ - List live published templates across workspaces (gallery). + Browse the public template gallery across all workspaces. - Authenticated but not workspace-scoped — the gallery is cross-workspace - by design (published rows from any workspace). Does not require - `X-Workspace-Id`, so a freshly signed-up user without a workspace can - still browse templates. + Returns only templates that have an active published snapshot (`is_public=true`, + `published_definition` set, not unpublished). Results come from any workspace — + the gallery is intentionally cross-workspace so callers can discover shared + starting points regardless of their own workspace membership. - Dual-auth: Clerk JWT or `op_live_*` API key (scope `templates:read`). + Does not require `X-Workspace-Id`, so callers without a workspace (e.g. during + onboarding) can still browse. The response reflects the published snapshot for + each row, not any unpublished draft edits. + + Dual-auth: Bearer JWT or API key (scope `templates:read`). Parameters ---------- category : typing.Optional[typing.Sequence[TemplateCategory]] - Repeat for OR, e.g. ?category=media&category=creative + Filter by category. Repeat the parameter for OR logic, e.g. `?category=media&category=creative`. search : typing.Optional[str] + Full-text search over template name and description. sort : typing.Optional[ListTemplatesRequestSort] + Sort order: `recent` (last published), `popular` (most cloned), or `name` (alphabetical). offset : typing.Optional[int] + Zero-based offset for page navigation. limit : typing.Optional[int] + Maximum number of templates to return (1–100). favorites_only : typing.Optional[bool] @@ -108,25 +116,35 @@ def create_template( request_options: typing.Optional[RequestOptions] = None, ) -> ApiResponseTemplateOut: """ - Create a new workflow template in the current workspace. + Create a reusable workflow template in the current workspace. + + Templates are workspace-private on creation (`is_public=false`, `is_starter=false`). + The full `WorkflowDefinition` (graph + execution config) is validated at write + time — structural errors (duplicate node/edge IDs, port mismatches, etc.) surface + here rather than when a caller later clones the template into a workflow. + + Use this to capture a workflow configuration you intend to reuse or share. To + make a template available in the public gallery, an admin must mark it public + via the admin API. To create a runnable workflow from an existing template, + use `POST /templates/{id}/clone` instead. - The caller supplies a full `WorkflowDefinition` (graph + execution). - Save-time validation (`validate_definition_save`) mirrors the - `/api/v1/workflows` contract — duplicate node/edge IDs, port mismatches, - and other structural errors fail at write time rather than surfacing - when a caller later clones the template into a workflow. + Requires workspace `editor` role or higher. Parameters ---------- name : str + Display name for the template (1–200 characters, not blank). workspace_id : typing.Optional[str] description : typing.Optional[str] + Optional human-readable description shown in the gallery (max 2,000 characters). category : typing.Optional[TemplateCategory] + Optional category tag used for gallery filtering. definition : typing.Optional[WorkflowDefinitionInput] + Full workflow definition (graph + execution config). Validated at write time — structural errors are rejected with 422. request_options : typing.Optional[RequestOptions] Request-specific configuration. @@ -165,9 +183,16 @@ def get( request_options: typing.Optional[RequestOptions] = None, ) -> ApiResponseTemplateOut: """ - Fetch a template by id if visible (own, public, or starter). + Fetch a single template by ID. + + Returns the template if it is visible to the caller: templates owned by the + caller's workspace are returned with the live draft definition; public/starter + templates from other workspaces are returned with the published snapshot. + + Returns 404 for templates that exist but are not visible to the caller (not + owned, not public, not a starter) — same response as for a missing ID. - Dual-auth: Clerk JWT or `op_live_*` API key (scope `templates:read`). + Dual-auth: Bearer JWT or API key (scope `templates:read`). Parameters ---------- @@ -205,7 +230,18 @@ def delete_template( request_options: typing.Optional[RequestOptions] = None, ) -> ApiResponseDict: """ - Soft-delete a template. Owner only; starter templates are read-only. + Delete a template owned by the caller's workspace. + + The delete is a soft delete — the record is hidden from the gallery and + all visibility checks immediately, but is not physically removed. Any + workflows previously cloned from this template are unaffected; clone + creates an independent copy of the definition at clone time. + + Restrictions: + - Only the owning workspace may delete its templates (403 otherwise). + - Platform starter templates (`is_starter=true`) cannot be deleted (403). + + Requires workspace `editor` role or higher. Parameters ---------- @@ -249,10 +285,20 @@ def update_template( request_options: typing.Optional[RequestOptions] = None, ) -> ApiResponseTemplateOut: """ - Update a template. Owner only; starter templates are read-only. + Update a template owned by the caller's workspace. + + All fields are optional (omit to keep the stored value). When `definition` + is supplied it is a full replace — send the complete graph, not a partial + diff. Structural validation runs on write, same as `POST /templates`. - `definition` is full-replace (matches `WorkflowUpdate` — the FE sends the - entire graph back on save). Other fields are partial via `exclude_unset`. + Restrictions: + - Only the owning workspace may update its templates (403 otherwise). + - Platform starter templates (`is_starter=true`) are read-only via this + endpoint regardless of workspace ownership (403). + - Updates apply only to the draft/live definition; the published gallery + snapshot is not updated until an admin republishes. + + Requires workspace `editor` role or higher. Parameters ---------- @@ -307,7 +353,28 @@ def estimate_template( request_options: typing.Optional[RequestOptions] = None, ) -> ApiResponseTemplateEstimateResponse: """ - Return per-1,000-character pricing for a visible template snapshot. + Estimate the credit cost of running a workflow built from this template. + + Returns a per-unit pricing guide expressed in credits per + `unit_chars` input characters (default 1,000). Because the template does not + contain the caller's actual script, the estimate uses a synthetic fixed-length + input to compute a reproducible per-unit rate. Multiply by your expected + character count to project total cost. + + The response distinguishes variable costs (scale with script length, e.g. + synthesis) from fixed costs (apply once per run regardless of length). A + node-level breakdown is included so callers can see which processing steps + drive the cost. + + Results are cached against the template definition and current pricing rates. + `cache_status` indicates whether this response was served from cache (`hit`), + computed fresh (`miss`), or recomputed because the definition or rates changed + (`stale`). + + Visibility rules match `GET /templates/{id}` — own-workspace templates use the + draft definition; cross-workspace templates use the published snapshot. + + Dual-auth: Bearer JWT or API key (scope `templates:read`). Parameters ---------- @@ -348,16 +415,24 @@ def clone( request_options: typing.Optional[RequestOptions] = None, ) -> ApiResponseWorkflowOut: """ - Clone a template into a workflow in the caller's workspace. + Create a runnable workflow in the caller's workspace from a template. + + This is the primary way to use a template: it produces a new `Workflow` + owned by the caller's workspace, ready to accept scripts and run jobs. + + Use `body.name` to set the workflow name; omit it (or send blank/whitespace) + to get the default `"{template name} (Copy)"`. - Dual-auth: Clerk JWT or `op_live_*` API key (scope `workflows:write`). + Cross-workspace clones (gallery/starter templates) copy the published + snapshot so unpublished draft edits made by the template owner never leak to + other workspaces. Same-workspace clones copy the live draft definition. - Same-workspace clones use the live `definition` (owner authoring). - Cross-workspace clones use the `published_definition` snapshot to avoid - leaking unpublished draft edits. + Use `GET /templates/{id}/estimate` first to preview credit costs before + committing to a clone and run. Use `POST /workflows/{id}/duplicate` to copy + an existing workflow rather than starting from a template. - Resolved name: explicit `body.name` (stripped) OR fallback to - `"{source_name} (Copy)"`. + Requires workspace `editor` role or higher. + Dual-auth: Bearer JWT or API key (scope `workflows:write`). Parameters ---------- @@ -395,15 +470,17 @@ def favorite_template( self, template_id: str, *, request_options: typing.Optional[RequestOptions] = None ) -> ApiResponseTemplateOut: """ - Mark a visible template as a favorite for the current user. + Add a template to the current user's favorites. - Authenticated but not workspace-scoped — favorites are cross-workspace by - design for public/starter templates and for the caller's own private - templates when they still belong to that template's workspace. + Favorites are per-user, not per-workspace — the same favorite list is + visible regardless of which workspace the caller is currently acting in. + Any template visible to the caller (own workspace, public, or starter) can + be favorited. - Returns 404 when the template does not exist or is not visible to the - caller. This POST intentionally enumerates success/failure for the toggle - UX, while DELETE stays non-enumerating. + Returns 404 when the template does not exist or is not visible to the caller. + Calling this endpoint on an already-favorited template is idempotent (returns + 200 with the template). Use `DELETE /templates/{id}/favorite` to remove. + Does not require `X-Workspace-Id`. Parameters ---------- @@ -435,10 +512,10 @@ def unfavorite_template( self, template_id: str, *, request_options: typing.Optional[RequestOptions] = None ) -> ApiResponseDict: """ - Remove a template favorite for the current user. + Remove a template from the current user's favorites. - Deletion is intentionally non-enumerating: authenticated callers receive a - successful empty response whether the row or template exists. + Idempotent and non-enumerating: returns an empty success response whether + or not the favorite or the template exists. Does not require `X-Workspace-Id`. Parameters ---------- @@ -494,27 +571,35 @@ async def list( request_options: typing.Optional[RequestOptions] = None, ) -> ApiListResponseTemplateOut: """ - List live published templates across workspaces (gallery). + Browse the public template gallery across all workspaces. - Authenticated but not workspace-scoped — the gallery is cross-workspace - by design (published rows from any workspace). Does not require - `X-Workspace-Id`, so a freshly signed-up user without a workspace can - still browse templates. + Returns only templates that have an active published snapshot (`is_public=true`, + `published_definition` set, not unpublished). Results come from any workspace — + the gallery is intentionally cross-workspace so callers can discover shared + starting points regardless of their own workspace membership. - Dual-auth: Clerk JWT or `op_live_*` API key (scope `templates:read`). + Does not require `X-Workspace-Id`, so callers without a workspace (e.g. during + onboarding) can still browse. The response reflects the published snapshot for + each row, not any unpublished draft edits. + + Dual-auth: Bearer JWT or API key (scope `templates:read`). Parameters ---------- category : typing.Optional[typing.Sequence[TemplateCategory]] - Repeat for OR, e.g. ?category=media&category=creative + Filter by category. Repeat the parameter for OR logic, e.g. `?category=media&category=creative`. search : typing.Optional[str] + Full-text search over template name and description. sort : typing.Optional[ListTemplatesRequestSort] + Sort order: `recent` (last published), `popular` (most cloned), or `name` (alphabetical). offset : typing.Optional[int] + Zero-based offset for page navigation. limit : typing.Optional[int] + Maximum number of templates to return (1–100). favorites_only : typing.Optional[bool] @@ -565,25 +650,35 @@ async def create_template( request_options: typing.Optional[RequestOptions] = None, ) -> ApiResponseTemplateOut: """ - Create a new workflow template in the current workspace. + Create a reusable workflow template in the current workspace. + + Templates are workspace-private on creation (`is_public=false`, `is_starter=false`). + The full `WorkflowDefinition` (graph + execution config) is validated at write + time — structural errors (duplicate node/edge IDs, port mismatches, etc.) surface + here rather than when a caller later clones the template into a workflow. + + Use this to capture a workflow configuration you intend to reuse or share. To + make a template available in the public gallery, an admin must mark it public + via the admin API. To create a runnable workflow from an existing template, + use `POST /templates/{id}/clone` instead. - The caller supplies a full `WorkflowDefinition` (graph + execution). - Save-time validation (`validate_definition_save`) mirrors the - `/api/v1/workflows` contract — duplicate node/edge IDs, port mismatches, - and other structural errors fail at write time rather than surfacing - when a caller later clones the template into a workflow. + Requires workspace `editor` role or higher. Parameters ---------- name : str + Display name for the template (1–200 characters, not blank). workspace_id : typing.Optional[str] description : typing.Optional[str] + Optional human-readable description shown in the gallery (max 2,000 characters). category : typing.Optional[TemplateCategory] + Optional category tag used for gallery filtering. definition : typing.Optional[WorkflowDefinitionInput] + Full workflow definition (graph + execution config). Validated at write time — structural errors are rejected with 422. request_options : typing.Optional[RequestOptions] Request-specific configuration. @@ -630,9 +725,16 @@ async def get( request_options: typing.Optional[RequestOptions] = None, ) -> ApiResponseTemplateOut: """ - Fetch a template by id if visible (own, public, or starter). + Fetch a single template by ID. + + Returns the template if it is visible to the caller: templates owned by the + caller's workspace are returned with the live draft definition; public/starter + templates from other workspaces are returned with the published snapshot. + + Returns 404 for templates that exist but are not visible to the caller (not + owned, not public, not a starter) — same response as for a missing ID. - Dual-auth: Clerk JWT or `op_live_*` API key (scope `templates:read`). + Dual-auth: Bearer JWT or API key (scope `templates:read`). Parameters ---------- @@ -678,7 +780,18 @@ async def delete_template( request_options: typing.Optional[RequestOptions] = None, ) -> ApiResponseDict: """ - Soft-delete a template. Owner only; starter templates are read-only. + Delete a template owned by the caller's workspace. + + The delete is a soft delete — the record is hidden from the gallery and + all visibility checks immediately, but is not physically removed. Any + workflows previously cloned from this template are unaffected; clone + creates an independent copy of the definition at clone time. + + Restrictions: + - Only the owning workspace may delete its templates (403 otherwise). + - Platform starter templates (`is_starter=true`) cannot be deleted (403). + + Requires workspace `editor` role or higher. Parameters ---------- @@ -730,10 +843,20 @@ async def update_template( request_options: typing.Optional[RequestOptions] = None, ) -> ApiResponseTemplateOut: """ - Update a template. Owner only; starter templates are read-only. + Update a template owned by the caller's workspace. + + All fields are optional (omit to keep the stored value). When `definition` + is supplied it is a full replace — send the complete graph, not a partial + diff. Structural validation runs on write, same as `POST /templates`. - `definition` is full-replace (matches `WorkflowUpdate` — the FE sends the - entire graph back on save). Other fields are partial via `exclude_unset`. + Restrictions: + - Only the owning workspace may update its templates (403 otherwise). + - Platform starter templates (`is_starter=true`) are read-only via this + endpoint regardless of workspace ownership (403). + - Updates apply only to the draft/live definition; the published gallery + snapshot is not updated until an admin republishes. + + Requires workspace `editor` role or higher. Parameters ---------- @@ -796,7 +919,28 @@ async def estimate_template( request_options: typing.Optional[RequestOptions] = None, ) -> ApiResponseTemplateEstimateResponse: """ - Return per-1,000-character pricing for a visible template snapshot. + Estimate the credit cost of running a workflow built from this template. + + Returns a per-unit pricing guide expressed in credits per + `unit_chars` input characters (default 1,000). Because the template does not + contain the caller's actual script, the estimate uses a synthetic fixed-length + input to compute a reproducible per-unit rate. Multiply by your expected + character count to project total cost. + + The response distinguishes variable costs (scale with script length, e.g. + synthesis) from fixed costs (apply once per run regardless of length). A + node-level breakdown is included so callers can see which processing steps + drive the cost. + + Results are cached against the template definition and current pricing rates. + `cache_status` indicates whether this response was served from cache (`hit`), + computed fresh (`miss`), or recomputed because the definition or rates changed + (`stale`). + + Visibility rules match `GET /templates/{id}` — own-workspace templates use the + draft definition; cross-workspace templates use the published snapshot. + + Dual-auth: Bearer JWT or API key (scope `templates:read`). Parameters ---------- @@ -845,16 +989,24 @@ async def clone( request_options: typing.Optional[RequestOptions] = None, ) -> ApiResponseWorkflowOut: """ - Clone a template into a workflow in the caller's workspace. + Create a runnable workflow in the caller's workspace from a template. + + This is the primary way to use a template: it produces a new `Workflow` + owned by the caller's workspace, ready to accept scripts and run jobs. + + Use `body.name` to set the workflow name; omit it (or send blank/whitespace) + to get the default `"{template name} (Copy)"`. - Dual-auth: Clerk JWT or `op_live_*` API key (scope `workflows:write`). + Cross-workspace clones (gallery/starter templates) copy the published + snapshot so unpublished draft edits made by the template owner never leak to + other workspaces. Same-workspace clones copy the live draft definition. - Same-workspace clones use the live `definition` (owner authoring). - Cross-workspace clones use the `published_definition` snapshot to avoid - leaking unpublished draft edits. + Use `GET /templates/{id}/estimate` first to preview credit costs before + committing to a clone and run. Use `POST /workflows/{id}/duplicate` to copy + an existing workflow rather than starting from a template. - Resolved name: explicit `body.name` (stripped) OR fallback to - `"{source_name} (Copy)"`. + Requires workspace `editor` role or higher. + Dual-auth: Bearer JWT or API key (scope `workflows:write`). Parameters ---------- @@ -900,15 +1052,17 @@ async def favorite_template( self, template_id: str, *, request_options: typing.Optional[RequestOptions] = None ) -> ApiResponseTemplateOut: """ - Mark a visible template as a favorite for the current user. + Add a template to the current user's favorites. - Authenticated but not workspace-scoped — favorites are cross-workspace by - design for public/starter templates and for the caller's own private - templates when they still belong to that template's workspace. + Favorites are per-user, not per-workspace — the same favorite list is + visible regardless of which workspace the caller is currently acting in. + Any template visible to the caller (own workspace, public, or starter) can + be favorited. - Returns 404 when the template does not exist or is not visible to the - caller. This POST intentionally enumerates success/failure for the toggle - UX, while DELETE stays non-enumerating. + Returns 404 when the template does not exist or is not visible to the caller. + Calling this endpoint on an already-favorited template is idempotent (returns + 200 with the template). Use `DELETE /templates/{id}/favorite` to remove. + Does not require `X-Workspace-Id`. Parameters ---------- @@ -948,10 +1102,10 @@ async def unfavorite_template( self, template_id: str, *, request_options: typing.Optional[RequestOptions] = None ) -> ApiResponseDict: """ - Remove a template favorite for the current user. + Remove a template from the current user's favorites. - Deletion is intentionally non-enumerating: authenticated callers receive a - successful empty response whether the row or template exists. + Idempotent and non-enumerating: returns an empty success response whether + or not the favorite or the template exists. Does not require `X-Workspace-Id`. Parameters ---------- diff --git a/src/onepin/templates/raw_client.py b/src/onepin/templates/raw_client.py index c761c86..7fce3d8 100644 --- a/src/onepin/templates/raw_client.py +++ b/src/onepin/templates/raw_client.py @@ -42,27 +42,35 @@ def list( request_options: typing.Optional[RequestOptions] = None, ) -> HttpResponse[ApiListResponseTemplateOut]: """ - List live published templates across workspaces (gallery). + Browse the public template gallery across all workspaces. - Authenticated but not workspace-scoped — the gallery is cross-workspace - by design (published rows from any workspace). Does not require - `X-Workspace-Id`, so a freshly signed-up user without a workspace can - still browse templates. + Returns only templates that have an active published snapshot (`is_public=true`, + `published_definition` set, not unpublished). Results come from any workspace — + the gallery is intentionally cross-workspace so callers can discover shared + starting points regardless of their own workspace membership. - Dual-auth: Clerk JWT or `op_live_*` API key (scope `templates:read`). + Does not require `X-Workspace-Id`, so callers without a workspace (e.g. during + onboarding) can still browse. The response reflects the published snapshot for + each row, not any unpublished draft edits. + + Dual-auth: Bearer JWT or API key (scope `templates:read`). Parameters ---------- category : typing.Optional[typing.Sequence[TemplateCategory]] - Repeat for OR, e.g. ?category=media&category=creative + Filter by category. Repeat the parameter for OR logic, e.g. `?category=media&category=creative`. search : typing.Optional[str] + Full-text search over template name and description. sort : typing.Optional[ListTemplatesRequestSort] + Sort order: `recent` (last published), `popular` (most cloned), or `name` (alphabetical). offset : typing.Optional[int] + Zero-based offset for page navigation. limit : typing.Optional[int] + Maximum number of templates to return (1–100). favorites_only : typing.Optional[bool] @@ -128,25 +136,35 @@ def create_template( request_options: typing.Optional[RequestOptions] = None, ) -> HttpResponse[ApiResponseTemplateOut]: """ - Create a new workflow template in the current workspace. + Create a reusable workflow template in the current workspace. + + Templates are workspace-private on creation (`is_public=false`, `is_starter=false`). + The full `WorkflowDefinition` (graph + execution config) is validated at write + time — structural errors (duplicate node/edge IDs, port mismatches, etc.) surface + here rather than when a caller later clones the template into a workflow. + + Use this to capture a workflow configuration you intend to reuse or share. To + make a template available in the public gallery, an admin must mark it public + via the admin API. To create a runnable workflow from an existing template, + use `POST /templates/{id}/clone` instead. - The caller supplies a full `WorkflowDefinition` (graph + execution). - Save-time validation (`validate_definition_save`) mirrors the - `/api/v1/workflows` contract — duplicate node/edge IDs, port mismatches, - and other structural errors fail at write time rather than surfacing - when a caller later clones the template into a workflow. + Requires workspace `editor` role or higher. Parameters ---------- name : str + Display name for the template (1–200 characters, not blank). workspace_id : typing.Optional[str] description : typing.Optional[str] + Optional human-readable description shown in the gallery (max 2,000 characters). category : typing.Optional[TemplateCategory] + Optional category tag used for gallery filtering. definition : typing.Optional[WorkflowDefinitionInput] + Full workflow definition (graph + execution config). Validated at write time — structural errors are rejected with 422. request_options : typing.Optional[RequestOptions] Request-specific configuration. @@ -212,9 +230,16 @@ def get( request_options: typing.Optional[RequestOptions] = None, ) -> HttpResponse[ApiResponseTemplateOut]: """ - Fetch a template by id if visible (own, public, or starter). + Fetch a single template by ID. + + Returns the template if it is visible to the caller: templates owned by the + caller's workspace are returned with the live draft definition; public/starter + templates from other workspaces are returned with the published snapshot. + + Returns 404 for templates that exist but are not visible to the caller (not + owned, not public, not a starter) — same response as for a missing ID. - Dual-auth: Clerk JWT or `op_live_*` API key (scope `templates:read`). + Dual-auth: Bearer JWT or API key (scope `templates:read`). Parameters ---------- @@ -276,7 +301,18 @@ def delete_template( request_options: typing.Optional[RequestOptions] = None, ) -> HttpResponse[ApiResponseDict]: """ - Soft-delete a template. Owner only; starter templates are read-only. + Delete a template owned by the caller's workspace. + + The delete is a soft delete — the record is hidden from the gallery and + all visibility checks immediately, but is not physically removed. Any + workflows previously cloned from this template are unaffected; clone + creates an independent copy of the definition at clone time. + + Restrictions: + - Only the owning workspace may delete its templates (403 otherwise). + - Platform starter templates (`is_starter=true`) cannot be deleted (403). + + Requires workspace `editor` role or higher. Parameters ---------- @@ -342,10 +378,20 @@ def update_template( request_options: typing.Optional[RequestOptions] = None, ) -> HttpResponse[ApiResponseTemplateOut]: """ - Update a template. Owner only; starter templates are read-only. + Update a template owned by the caller's workspace. + + All fields are optional (omit to keep the stored value). When `definition` + is supplied it is a full replace — send the complete graph, not a partial + diff. Structural validation runs on write, same as `POST /templates`. - `definition` is full-replace (matches `WorkflowUpdate` — the FE sends the - entire graph back on save). Other fields are partial via `exclude_unset`. + Restrictions: + - Only the owning workspace may update its templates (403 otherwise). + - Platform starter templates (`is_starter=true`) are read-only via this + endpoint regardless of workspace ownership (403). + - Updates apply only to the draft/live definition; the published gallery + snapshot is not updated until an admin republishes. + + Requires workspace `editor` role or higher. Parameters ---------- @@ -426,7 +472,28 @@ def estimate_template( request_options: typing.Optional[RequestOptions] = None, ) -> HttpResponse[ApiResponseTemplateEstimateResponse]: """ - Return per-1,000-character pricing for a visible template snapshot. + Estimate the credit cost of running a workflow built from this template. + + Returns a per-unit pricing guide expressed in credits per + `unit_chars` input characters (default 1,000). Because the template does not + contain the caller's actual script, the estimate uses a synthetic fixed-length + input to compute a reproducible per-unit rate. Multiply by your expected + character count to project total cost. + + The response distinguishes variable costs (scale with script length, e.g. + synthesis) from fixed costs (apply once per run regardless of length). A + node-level breakdown is included so callers can see which processing steps + drive the cost. + + Results are cached against the template definition and current pricing rates. + `cache_status` indicates whether this response was served from cache (`hit`), + computed fresh (`miss`), or recomputed because the definition or rates changed + (`stale`). + + Visibility rules match `GET /templates/{id}` — own-workspace templates use the + draft definition; cross-workspace templates use the published snapshot. + + Dual-auth: Bearer JWT or API key (scope `templates:read`). Parameters ---------- @@ -489,16 +556,24 @@ def clone( request_options: typing.Optional[RequestOptions] = None, ) -> HttpResponse[ApiResponseWorkflowOut]: """ - Clone a template into a workflow in the caller's workspace. + Create a runnable workflow in the caller's workspace from a template. + + This is the primary way to use a template: it produces a new `Workflow` + owned by the caller's workspace, ready to accept scripts and run jobs. + + Use `body.name` to set the workflow name; omit it (or send blank/whitespace) + to get the default `"{template name} (Copy)"`. - Dual-auth: Clerk JWT or `op_live_*` API key (scope `workflows:write`). + Cross-workspace clones (gallery/starter templates) copy the published + snapshot so unpublished draft edits made by the template owner never leak to + other workspaces. Same-workspace clones copy the live draft definition. - Same-workspace clones use the live `definition` (owner authoring). - Cross-workspace clones use the `published_definition` snapshot to avoid - leaking unpublished draft edits. + Use `GET /templates/{id}/estimate` first to preview credit costs before + committing to a clone and run. Use `POST /workflows/{id}/duplicate` to copy + an existing workflow rather than starting from a template. - Resolved name: explicit `body.name` (stripped) OR fallback to - `"{source_name} (Copy)"`. + Requires workspace `editor` role or higher. + Dual-auth: Bearer JWT or API key (scope `workflows:write`). Parameters ---------- @@ -563,15 +638,17 @@ def favorite_template( self, template_id: str, *, request_options: typing.Optional[RequestOptions] = None ) -> HttpResponse[ApiResponseTemplateOut]: """ - Mark a visible template as a favorite for the current user. + Add a template to the current user's favorites. - Authenticated but not workspace-scoped — favorites are cross-workspace by - design for public/starter templates and for the caller's own private - templates when they still belong to that template's workspace. + Favorites are per-user, not per-workspace — the same favorite list is + visible regardless of which workspace the caller is currently acting in. + Any template visible to the caller (own workspace, public, or starter) can + be favorited. - Returns 404 when the template does not exist or is not visible to the - caller. This POST intentionally enumerates success/failure for the toggle - UX, while DELETE stays non-enumerating. + Returns 404 when the template does not exist or is not visible to the caller. + Calling this endpoint on an already-favorited template is idempotent (returns + 200 with the template). Use `DELETE /templates/{id}/favorite` to remove. + Does not require `X-Workspace-Id`. Parameters ---------- @@ -624,10 +701,10 @@ def unfavorite_template( self, template_id: str, *, request_options: typing.Optional[RequestOptions] = None ) -> HttpResponse[ApiResponseDict]: """ - Remove a template favorite for the current user. + Remove a template from the current user's favorites. - Deletion is intentionally non-enumerating: authenticated callers receive a - successful empty response whether the row or template exists. + Idempotent and non-enumerating: returns an empty success response whether + or not the favorite or the template exists. Does not require `X-Workspace-Id`. Parameters ---------- @@ -693,27 +770,35 @@ async def list( request_options: typing.Optional[RequestOptions] = None, ) -> AsyncHttpResponse[ApiListResponseTemplateOut]: """ - List live published templates across workspaces (gallery). + Browse the public template gallery across all workspaces. - Authenticated but not workspace-scoped — the gallery is cross-workspace - by design (published rows from any workspace). Does not require - `X-Workspace-Id`, so a freshly signed-up user without a workspace can - still browse templates. + Returns only templates that have an active published snapshot (`is_public=true`, + `published_definition` set, not unpublished). Results come from any workspace — + the gallery is intentionally cross-workspace so callers can discover shared + starting points regardless of their own workspace membership. - Dual-auth: Clerk JWT or `op_live_*` API key (scope `templates:read`). + Does not require `X-Workspace-Id`, so callers without a workspace (e.g. during + onboarding) can still browse. The response reflects the published snapshot for + each row, not any unpublished draft edits. + + Dual-auth: Bearer JWT or API key (scope `templates:read`). Parameters ---------- category : typing.Optional[typing.Sequence[TemplateCategory]] - Repeat for OR, e.g. ?category=media&category=creative + Filter by category. Repeat the parameter for OR logic, e.g. `?category=media&category=creative`. search : typing.Optional[str] + Full-text search over template name and description. sort : typing.Optional[ListTemplatesRequestSort] + Sort order: `recent` (last published), `popular` (most cloned), or `name` (alphabetical). offset : typing.Optional[int] + Zero-based offset for page navigation. limit : typing.Optional[int] + Maximum number of templates to return (1–100). favorites_only : typing.Optional[bool] @@ -779,25 +864,35 @@ async def create_template( request_options: typing.Optional[RequestOptions] = None, ) -> AsyncHttpResponse[ApiResponseTemplateOut]: """ - Create a new workflow template in the current workspace. + Create a reusable workflow template in the current workspace. + + Templates are workspace-private on creation (`is_public=false`, `is_starter=false`). + The full `WorkflowDefinition` (graph + execution config) is validated at write + time — structural errors (duplicate node/edge IDs, port mismatches, etc.) surface + here rather than when a caller later clones the template into a workflow. + + Use this to capture a workflow configuration you intend to reuse or share. To + make a template available in the public gallery, an admin must mark it public + via the admin API. To create a runnable workflow from an existing template, + use `POST /templates/{id}/clone` instead. - The caller supplies a full `WorkflowDefinition` (graph + execution). - Save-time validation (`validate_definition_save`) mirrors the - `/api/v1/workflows` contract — duplicate node/edge IDs, port mismatches, - and other structural errors fail at write time rather than surfacing - when a caller later clones the template into a workflow. + Requires workspace `editor` role or higher. Parameters ---------- name : str + Display name for the template (1–200 characters, not blank). workspace_id : typing.Optional[str] description : typing.Optional[str] + Optional human-readable description shown in the gallery (max 2,000 characters). category : typing.Optional[TemplateCategory] + Optional category tag used for gallery filtering. definition : typing.Optional[WorkflowDefinitionInput] + Full workflow definition (graph + execution config). Validated at write time — structural errors are rejected with 422. request_options : typing.Optional[RequestOptions] Request-specific configuration. @@ -863,9 +958,16 @@ async def get( request_options: typing.Optional[RequestOptions] = None, ) -> AsyncHttpResponse[ApiResponseTemplateOut]: """ - Fetch a template by id if visible (own, public, or starter). + Fetch a single template by ID. + + Returns the template if it is visible to the caller: templates owned by the + caller's workspace are returned with the live draft definition; public/starter + templates from other workspaces are returned with the published snapshot. + + Returns 404 for templates that exist but are not visible to the caller (not + owned, not public, not a starter) — same response as for a missing ID. - Dual-auth: Clerk JWT or `op_live_*` API key (scope `templates:read`). + Dual-auth: Bearer JWT or API key (scope `templates:read`). Parameters ---------- @@ -927,7 +1029,18 @@ async def delete_template( request_options: typing.Optional[RequestOptions] = None, ) -> AsyncHttpResponse[ApiResponseDict]: """ - Soft-delete a template. Owner only; starter templates are read-only. + Delete a template owned by the caller's workspace. + + The delete is a soft delete — the record is hidden from the gallery and + all visibility checks immediately, but is not physically removed. Any + workflows previously cloned from this template are unaffected; clone + creates an independent copy of the definition at clone time. + + Restrictions: + - Only the owning workspace may delete its templates (403 otherwise). + - Platform starter templates (`is_starter=true`) cannot be deleted (403). + + Requires workspace `editor` role or higher. Parameters ---------- @@ -993,10 +1106,20 @@ async def update_template( request_options: typing.Optional[RequestOptions] = None, ) -> AsyncHttpResponse[ApiResponseTemplateOut]: """ - Update a template. Owner only; starter templates are read-only. + Update a template owned by the caller's workspace. + + All fields are optional (omit to keep the stored value). When `definition` + is supplied it is a full replace — send the complete graph, not a partial + diff. Structural validation runs on write, same as `POST /templates`. - `definition` is full-replace (matches `WorkflowUpdate` — the FE sends the - entire graph back on save). Other fields are partial via `exclude_unset`. + Restrictions: + - Only the owning workspace may update its templates (403 otherwise). + - Platform starter templates (`is_starter=true`) are read-only via this + endpoint regardless of workspace ownership (403). + - Updates apply only to the draft/live definition; the published gallery + snapshot is not updated until an admin republishes. + + Requires workspace `editor` role or higher. Parameters ---------- @@ -1077,7 +1200,28 @@ async def estimate_template( request_options: typing.Optional[RequestOptions] = None, ) -> AsyncHttpResponse[ApiResponseTemplateEstimateResponse]: """ - Return per-1,000-character pricing for a visible template snapshot. + Estimate the credit cost of running a workflow built from this template. + + Returns a per-unit pricing guide expressed in credits per + `unit_chars` input characters (default 1,000). Because the template does not + contain the caller's actual script, the estimate uses a synthetic fixed-length + input to compute a reproducible per-unit rate. Multiply by your expected + character count to project total cost. + + The response distinguishes variable costs (scale with script length, e.g. + synthesis) from fixed costs (apply once per run regardless of length). A + node-level breakdown is included so callers can see which processing steps + drive the cost. + + Results are cached against the template definition and current pricing rates. + `cache_status` indicates whether this response was served from cache (`hit`), + computed fresh (`miss`), or recomputed because the definition or rates changed + (`stale`). + + Visibility rules match `GET /templates/{id}` — own-workspace templates use the + draft definition; cross-workspace templates use the published snapshot. + + Dual-auth: Bearer JWT or API key (scope `templates:read`). Parameters ---------- @@ -1140,16 +1284,24 @@ async def clone( request_options: typing.Optional[RequestOptions] = None, ) -> AsyncHttpResponse[ApiResponseWorkflowOut]: """ - Clone a template into a workflow in the caller's workspace. + Create a runnable workflow in the caller's workspace from a template. + + This is the primary way to use a template: it produces a new `Workflow` + owned by the caller's workspace, ready to accept scripts and run jobs. + + Use `body.name` to set the workflow name; omit it (or send blank/whitespace) + to get the default `"{template name} (Copy)"`. - Dual-auth: Clerk JWT or `op_live_*` API key (scope `workflows:write`). + Cross-workspace clones (gallery/starter templates) copy the published + snapshot so unpublished draft edits made by the template owner never leak to + other workspaces. Same-workspace clones copy the live draft definition. - Same-workspace clones use the live `definition` (owner authoring). - Cross-workspace clones use the `published_definition` snapshot to avoid - leaking unpublished draft edits. + Use `GET /templates/{id}/estimate` first to preview credit costs before + committing to a clone and run. Use `POST /workflows/{id}/duplicate` to copy + an existing workflow rather than starting from a template. - Resolved name: explicit `body.name` (stripped) OR fallback to - `"{source_name} (Copy)"`. + Requires workspace `editor` role or higher. + Dual-auth: Bearer JWT or API key (scope `workflows:write`). Parameters ---------- @@ -1214,15 +1366,17 @@ async def favorite_template( self, template_id: str, *, request_options: typing.Optional[RequestOptions] = None ) -> AsyncHttpResponse[ApiResponseTemplateOut]: """ - Mark a visible template as a favorite for the current user. + Add a template to the current user's favorites. - Authenticated but not workspace-scoped — favorites are cross-workspace by - design for public/starter templates and for the caller's own private - templates when they still belong to that template's workspace. + Favorites are per-user, not per-workspace — the same favorite list is + visible regardless of which workspace the caller is currently acting in. + Any template visible to the caller (own workspace, public, or starter) can + be favorited. - Returns 404 when the template does not exist or is not visible to the - caller. This POST intentionally enumerates success/failure for the toggle - UX, while DELETE stays non-enumerating. + Returns 404 when the template does not exist or is not visible to the caller. + Calling this endpoint on an already-favorited template is idempotent (returns + 200 with the template). Use `DELETE /templates/{id}/favorite` to remove. + Does not require `X-Workspace-Id`. Parameters ---------- @@ -1275,10 +1429,10 @@ async def unfavorite_template( self, template_id: str, *, request_options: typing.Optional[RequestOptions] = None ) -> AsyncHttpResponse[ApiResponseDict]: """ - Remove a template favorite for the current user. + Remove a template from the current user's favorites. - Deletion is intentionally non-enumerating: authenticated callers receive a - successful empty response whether the row or template exists. + Idempotent and non-enumerating: returns an empty success response whether + or not the favorite or the template exists. Does not require `X-Workspace-Id`. Parameters ---------- diff --git a/src/onepin/types/__init__.py b/src/onepin/types/__init__.py index 30f2136..bdee59a 100644 --- a/src/onepin/types/__init__.py +++ b/src/onepin/types/__init__.py @@ -6,7 +6,6 @@ from importlib import import_module if typing.TYPE_CHECKING: - from .api_counted_list_response_api_key_out import ApiCountedListResponseApiKeyOut from .api_counted_list_response_catalog_voice_out import ApiCountedListResponseCatalogVoiceOut from .api_counted_list_response_voice_out import ApiCountedListResponseVoiceOut from .api_counted_list_response_workflow_list_item import ApiCountedListResponseWorkflowListItem @@ -14,14 +13,9 @@ from .api_error_body import ApiErrorBody from .api_error_detail import ApiErrorDetail from .api_error_response import ApiErrorResponse - from .api_key_created_out import ApiKeyCreatedOut - from .api_key_list_status import ApiKeyListStatus - from .api_key_out import ApiKeyOut - from .api_key_rotate_out import ApiKeyRotateOut from .api_key_scope import ApiKeyScope from .api_list_response_catalog_model_out import ApiListResponseCatalogModelOut from .api_list_response_catalog_provider_out import ApiListResponseCatalogProviderOut - from .api_list_response_customer_plan_response import ApiListResponseCustomerPlanResponse from .api_list_response_dictionary_out import ApiListResponseDictionaryOut from .api_list_response_node_ports_out import ApiListResponseNodePortsOut from .api_list_response_template_out import ApiListResponseTemplateOut @@ -30,42 +24,29 @@ from .api_list_response_voice_similar_out import ApiListResponseVoiceSimilarOut from .api_list_response_workspace_member_out import ApiListResponseWorkspaceMemberOut from .api_list_response_workspace_out import ApiListResponseWorkspaceOut - from .api_response_api_key_created_out import ApiResponseApiKeyCreatedOut - from .api_response_api_key_out import ApiResponseApiKeyOut - from .api_response_api_key_rotate_out import ApiResponseApiKeyRotateOut from .api_response_auth_whoami_out import ApiResponseAuthWhoamiOut from .api_response_balance_response import ApiResponseBalanceResponse from .api_response_catalog_model_out import ApiResponseCatalogModelOut from .api_response_catalog_provider_out import ApiResponseCatalogProviderOut - from .api_response_checkout_response import ApiResponseCheckoutResponse - from .api_response_customer_plan_change_preview_response import ApiResponseCustomerPlanChangePreviewResponse - from .api_response_customer_subscription_response import ApiResponseCustomerSubscriptionResponse from .api_response_dict import ApiResponseDict from .api_response_dictionary_out import ApiResponseDictionaryOut from .api_response_download_url_out import ApiResponseDownloadUrlOut from .api_response_email_notification_preferences_out import ApiResponseEmailNotificationPreferencesOut from .api_response_estimate_response import ApiResponseEstimateResponse - from .api_response_invoice_list_response import ApiResponseInvoiceListResponse from .api_response_list_dictionary_language_out import ApiResponseListDictionaryLanguageOut - from .api_response_list_payment_method_response import ApiResponseListPaymentMethodResponse from .api_response_list_workflow_run_step_out import ApiResponseListWorkflowRunStepOut from .api_response_node_detail_out import ApiResponseNodeDetailOut from .api_response_plan_limits import ApiResponsePlanLimits from .api_response_pronunciation_suggestion import ApiResponsePronunciationSuggestion - from .api_response_provider_key_item_out import ApiResponseProviderKeyItemOut - from .api_response_provider_keys_manifest_out import ApiResponseProviderKeysManifestOut from .api_response_runs_summary_out import ApiResponseRunsSummaryOut - from .api_response_setup_intent_response import ApiResponseSetupIntentResponse from .api_response_slug_availability_out import ApiResponseSlugAvailabilityOut from .api_response_template_estimate_response import ApiResponseTemplateEstimateResponse from .api_response_template_out import ApiResponseTemplateOut - from .api_response_union_customer_subscription_response_none_type import ( - ApiResponseUnionCustomerSubscriptionResponseNoneType, - ) from .api_response_upload_create_response import ApiResponseUploadCreateResponse from .api_response_upload_out import ApiResponseUploadOut from .api_response_usage_by_language_out import ApiResponseUsageByLanguageOut 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_out import ApiResponseWorkflowOut from .api_response_workflow_run_detail_out import ApiResponseWorkflowRunDetailOut @@ -74,9 +55,7 @@ from .api_response_workflow_run_status_out import ApiResponseWorkflowRunStatusOut from .api_response_workspace_invite_out import ApiResponseWorkspaceInviteOut from .api_response_workspace_out import ApiResponseWorkspaceOut - from .api_response_workspace_runs_stats_out import ApiResponseWorkspaceRunsStatsOut from .api_response_workspace_settings_out import ApiResponseWorkspaceSettingsOut - from .api_response_workspace_workflows_stats_out import ApiResponseWorkspaceWorkflowsStatsOut from .auth_whoami_out import AuthWhoamiOut from .auth_whoami_out_auth_kind import AuthWhoamiOutAuthKind from .balance_response import BalanceResponse @@ -84,11 +63,7 @@ from .catalog_model_out import CatalogModelOut from .catalog_provider_out import CatalogProviderOut from .catalog_voice_out import CatalogVoiceOut - from .checkout_response import CheckoutResponse from .counted_pagination_meta import CountedPaginationMeta - from .customer_plan_change_preview_response import CustomerPlanChangePreviewResponse - from .customer_plan_response import CustomerPlanResponse - from .customer_subscription_response import CustomerSubscriptionResponse from .dictionary_language_out import DictionaryLanguageOut from .dictionary_method import DictionaryMethod from .dictionary_out import DictionaryOut @@ -102,8 +77,6 @@ from .graph_edge import GraphEdge from .graph_node import GraphNode from .http_validation_error import HttpValidationError - from .invoice_list_response import InvoiceListResponse - from .invoice_response import InvoiceResponse from .meta import Meta from .model_out import ModelOut from .node_category import NodeCategory @@ -123,21 +96,12 @@ from .node_type import NodeType from .numeric_option import NumericOption from .pagination_meta import PaginationMeta - from .payment_method_response import PaymentMethodResponse - from .plan_details import PlanDetails - from .plan_details_item import PlanDetailsItem - from .plan_details_section import PlanDetailsSection from .plan_limits import PlanLimits from .plan_tier import PlanTier from .port_out import PortOut from .pronunciation_suggestion import PronunciationSuggestion from .provider_group_out import ProviderGroupOut - from .provider_key_item_out import ProviderKeyItemOut - from .provider_key_provider import ProviderKeyProvider - from .provider_key_status import ProviderKeyStatus - from .provider_keys_manifest_out import ProviderKeysManifestOut from .runs_summary_out import RunsSummaryOut - from .setup_intent_response import SetupIntentResponse from .slug_availability_out import SlugAvailabilityOut from .slug_availability_out_reason import SlugAvailabilityOutReason from .template_category import TemplateCategory @@ -175,6 +139,8 @@ from .voice_accent import VoiceAccent from .voice_age import VoiceAge from .voice_category import VoiceCategory + from .voice_facet_item import VoiceFacetItem + from .voice_facets_out import VoiceFacetsOut from .voice_gender import VoiceGender from .voice_out import VoiceOut from .voice_similar_out import VoiceSimilarOut @@ -224,11 +190,8 @@ from .workspace_member_role_update import WorkspaceMemberRoleUpdate from .workspace_out import WorkspaceOut from .workspace_role import WorkspaceRole - from .workspace_runs_stats_out import WorkspaceRunsStatsOut from .workspace_settings_out import WorkspaceSettingsOut - from .workspace_workflows_stats_out import WorkspaceWorkflowsStatsOut _dynamic_imports: typing.Dict[str, str] = { - "ApiCountedListResponseApiKeyOut": ".api_counted_list_response_api_key_out", "ApiCountedListResponseCatalogVoiceOut": ".api_counted_list_response_catalog_voice_out", "ApiCountedListResponseVoiceOut": ".api_counted_list_response_voice_out", "ApiCountedListResponseWorkflowListItem": ".api_counted_list_response_workflow_list_item", @@ -236,14 +199,9 @@ "ApiErrorBody": ".api_error_body", "ApiErrorDetail": ".api_error_detail", "ApiErrorResponse": ".api_error_response", - "ApiKeyCreatedOut": ".api_key_created_out", - "ApiKeyListStatus": ".api_key_list_status", - "ApiKeyOut": ".api_key_out", - "ApiKeyRotateOut": ".api_key_rotate_out", "ApiKeyScope": ".api_key_scope", "ApiListResponseCatalogModelOut": ".api_list_response_catalog_model_out", "ApiListResponseCatalogProviderOut": ".api_list_response_catalog_provider_out", - "ApiListResponseCustomerPlanResponse": ".api_list_response_customer_plan_response", "ApiListResponseDictionaryOut": ".api_list_response_dictionary_out", "ApiListResponseNodePortsOut": ".api_list_response_node_ports_out", "ApiListResponseTemplateOut": ".api_list_response_template_out", @@ -252,40 +210,29 @@ "ApiListResponseVoiceSimilarOut": ".api_list_response_voice_similar_out", "ApiListResponseWorkspaceMemberOut": ".api_list_response_workspace_member_out", "ApiListResponseWorkspaceOut": ".api_list_response_workspace_out", - "ApiResponseApiKeyCreatedOut": ".api_response_api_key_created_out", - "ApiResponseApiKeyOut": ".api_response_api_key_out", - "ApiResponseApiKeyRotateOut": ".api_response_api_key_rotate_out", "ApiResponseAuthWhoamiOut": ".api_response_auth_whoami_out", "ApiResponseBalanceResponse": ".api_response_balance_response", "ApiResponseCatalogModelOut": ".api_response_catalog_model_out", "ApiResponseCatalogProviderOut": ".api_response_catalog_provider_out", - "ApiResponseCheckoutResponse": ".api_response_checkout_response", - "ApiResponseCustomerPlanChangePreviewResponse": ".api_response_customer_plan_change_preview_response", - "ApiResponseCustomerSubscriptionResponse": ".api_response_customer_subscription_response", "ApiResponseDict": ".api_response_dict", "ApiResponseDictionaryOut": ".api_response_dictionary_out", "ApiResponseDownloadUrlOut": ".api_response_download_url_out", "ApiResponseEmailNotificationPreferencesOut": ".api_response_email_notification_preferences_out", "ApiResponseEstimateResponse": ".api_response_estimate_response", - "ApiResponseInvoiceListResponse": ".api_response_invoice_list_response", "ApiResponseListDictionaryLanguageOut": ".api_response_list_dictionary_language_out", - "ApiResponseListPaymentMethodResponse": ".api_response_list_payment_method_response", "ApiResponseListWorkflowRunStepOut": ".api_response_list_workflow_run_step_out", "ApiResponseNodeDetailOut": ".api_response_node_detail_out", "ApiResponsePlanLimits": ".api_response_plan_limits", "ApiResponsePronunciationSuggestion": ".api_response_pronunciation_suggestion", - "ApiResponseProviderKeyItemOut": ".api_response_provider_key_item_out", - "ApiResponseProviderKeysManifestOut": ".api_response_provider_keys_manifest_out", "ApiResponseRunsSummaryOut": ".api_response_runs_summary_out", - "ApiResponseSetupIntentResponse": ".api_response_setup_intent_response", "ApiResponseSlugAvailabilityOut": ".api_response_slug_availability_out", "ApiResponseTemplateEstimateResponse": ".api_response_template_estimate_response", "ApiResponseTemplateOut": ".api_response_template_out", - "ApiResponseUnionCustomerSubscriptionResponseNoneType": ".api_response_union_customer_subscription_response_none_type", "ApiResponseUploadCreateResponse": ".api_response_upload_create_response", "ApiResponseUploadOut": ".api_response_upload_out", "ApiResponseUsageByLanguageOut": ".api_response_usage_by_language_out", "ApiResponseUsageSummaryOut": ".api_response_usage_summary_out", + "ApiResponseVoiceFacetsOut": ".api_response_voice_facets_out", "ApiResponseVoiceOut": ".api_response_voice_out", "ApiResponseWorkflowOut": ".api_response_workflow_out", "ApiResponseWorkflowRunDetailOut": ".api_response_workflow_run_detail_out", @@ -294,9 +241,7 @@ "ApiResponseWorkflowRunStatusOut": ".api_response_workflow_run_status_out", "ApiResponseWorkspaceInviteOut": ".api_response_workspace_invite_out", "ApiResponseWorkspaceOut": ".api_response_workspace_out", - "ApiResponseWorkspaceRunsStatsOut": ".api_response_workspace_runs_stats_out", "ApiResponseWorkspaceSettingsOut": ".api_response_workspace_settings_out", - "ApiResponseWorkspaceWorkflowsStatsOut": ".api_response_workspace_workflows_stats_out", "AuthWhoamiOut": ".auth_whoami_out", "AuthWhoamiOutAuthKind": ".auth_whoami_out_auth_kind", "BalanceResponse": ".balance_response", @@ -304,11 +249,7 @@ "CatalogModelOut": ".catalog_model_out", "CatalogProviderOut": ".catalog_provider_out", "CatalogVoiceOut": ".catalog_voice_out", - "CheckoutResponse": ".checkout_response", "CountedPaginationMeta": ".counted_pagination_meta", - "CustomerPlanChangePreviewResponse": ".customer_plan_change_preview_response", - "CustomerPlanResponse": ".customer_plan_response", - "CustomerSubscriptionResponse": ".customer_subscription_response", "DictionaryLanguageOut": ".dictionary_language_out", "DictionaryMethod": ".dictionary_method", "DictionaryOut": ".dictionary_out", @@ -322,8 +263,6 @@ "GraphEdge": ".graph_edge", "GraphNode": ".graph_node", "HttpValidationError": ".http_validation_error", - "InvoiceListResponse": ".invoice_list_response", - "InvoiceResponse": ".invoice_response", "Meta": ".meta", "ModelOut": ".model_out", "NodeCategory": ".node_category", @@ -341,21 +280,12 @@ "NodeType": ".node_type", "NumericOption": ".numeric_option", "PaginationMeta": ".pagination_meta", - "PaymentMethodResponse": ".payment_method_response", - "PlanDetails": ".plan_details", - "PlanDetailsItem": ".plan_details_item", - "PlanDetailsSection": ".plan_details_section", "PlanLimits": ".plan_limits", "PlanTier": ".plan_tier", "PortOut": ".port_out", "PronunciationSuggestion": ".pronunciation_suggestion", "ProviderGroupOut": ".provider_group_out", - "ProviderKeyItemOut": ".provider_key_item_out", - "ProviderKeyProvider": ".provider_key_provider", - "ProviderKeyStatus": ".provider_key_status", - "ProviderKeysManifestOut": ".provider_keys_manifest_out", "RunsSummaryOut": ".runs_summary_out", - "SetupIntentResponse": ".setup_intent_response", "SlugAvailabilityOut": ".slug_availability_out", "SlugAvailabilityOutReason": ".slug_availability_out_reason", "TemplateCategory": ".template_category", @@ -393,6 +323,8 @@ "VoiceAccent": ".voice_accent", "VoiceAge": ".voice_age", "VoiceCategory": ".voice_category", + "VoiceFacetItem": ".voice_facet_item", + "VoiceFacetsOut": ".voice_facets_out", "VoiceGender": ".voice_gender", "VoiceOut": ".voice_out", "VoiceSimilarOut": ".voice_similar_out", @@ -442,9 +374,7 @@ "WorkspaceMemberRoleUpdate": ".workspace_member_role_update", "WorkspaceOut": ".workspace_out", "WorkspaceRole": ".workspace_role", - "WorkspaceRunsStatsOut": ".workspace_runs_stats_out", "WorkspaceSettingsOut": ".workspace_settings_out", - "WorkspaceWorkflowsStatsOut": ".workspace_workflows_stats_out", } @@ -470,7 +400,6 @@ def __dir__(): __all__ = [ - "ApiCountedListResponseApiKeyOut", "ApiCountedListResponseCatalogVoiceOut", "ApiCountedListResponseVoiceOut", "ApiCountedListResponseWorkflowListItem", @@ -478,14 +407,9 @@ def __dir__(): "ApiErrorBody", "ApiErrorDetail", "ApiErrorResponse", - "ApiKeyCreatedOut", - "ApiKeyListStatus", - "ApiKeyOut", - "ApiKeyRotateOut", "ApiKeyScope", "ApiListResponseCatalogModelOut", "ApiListResponseCatalogProviderOut", - "ApiListResponseCustomerPlanResponse", "ApiListResponseDictionaryOut", "ApiListResponseNodePortsOut", "ApiListResponseTemplateOut", @@ -494,40 +418,29 @@ def __dir__(): "ApiListResponseVoiceSimilarOut", "ApiListResponseWorkspaceMemberOut", "ApiListResponseWorkspaceOut", - "ApiResponseApiKeyCreatedOut", - "ApiResponseApiKeyOut", - "ApiResponseApiKeyRotateOut", "ApiResponseAuthWhoamiOut", "ApiResponseBalanceResponse", "ApiResponseCatalogModelOut", "ApiResponseCatalogProviderOut", - "ApiResponseCheckoutResponse", - "ApiResponseCustomerPlanChangePreviewResponse", - "ApiResponseCustomerSubscriptionResponse", "ApiResponseDict", "ApiResponseDictionaryOut", "ApiResponseDownloadUrlOut", "ApiResponseEmailNotificationPreferencesOut", "ApiResponseEstimateResponse", - "ApiResponseInvoiceListResponse", "ApiResponseListDictionaryLanguageOut", - "ApiResponseListPaymentMethodResponse", "ApiResponseListWorkflowRunStepOut", "ApiResponseNodeDetailOut", "ApiResponsePlanLimits", "ApiResponsePronunciationSuggestion", - "ApiResponseProviderKeyItemOut", - "ApiResponseProviderKeysManifestOut", "ApiResponseRunsSummaryOut", - "ApiResponseSetupIntentResponse", "ApiResponseSlugAvailabilityOut", "ApiResponseTemplateEstimateResponse", "ApiResponseTemplateOut", - "ApiResponseUnionCustomerSubscriptionResponseNoneType", "ApiResponseUploadCreateResponse", "ApiResponseUploadOut", "ApiResponseUsageByLanguageOut", "ApiResponseUsageSummaryOut", + "ApiResponseVoiceFacetsOut", "ApiResponseVoiceOut", "ApiResponseWorkflowOut", "ApiResponseWorkflowRunDetailOut", @@ -536,9 +449,7 @@ def __dir__(): "ApiResponseWorkflowRunStatusOut", "ApiResponseWorkspaceInviteOut", "ApiResponseWorkspaceOut", - "ApiResponseWorkspaceRunsStatsOut", "ApiResponseWorkspaceSettingsOut", - "ApiResponseWorkspaceWorkflowsStatsOut", "AuthWhoamiOut", "AuthWhoamiOutAuthKind", "BalanceResponse", @@ -546,11 +457,7 @@ def __dir__(): "CatalogModelOut", "CatalogProviderOut", "CatalogVoiceOut", - "CheckoutResponse", "CountedPaginationMeta", - "CustomerPlanChangePreviewResponse", - "CustomerPlanResponse", - "CustomerSubscriptionResponse", "DictionaryLanguageOut", "DictionaryMethod", "DictionaryOut", @@ -564,8 +471,6 @@ def __dir__(): "GraphEdge", "GraphNode", "HttpValidationError", - "InvoiceListResponse", - "InvoiceResponse", "Meta", "ModelOut", "NodeCategory", @@ -583,21 +488,12 @@ def __dir__(): "NodeType", "NumericOption", "PaginationMeta", - "PaymentMethodResponse", - "PlanDetails", - "PlanDetailsItem", - "PlanDetailsSection", "PlanLimits", "PlanTier", "PortOut", "PronunciationSuggestion", "ProviderGroupOut", - "ProviderKeyItemOut", - "ProviderKeyProvider", - "ProviderKeyStatus", - "ProviderKeysManifestOut", "RunsSummaryOut", - "SetupIntentResponse", "SlugAvailabilityOut", "SlugAvailabilityOutReason", "TemplateCategory", @@ -635,6 +531,8 @@ def __dir__(): "VoiceAccent", "VoiceAge", "VoiceCategory", + "VoiceFacetItem", + "VoiceFacetsOut", "VoiceGender", "VoiceOut", "VoiceSimilarOut", @@ -684,7 +582,5 @@ def __dir__(): "WorkspaceMemberRoleUpdate", "WorkspaceOut", "WorkspaceRole", - "WorkspaceRunsStatsOut", "WorkspaceSettingsOut", - "WorkspaceWorkflowsStatsOut", ] diff --git a/src/onepin/types/api_counted_list_response_api_key_out.py b/src/onepin/types/api_counted_list_response_api_key_out.py deleted file mode 100644 index 52e8b4f..0000000 --- a/src/onepin/types/api_counted_list_response_api_key_out.py +++ /dev/null @@ -1,24 +0,0 @@ -# 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 .api_key_out import ApiKeyOut -from .counted_pagination_meta import CountedPaginationMeta -from .meta import Meta - - -class ApiCountedListResponseApiKeyOut(UniversalBaseModel): - data: typing.List[ApiKeyOut] - meta: Meta - pagination: CountedPaginationMeta - - 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/api_key_created_out.py b/src/onepin/types/api_key_created_out.py deleted file mode 100644 index 36d9ce1..0000000 --- a/src/onepin/types/api_key_created_out.py +++ /dev/null @@ -1,40 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -import pydantic -from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel -from .api_key_scope import ApiKeyScope - - -class ApiKeyCreatedOut(UniversalBaseModel): - id: str - workspace_id: str - created_by: typing.Optional[str] = None - name: str - key_type: str - key_prefix: str - key_suffix: str - scopes: typing.List[ApiKeyScope] - active: bool - last_used_at: typing.Optional[dt.datetime] = None - rate_limit_per_min: typing.Optional[int] = None - revoked_at: typing.Optional[dt.datetime] = None - created_at: dt.datetime - updated_at: dt.datetime - api_key: str = pydantic.Field() - """ - Plaintext API key. Returned only once on create/rotate. - """ - - preview: typing.Optional[str] = None - - 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/api_key_list_status.py b/src/onepin/types/api_key_list_status.py deleted file mode 100644 index aab9c0b..0000000 --- a/src/onepin/types/api_key_list_status.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -ApiKeyListStatus = typing.Union[typing.Literal["active", "revoked", "all"], typing.Any] diff --git a/src/onepin/types/api_key_out.py b/src/onepin/types/api_key_out.py deleted file mode 100644 index 8a0ddb5..0000000 --- a/src/onepin/types/api_key_out.py +++ /dev/null @@ -1,35 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -import pydantic -from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel -from .api_key_scope import ApiKeyScope - - -class ApiKeyOut(UniversalBaseModel): - id: str - workspace_id: str - created_by: typing.Optional[str] = None - name: str - key_type: str - key_prefix: str - key_suffix: str - scopes: typing.List[ApiKeyScope] - active: bool - last_used_at: typing.Optional[dt.datetime] = None - rate_limit_per_min: typing.Optional[int] = None - revoked_at: typing.Optional[dt.datetime] = None - created_at: dt.datetime - updated_at: dt.datetime - preview: typing.Optional[str] = None - - 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/api_key_rotate_out.py b/src/onepin/types/api_key_rotate_out.py deleted file mode 100644 index 7819622..0000000 --- a/src/onepin/types/api_key_rotate_out.py +++ /dev/null @@ -1,22 +0,0 @@ -# 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 .api_key_created_out import ApiKeyCreatedOut -from .api_key_out import ApiKeyOut - - -class ApiKeyRotateOut(UniversalBaseModel): - old_key: ApiKeyOut - new_key: ApiKeyCreatedOut - - 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/api_list_response_customer_plan_response.py b/src/onepin/types/api_list_response_customer_plan_response.py deleted file mode 100644 index bda0ff3..0000000 --- a/src/onepin/types/api_list_response_customer_plan_response.py +++ /dev/null @@ -1,24 +0,0 @@ -# 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 .customer_plan_response import CustomerPlanResponse -from .meta import Meta -from .pagination_meta import PaginationMeta - - -class ApiListResponseCustomerPlanResponse(UniversalBaseModel): - data: typing.List[CustomerPlanResponse] - meta: Meta - pagination: PaginationMeta - - 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/api_response_api_key_created_out.py b/src/onepin/types/api_response_api_key_created_out.py deleted file mode 100644 index 77ba70d..0000000 --- a/src/onepin/types/api_response_api_key_created_out.py +++ /dev/null @@ -1,22 +0,0 @@ -# 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 .api_key_created_out import ApiKeyCreatedOut -from .meta import Meta - - -class ApiResponseApiKeyCreatedOut(UniversalBaseModel): - data: ApiKeyCreatedOut - 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/api_response_api_key_rotate_out.py b/src/onepin/types/api_response_api_key_rotate_out.py deleted file mode 100644 index 737bbb8..0000000 --- a/src/onepin/types/api_response_api_key_rotate_out.py +++ /dev/null @@ -1,22 +0,0 @@ -# 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 .api_key_rotate_out import ApiKeyRotateOut -from .meta import Meta - - -class ApiResponseApiKeyRotateOut(UniversalBaseModel): - data: ApiKeyRotateOut - 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/api_response_checkout_response.py b/src/onepin/types/api_response_checkout_response.py deleted file mode 100644 index 7db4a41..0000000 --- a/src/onepin/types/api_response_checkout_response.py +++ /dev/null @@ -1,22 +0,0 @@ -# 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 .checkout_response import CheckoutResponse -from .meta import Meta - - -class ApiResponseCheckoutResponse(UniversalBaseModel): - data: CheckoutResponse - 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/api_response_customer_plan_change_preview_response.py b/src/onepin/types/api_response_customer_plan_change_preview_response.py deleted file mode 100644 index c539ed8..0000000 --- a/src/onepin/types/api_response_customer_plan_change_preview_response.py +++ /dev/null @@ -1,22 +0,0 @@ -# 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 .customer_plan_change_preview_response import CustomerPlanChangePreviewResponse -from .meta import Meta - - -class ApiResponseCustomerPlanChangePreviewResponse(UniversalBaseModel): - data: CustomerPlanChangePreviewResponse - 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/api_response_customer_subscription_response.py b/src/onepin/types/api_response_customer_subscription_response.py deleted file mode 100644 index 9267edd..0000000 --- a/src/onepin/types/api_response_customer_subscription_response.py +++ /dev/null @@ -1,22 +0,0 @@ -# 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 .customer_subscription_response import CustomerSubscriptionResponse -from .meta import Meta - - -class ApiResponseCustomerSubscriptionResponse(UniversalBaseModel): - data: CustomerSubscriptionResponse - 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/api_response_invoice_list_response.py b/src/onepin/types/api_response_invoice_list_response.py deleted file mode 100644 index 70dd54c..0000000 --- a/src/onepin/types/api_response_invoice_list_response.py +++ /dev/null @@ -1,22 +0,0 @@ -# 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 .invoice_list_response import InvoiceListResponse -from .meta import Meta - - -class ApiResponseInvoiceListResponse(UniversalBaseModel): - data: InvoiceListResponse - 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/api_response_list_payment_method_response.py b/src/onepin/types/api_response_list_payment_method_response.py deleted file mode 100644 index 551af7a..0000000 --- a/src/onepin/types/api_response_list_payment_method_response.py +++ /dev/null @@ -1,22 +0,0 @@ -# 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 .payment_method_response import PaymentMethodResponse - - -class ApiResponseListPaymentMethodResponse(UniversalBaseModel): - data: typing.List[PaymentMethodResponse] - 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/api_response_provider_key_item_out.py b/src/onepin/types/api_response_provider_key_item_out.py deleted file mode 100644 index bfff79a..0000000 --- a/src/onepin/types/api_response_provider_key_item_out.py +++ /dev/null @@ -1,22 +0,0 @@ -# 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 .provider_key_item_out import ProviderKeyItemOut - - -class ApiResponseProviderKeyItemOut(UniversalBaseModel): - data: ProviderKeyItemOut - 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/api_response_provider_keys_manifest_out.py b/src/onepin/types/api_response_provider_keys_manifest_out.py deleted file mode 100644 index 428ffc1..0000000 --- a/src/onepin/types/api_response_provider_keys_manifest_out.py +++ /dev/null @@ -1,22 +0,0 @@ -# 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 .provider_keys_manifest_out import ProviderKeysManifestOut - - -class ApiResponseProviderKeysManifestOut(UniversalBaseModel): - data: ProviderKeysManifestOut - 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/api_response_setup_intent_response.py b/src/onepin/types/api_response_setup_intent_response.py deleted file mode 100644 index 5171ff6..0000000 --- a/src/onepin/types/api_response_setup_intent_response.py +++ /dev/null @@ -1,22 +0,0 @@ -# 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 .setup_intent_response import SetupIntentResponse - - -class ApiResponseSetupIntentResponse(UniversalBaseModel): - data: SetupIntentResponse - 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/api_response_union_customer_subscription_response_none_type.py b/src/onepin/types/api_response_union_customer_subscription_response_none_type.py deleted file mode 100644 index 07df904..0000000 --- a/src/onepin/types/api_response_union_customer_subscription_response_none_type.py +++ /dev/null @@ -1,22 +0,0 @@ -# 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 .customer_subscription_response import CustomerSubscriptionResponse -from .meta import Meta - - -class ApiResponseUnionCustomerSubscriptionResponseNoneType(UniversalBaseModel): - data: typing.Optional[CustomerSubscriptionResponse] = None - 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/api_response_api_key_out.py b/src/onepin/types/api_response_voice_facets_out.py similarity index 80% rename from src/onepin/types/api_response_api_key_out.py rename to src/onepin/types/api_response_voice_facets_out.py index 11c5d18..7aa9b2b 100644 --- a/src/onepin/types/api_response_api_key_out.py +++ b/src/onepin/types/api_response_voice_facets_out.py @@ -4,12 +4,12 @@ import pydantic from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel -from .api_key_out import ApiKeyOut from .meta import Meta +from .voice_facets_out import VoiceFacetsOut -class ApiResponseApiKeyOut(UniversalBaseModel): - data: ApiKeyOut +class ApiResponseVoiceFacetsOut(UniversalBaseModel): + data: VoiceFacetsOut meta: Meta if IS_PYDANTIC_V2: diff --git a/src/onepin/types/api_response_workspace_runs_stats_out.py b/src/onepin/types/api_response_workspace_runs_stats_out.py deleted file mode 100644 index c892d5e..0000000 --- a/src/onepin/types/api_response_workspace_runs_stats_out.py +++ /dev/null @@ -1,22 +0,0 @@ -# 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 .workspace_runs_stats_out import WorkspaceRunsStatsOut - - -class ApiResponseWorkspaceRunsStatsOut(UniversalBaseModel): - data: WorkspaceRunsStatsOut - 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/api_response_workspace_workflows_stats_out.py b/src/onepin/types/api_response_workspace_workflows_stats_out.py deleted file mode 100644 index 37c55a5..0000000 --- a/src/onepin/types/api_response_workspace_workflows_stats_out.py +++ /dev/null @@ -1,22 +0,0 @@ -# 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 .workspace_workflows_stats_out import WorkspaceWorkflowsStatsOut - - -class ApiResponseWorkspaceWorkflowsStatsOut(UniversalBaseModel): - data: WorkspaceWorkflowsStatsOut - 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_model_out.py b/src/onepin/types/catalog_model_out.py index 05a0ba8..e736061 100644 --- a/src/onepin/types/catalog_model_out.py +++ b/src/onepin/types/catalog_model_out.py @@ -11,15 +11,18 @@ class CatalogModelOut(UniversalBaseModel): """ Catalog model entry — lean, customer-safe. - ``config_schema`` is the per-model TTS config JSON-schema (from the provider - registry's Pydantic config class). ``voice_count`` is the number of active - platform voices catalogued under this exact ``(provider, model)``. + ``config_schema`` is the per-model NATIVE TTS config JSON-schema. POD-670: + ``config_schema`` is now DEPRECATED as the FE slider source — read ``controls`` + instead (the provider-agnostic canonical supported-set). ``config_schema`` is + kept populated for back-compat until the FE migrates (POD-718). ``voice_count`` + is the number of active platform voices catalogued under this ``(provider, model)``. """ model: str display_name: str content_type: typing.Optional[str] = None config_schema: typing.Optional[typing.Dict[str, typing.Any]] = None + controls: typing.Optional[typing.Dict[str, typing.Any]] = None voice_count: int links: typing.Optional[typing.Dict[str, CatalogLink]] = None diff --git a/src/onepin/types/catalog_voice_out.py b/src/onepin/types/catalog_voice_out.py index a46102c..8c30f97 100644 --- a/src/onepin/types/catalog_voice_out.py +++ b/src/onepin/types/catalog_voice_out.py @@ -14,7 +14,7 @@ class CatalogVoiceOut(UniversalBaseModel): Lean voice entry for ``/providers/{p}/models/{m}/voices``. ``provider``/``model`` are implied by the path, so they are omitted here. - ``preview_url`` is a short-lived presigned S3 URL for the preview sample. + ``preview_url`` is a short-lived presigned URL for the preview sample. """ id: str @@ -25,6 +25,7 @@ class CatalogVoiceOut(UniversalBaseModel): accent: typing.Optional[VoiceAccent] = None supported_languages: typing.Optional[typing.List[str]] = None preview_url: typing.Optional[str] = None + emotion_options: 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/checkout_response.py b/src/onepin/types/checkout_response.py deleted file mode 100644 index 31ad9bf..0000000 --- a/src/onepin/types/checkout_response.py +++ /dev/null @@ -1,19 +0,0 @@ -# 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 CheckoutResponse(UniversalBaseModel): - client_secret: str - - 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/customer_plan_change_preview_response.py b/src/onepin/types/customer_plan_change_preview_response.py deleted file mode 100644 index 76bfa8a..0000000 --- a/src/onepin/types/customer_plan_change_preview_response.py +++ /dev/null @@ -1,25 +0,0 @@ -# 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 .customer_plan_response import CustomerPlanResponse - - -class CustomerPlanChangePreviewResponse(UniversalBaseModel): - current_plan: CustomerPlanResponse - new_plan: CustomerPlanResponse - type: str - proration_amount: typing.Optional[int] = None - effective_at: str - currency: str - - 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/customer_plan_response.py b/src/onepin/types/customer_plan_response.py deleted file mode 100644 index 7f8b24b..0000000 --- a/src/onepin/types/customer_plan_response.py +++ /dev/null @@ -1,29 +0,0 @@ -# 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 .plan_details import PlanDetails -from .plan_tier import PlanTier - - -class CustomerPlanResponse(UniversalBaseModel): - id: str - name: str - tier: PlanTier - limits: typing.Dict[str, typing.Any] - billing_interval: str - amount: int - currency: str - display_order: int - plan_details: typing.Optional[PlanDetails] = None - - 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/customer_subscription_response.py b/src/onepin/types/customer_subscription_response.py deleted file mode 100644 index 144d331..0000000 --- a/src/onepin/types/customer_subscription_response.py +++ /dev/null @@ -1,29 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -import pydantic -from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel -from .customer_plan_response import CustomerPlanResponse - - -class CustomerSubscriptionResponse(UniversalBaseModel): - id: str - plan: CustomerPlanResponse - status: str - current_period_start: dt.datetime - current_period_end: dt.datetime - cancel_at_period_end: bool - canceled_at: typing.Optional[dt.datetime] = None - scheduled_plan: typing.Optional[CustomerPlanResponse] = None - scheduled_at: typing.Optional[dt.datetime] = None - - 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/dictionary_language_out.py b/src/onepin/types/dictionary_language_out.py index ab30d80..0b74603 100644 --- a/src/onepin/types/dictionary_language_out.py +++ b/src/onepin/types/dictionary_language_out.py @@ -7,8 +7,15 @@ class DictionaryLanguageOut(UniversalBaseModel): - code: str - count: int + code: str = pydantic.Field() + """ + BCP-47 locale code (e.g. `ko-kr`, `en-us`). + """ + + count: int = pydantic.Field() + """ + Number of active dictionary entries for this locale. + """ 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/dictionary_out.py b/src/onepin/types/dictionary_out.py index 8fd6245..9679233 100644 --- a/src/onepin/types/dictionary_out.py +++ b/src/onepin/types/dictionary_out.py @@ -9,16 +9,56 @@ class DictionaryOut(UniversalBaseModel): - id: str - word: str - description: typing.Optional[str] = None - pronunciation: typing.Optional[str] = None - audio_url: typing.Optional[str] = None - method: DictionaryMethod - language: str - uses_count: int - ipa: typing.Optional[str] = None - created_by: typing.Optional[str] = None + id: str = pydantic.Field() + """ + Unique identifier for the dictionary entry. + """ + + word: str = pydantic.Field() + """ + The surface form of the word or phrase as it appears in a script. + """ + + description: typing.Optional[str] = pydantic.Field(default=None) + """ + Optional human-readable note about the entry (e.g. context, source). + """ + + pronunciation: typing.Optional[str] = pydantic.Field(default=None) + """ + Phonetic respelling used when `method` is `spelled`. + """ + + audio_url: typing.Optional[str] = pydantic.Field(default=None) + """ + Short-lived presigned URL to the reference audio clip; only present when `method` is `recorded`. Do not cache across sessions. + """ + + method: DictionaryMethod = pydantic.Field() + """ + How the pronunciation is specified: `spelled` (phonetic respelling), `recorded` (reference audio), or `ipa` (IPA transcription). + """ + + language: str = pydantic.Field() + """ + BCP-47 locale this entry applies to (e.g. `ko-kr`, `en-us`). + """ + + uses_count: int = pydantic.Field() + """ + Number of times this entry has been matched and applied during workflow execution. + """ + + ipa: typing.Optional[str] = pydantic.Field(default=None) + """ + IPA transcription of the word. Supplied by the caller; automatic generation is a planned enhancement. + """ + + created_by: typing.Optional[str] = pydantic.Field(default=None) + """ + ID of the user who created the entry, or `null` when created programmatically via API key. + """ + created_at: dt.datetime updated_at: dt.datetime diff --git a/src/onepin/types/download_url_out.py b/src/onepin/types/download_url_out.py index c19883f..ec16331 100644 --- a/src/onepin/types/download_url_out.py +++ b/src/onepin/types/download_url_out.py @@ -8,9 +8,20 @@ class DownloadUrlOut(UniversalBaseModel): - url: str - filename: str - expires_at: dt.datetime + url: str = pydantic.Field() + """ + Pre-signed download URL. Valid until `expires_at`; do not cache beyond that time. + """ + + filename: str = pydantic.Field() + """ + Suggested filename for the downloaded ZIP archive. + """ + + expires_at: dt.datetime = pydantic.Field() + """ + UTC datetime after which the URL is no longer valid (15 minutes from generation). + """ 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/email_notification_preferences_out.py b/src/onepin/types/email_notification_preferences_out.py index fa1eb60..0be94d3 100644 --- a/src/onepin/types/email_notification_preferences_out.py +++ b/src/onepin/types/email_notification_preferences_out.py @@ -11,8 +11,15 @@ class EmailNotificationPreferencesOut(UniversalBaseModel): Current user's email notification preferences. """ - completed_generation_email: bool - failed_generation_email: bool + completed_generation_email: bool = pydantic.Field() + """ + Send an email when a workflow run completes successfully. + """ + + failed_generation_email: bool = pydantic.Field() + """ + Send an email when a workflow run fails. + """ 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/invoice_list_response.py b/src/onepin/types/invoice_list_response.py deleted file mode 100644 index 500233e..0000000 --- a/src/onepin/types/invoice_list_response.py +++ /dev/null @@ -1,21 +0,0 @@ -# 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 .invoice_response import InvoiceResponse - - -class InvoiceListResponse(UniversalBaseModel): - invoices: typing.List[InvoiceResponse] - has_more: bool - - 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/invoice_response.py b/src/onepin/types/invoice_response.py deleted file mode 100644 index 3857adc..0000000 --- a/src/onepin/types/invoice_response.py +++ /dev/null @@ -1,29 +0,0 @@ -# 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 InvoiceResponse(UniversalBaseModel): - id: str - number: typing.Optional[str] = None - status: str - amount_due: int - amount_paid: int - currency: str - period_start: str - period_end: str - created: str - hosted_invoice_url: typing.Optional[str] = None - invoice_pdf: typing.Optional[str] = None - - 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/node_type.py b/src/onepin/types/node_type.py index 4794994..121623c 100644 --- a/src/onepin/types/node_type.py +++ b/src/onepin/types/node_type.py @@ -11,6 +11,7 @@ "sink_preview", "validator_error_rate", "validator_naturalness", + "validator_noise", ], typing.Any, ] diff --git a/src/onepin/types/payment_method_response.py b/src/onepin/types/payment_method_response.py deleted file mode 100644 index d2f2c30..0000000 --- a/src/onepin/types/payment_method_response.py +++ /dev/null @@ -1,24 +0,0 @@ -# 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 PaymentMethodResponse(UniversalBaseModel): - id: str - brand: str - last4: str - exp_month: int - exp_year: int - is_default: bool - - 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/plan_details.py b/src/onepin/types/plan_details.py deleted file mode 100644 index cc507e1..0000000 --- a/src/onepin/types/plan_details.py +++ /dev/null @@ -1,20 +0,0 @@ -# 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 .plan_details_section import PlanDetailsSection - - -class PlanDetails(UniversalBaseModel): - sections: typing.List[PlanDetailsSection] - - 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/plan_details_item.py b/src/onepin/types/plan_details_item.py deleted file mode 100644 index 4bdd40f..0000000 --- a/src/onepin/types/plan_details_item.py +++ /dev/null @@ -1,20 +0,0 @@ -# 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 PlanDetailsItem(UniversalBaseModel): - label: str - value: str - - 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/plan_details_section.py b/src/onepin/types/plan_details_section.py deleted file mode 100644 index c487440..0000000 --- a/src/onepin/types/plan_details_section.py +++ /dev/null @@ -1,21 +0,0 @@ -# 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 .plan_details_item import PlanDetailsItem - - -class PlanDetailsSection(UniversalBaseModel): - title: str - items: typing.List[PlanDetailsItem] - - 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/pronunciation_suggestion.py b/src/onepin/types/pronunciation_suggestion.py index b0e3e7d..e45ed97 100644 --- a/src/onepin/types/pronunciation_suggestion.py +++ b/src/onepin/types/pronunciation_suggestion.py @@ -7,8 +7,15 @@ class PronunciationSuggestion(UniversalBaseModel): - pronunciation: str - ipa: typing.Optional[str] = None + pronunciation: str = pydantic.Field() + """ + Suggested phonetic respelling, suitable for use as the `pronunciation` field when creating a `spelled`-method entry. + """ + + ipa: typing.Optional[str] = pydantic.Field(default=None) + """ + IPA transcription. Always `null` in this version; automatic generation is a planned enhancement. + """ 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/provider_key_item_out.py b/src/onepin/types/provider_key_item_out.py deleted file mode 100644 index e3ee2bb..0000000 --- a/src/onepin/types/provider_key_item_out.py +++ /dev/null @@ -1,28 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -import pydantic -from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel -from .provider_key_provider import ProviderKeyProvider -from .provider_key_status import ProviderKeyStatus - - -class ProviderKeyItemOut(UniversalBaseModel): - provider: ProviderKeyProvider - credentials_schema: typing.Dict[str, typing.Any] - configured: bool - is_valid: bool - validated_at: typing.Optional[dt.datetime] = None - key_preview: typing.Optional[str] = None - status: ProviderKeyStatus - - 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/provider_key_provider.py b/src/onepin/types/provider_key_provider.py deleted file mode 100644 index ee34fec..0000000 --- a/src/onepin/types/provider_key_provider.py +++ /dev/null @@ -1,7 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -ProviderKeyProvider = typing.Union[ - typing.Literal["elevenlabs", "cartesia", "naver", "google", "fish_audio", "rime", "respeecher"], typing.Any -] diff --git a/src/onepin/types/provider_key_status.py b/src/onepin/types/provider_key_status.py deleted file mode 100644 index eb2a043..0000000 --- a/src/onepin/types/provider_key_status.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -ProviderKeyStatus = typing.Union[typing.Literal["not_configured", "valid", "invalid"], typing.Any] diff --git a/src/onepin/types/provider_keys_manifest_out.py b/src/onepin/types/provider_keys_manifest_out.py deleted file mode 100644 index 851e7ba..0000000 --- a/src/onepin/types/provider_keys_manifest_out.py +++ /dev/null @@ -1,20 +0,0 @@ -# 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 .provider_key_item_out import ProviderKeyItemOut - - -class ProviderKeysManifestOut(UniversalBaseModel): - providers: typing.List[ProviderKeyItemOut] - - 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/runs_summary_out.py b/src/onepin/types/runs_summary_out.py index eadb21d..604741d 100644 --- a/src/onepin/types/runs_summary_out.py +++ b/src/onepin/types/runs_summary_out.py @@ -7,15 +7,50 @@ class RunsSummaryOut(UniversalBaseModel): - total_runs: int - completed: int - failed: int - cancelled: int - pending: int - running: int - paused: int - pass_rate: typing.Optional[float] = None - average_duration_seconds: typing.Optional[float] = None + total_runs: int = pydantic.Field() + """ + Total runs in the queried window. + """ + + completed: int = pydantic.Field() + """ + Runs that finished successfully. + """ + + failed: int = pydantic.Field() + """ + Runs that ended in a failure state. + """ + + cancelled: int = pydantic.Field() + """ + Runs explicitly cancelled by a user. + """ + + pending: int = pydantic.Field() + """ + Runs queued but not yet started. + """ + + running: int = pydantic.Field() + """ + Runs currently executing. + """ + + paused: int = pydantic.Field() + """ + Runs paused at a wave boundary. + """ + + pass_rate: typing.Optional[float] = pydantic.Field(default=None) + """ + Fraction of terminal runs that completed successfully: `completed / (completed + failed + cancelled)`. Null when there are no terminal runs. + """ + + average_duration_seconds: typing.Optional[float] = pydantic.Field(default=None) + """ + Mean wall-clock duration in seconds over completed runs only (`completed_at - started_at`). Null when no runs have completed. + """ 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/setup_intent_response.py b/src/onepin/types/setup_intent_response.py deleted file mode 100644 index b8a83bd..0000000 --- a/src/onepin/types/setup_intent_response.py +++ /dev/null @@ -1,19 +0,0 @@ -# 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 SetupIntentResponse(UniversalBaseModel): - client_secret: str - - 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/template_estimate_response.py b/src/onepin/types/template_estimate_response.py index 040782b..91893a3 100644 --- a/src/onepin/types/template_estimate_response.py +++ b/src/onepin/types/template_estimate_response.py @@ -19,23 +19,90 @@ class TemplateEstimateResponse(UniversalBaseModel): workflows still use `EstimateResponse` once real script text exists. """ - unit_chars: int - variable_min_credits_per_unit: int - variable_expected_credits_per_unit: int - variable_max_credits_per_unit: int - fixed_min_credits: int - fixed_expected_credits: int - fixed_max_credits: int - total_min_credits_per_unit: int - total_expected_credits_per_unit: int - total_max_credits_per_unit: int - breakdown: typing.List[NodeEstimate] - source_snapshot: TemplateEstimateResponseSourceSnapshot - source_node_ids: typing.List[str] - definition_fingerprint: str - rate_fingerprint: str - cache_status: TemplateEstimateResponseCacheStatus - computed_at: dt.datetime + unit_chars: int = pydantic.Field() + """ + Character count of the synthetic input used to compute the per-unit rates. Divide your expected script length by this value and multiply by the per-unit credit fields to project total cost. + """ + + variable_min_credits_per_unit: int = pydantic.Field() + """ + Minimum variable credits per `unit_chars` characters (best-case pricing, scales with script length). + """ + + variable_expected_credits_per_unit: int = pydantic.Field() + """ + Expected variable credits per `unit_chars` characters (typical pricing). + """ + + variable_max_credits_per_unit: int = pydantic.Field() + """ + Maximum variable credits per `unit_chars` characters (worst-case pricing). + """ + + fixed_min_credits: int = pydantic.Field() + """ + Minimum fixed credits charged once per run regardless of script length. + """ + + fixed_expected_credits: int = pydantic.Field() + """ + Expected fixed credits per run. + """ + + fixed_max_credits: int = pydantic.Field() + """ + Maximum fixed credits per run. + """ + + total_min_credits_per_unit: int = pydantic.Field() + """ + Total minimum credits per `unit_chars` (variable_min + fixed_min). + """ + + total_expected_credits_per_unit: int = pydantic.Field() + """ + Total expected credits per `unit_chars`. + """ + + total_max_credits_per_unit: int = pydantic.Field() + """ + Total maximum credits per `unit_chars`. + """ + + breakdown: typing.List[NodeEstimate] = pydantic.Field() + """ + Per-node credit breakdown showing which processing steps drive the cost. + """ + + source_snapshot: TemplateEstimateResponseSourceSnapshot = pydantic.Field() + """ + `draft` when the estimate is based on the owner's live definition; `published` when based on the gallery snapshot. + """ + + source_node_ids: typing.List[str] = pydantic.Field() + """ + IDs of the source nodes used to anchor the estimate calculation. + """ + + definition_fingerprint: str = pydantic.Field() + """ + Hash of the template definition at the time this estimate was computed. Changes when the workflow graph is modified. + """ + + rate_fingerprint: str = pydantic.Field() + """ + Hash of the pricing rates used. Changes when credit rates are updated. + """ + + cache_status: TemplateEstimateResponseCacheStatus = pydantic.Field() + """ + `hit` — served from cache; `miss` — computed fresh (no prior cache); `stale` — recomputed because the definition or rates changed. + """ + + computed_at: dt.datetime = pydantic.Field() + """ + UTC timestamp when this estimate was last computed. + """ 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/template_out.py b/src/onepin/types/template_out.py index efcf3da..271e7e7 100644 --- a/src/onepin/types/template_out.py +++ b/src/onepin/types/template_out.py @@ -20,18 +20,61 @@ class TemplateOut(UniversalBaseModel): `Workflow.definition`) — a template is a reusable workflow snapshot. """ - id: str - name: str - description: typing.Optional[str] = None - category: typing.Optional[TemplateCategory] = None - definition: WorkflowDefinitionOutput - is_starter: bool - is_public: bool - is_favorite: typing.Optional[bool] = None - uses_count: int - created_by: typing.Optional[str] = None + id: str = pydantic.Field() + """ + Unique template identifier. + """ + + name: str = pydantic.Field() + """ + Display name of the template. + """ + + description: typing.Optional[str] = pydantic.Field(default=None) + """ + Optional human-readable description. + """ + + category: typing.Optional[TemplateCategory] = pydantic.Field(default=None) + """ + Gallery category tag, if set. + """ + + definition: WorkflowDefinitionOutput = pydantic.Field() + """ + Full workflow definition (graph + execution config). Use this directly as the `definition` body when creating a workflow from scratch, or clone via `POST /templates/{id}/clone`. + """ + + is_starter: bool = pydantic.Field() + """ + `true` for platform-curated starter templates. Starter templates cannot be updated or deleted. + """ + + is_public: bool = pydantic.Field() + """ + `true` when this template has an active published snapshot visible in the gallery. + """ + + is_favorite: typing.Optional[bool] = pydantic.Field(default=None) + """ + `true` when the authenticated caller has favorited this template. + """ + + uses_count: int = pydantic.Field() + """ + Number of times this template has been cloned into a workflow. + """ + + created_by: typing.Optional[str] = pydantic.Field(default=None) + """ + User ID of the template author. + """ + created_at: dt.datetime - updated_at: dt.datetime + updated_at: dt.datetime = pydantic.Field() + """ + Last-modified timestamp. For gallery rows this reflects the most recent publish; for own-workspace rows it reflects the most recent draft save. + """ 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/upload_create_response.py b/src/onepin/types/upload_create_response.py index 683f69b..c0be02e 100644 --- a/src/onepin/types/upload_create_response.py +++ b/src/onepin/types/upload_create_response.py @@ -8,8 +8,15 @@ class UploadCreateResponse(UniversalBaseModel): - upload: UploadOut - upload_url: str + upload: UploadOut = pydantic.Field() + """ + The newly created upload record in `pending` status. + """ + + upload_url: str = pydantic.Field() + """ + Short-lived presigned URL. PUT your file bytes directly to this URL to complete the upload. Do not send the file to this API. After the PUT succeeds, call `POST /uploads/{id}` to confirm. + """ 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/upload_out.py b/src/onepin/types/upload_out.py index b3d5b42..c96ba9d 100644 --- a/src/onepin/types/upload_out.py +++ b/src/onepin/types/upload_out.py @@ -8,18 +8,66 @@ class UploadOut(UniversalBaseModel): - id: str - user_id: str - workspace_id: typing.Optional[str] = None - filename: str - category: str - content_type: str - format: typing.Optional[str] = None - status: str - size_bytes: typing.Optional[int] = None - download_url: typing.Optional[str] = None - context_type: typing.Optional[str] = None - context_id: typing.Optional[str] = None + id: str = pydantic.Field() + """ + Unique upload identifier. Use this as `upload_id` in confirm and delete calls. + """ + + user_id: str = pydantic.Field() + """ + ID of the user who created the upload. + """ + + workspace_id: typing.Optional[str] = pydantic.Field(default=None) + """ + Workspace this upload is scoped to, if any. Set at create time via `X-Workspace-Id` or derived from the bound resource at confirm time. + """ + + filename: str = pydantic.Field() + """ + Sanitized display filename (Unicode-safe, extension preserved). + """ + + category: str = pydantic.Field() + """ + Upload category: `script` or `dictionary`. + """ + + content_type: str = pydantic.Field() + """ + MIME type inferred from the file extension at create time. + """ + + format: typing.Optional[str] = pydantic.Field(default=None) + """ + Normalized format identifier (e.g. `mp3`, `txt`), if applicable. + """ + + status: str = pydantic.Field() + """ + `pending` until confirmed; `uploaded` after a successful confirm call. + """ + + size_bytes: typing.Optional[int] = pydantic.Field(default=None) + """ + File size in bytes. Populated after a successful confirm. + """ + + download_url: typing.Optional[str] = pydantic.Field(default=None) + """ + Short-lived presigned URL for downloading the confirmed file. `null` for pending uploads. + """ + + context_type: typing.Optional[str] = pydantic.Field(default=None) + """ + Resource type this upload is attached to (e.g. `workflow`). Set at confirm time. + """ + + context_id: typing.Optional[str] = pydantic.Field(default=None) + """ + ID of the attached resource. Set at confirm time. + """ + created_at: dt.datetime updated_at: dt.datetime diff --git a/src/onepin/types/usage_activity_bucket_out.py b/src/onepin/types/usage_activity_bucket_out.py index b6a8e48..e39f25d 100644 --- a/src/onepin/types/usage_activity_bucket_out.py +++ b/src/onepin/types/usage_activity_bucket_out.py @@ -8,13 +8,40 @@ class UsageActivityBucketOut(UniversalBaseModel): - label: str - start: dt.datetime - end: dt.datetime - credits: int - characters: int - lines: int - runs: int + label: str = pydantic.Field() + """ + Human-readable label for the bucket period (e.g. `Mon`, `Jan`, `Week 1`). + """ + + start: dt.datetime = pydantic.Field() + """ + Start of the bucket period (inclusive), in UTC. + """ + + end: dt.datetime = pydantic.Field() + """ + End of the bucket period (exclusive), in UTC. + """ + + credits: int = pydantic.Field() + """ + Credits consumed in this bucket. + """ + + characters: int = pydantic.Field() + """ + Characters processed in this bucket. + """ + + lines: int = pydantic.Field() + """ + Script lines generated in this bucket. + """ + + runs: int = pydantic.Field() + """ + Workflow runs in this bucket. + """ 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/usage_activity_out.py b/src/onepin/types/usage_activity_out.py index 0118fde..b1cdaaf 100644 --- a/src/onepin/types/usage_activity_out.py +++ b/src/onepin/types/usage_activity_out.py @@ -11,14 +11,45 @@ class UsageActivityOut(UniversalBaseModel): - id: str - timestamp: dt.datetime - user: typing.Optional[UsageActivityUserOut] = None - action: UsageActivityAction - kind: str - resource: UsageActivityResourceOut - metadata: typing.Optional[typing.Dict[str, typing.Any]] = None - ip: typing.Optional[str] = None + id: str = pydantic.Field() + """ + Unique activity event identifier. + """ + + timestamp: dt.datetime = pydantic.Field() + """ + UTC timestamp when the event occurred. + """ + + user: typing.Optional[UsageActivityUserOut] = pydantic.Field(default=None) + """ + Actor who triggered the event, or `null` for system-generated events. + """ + + action: UsageActivityAction = pydantic.Field() + """ + Event type (e.g. `workflow_run`, `voice_generated`, `member_invited`). + """ + + kind: str = pydantic.Field() + """ + Sub-kind providing additional detail about the action. + """ + + resource: UsageActivityResourceOut = pydantic.Field() + """ + The primary resource involved in this event. + """ + + metadata: typing.Optional[typing.Dict[str, typing.Any]] = pydantic.Field(default=None) + """ + Additional structured context for the event. Keys vary by action type. + """ + + ip: typing.Optional[str] = pydantic.Field(default=None) + """ + IP address of the actor at the time of the event, if recorded. + """ 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/usage_activity_resource_out.py b/src/onepin/types/usage_activity_resource_out.py index c04fbf3..126fa21 100644 --- a/src/onepin/types/usage_activity_resource_out.py +++ b/src/onepin/types/usage_activity_resource_out.py @@ -7,9 +7,20 @@ class UsageActivityResourceOut(UniversalBaseModel): - type: str - id: typing.Optional[str] = None - name: typing.Optional[str] = None + type: str = pydantic.Field() + """ + Resource type involved in the event (e.g. `workflow`, `template`, `voice`). + """ + + id: typing.Optional[str] = pydantic.Field(default=None) + """ + ID of the affected resource, if applicable. + """ + + name: typing.Optional[str] = pydantic.Field(default=None) + """ + Display name of the affected resource at the time of the event. + """ 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/usage_activity_summary_out.py b/src/onepin/types/usage_activity_summary_out.py index 94bff8e..2acc887 100644 --- a/src/onepin/types/usage_activity_summary_out.py +++ b/src/onepin/types/usage_activity_summary_out.py @@ -11,10 +11,26 @@ class UsageActivitySummaryOut(UniversalBaseModel): - view: UsageActivitySummaryOutView - bucket_unit: UsageActivitySummaryOutBucketUnit - range_label: str - buckets: typing.Optional[typing.List[UsageActivityBucketOut]] = None + view: UsageActivitySummaryOutView = pydantic.Field() + """ + The activity view period applied: `daily`, `weekly`, or `monthly`. + """ + + bucket_unit: UsageActivitySummaryOutBucketUnit = pydantic.Field() + """ + Time unit for each bucket: `day`, `week`, or `month`. + """ + + range_label: str = pydantic.Field() + """ + Human-readable label for the chart range (e.g. `Last 7 days`, `Last 12 weeks`). + """ + + buckets: typing.Optional[typing.List[UsageActivityBucketOut]] = pydantic.Field(default=None) + """ + Ordered list of time buckets covering the activity view period, newest last. + """ + total: int = pydantic.Field() """ Total generated line count across all activity buckets. @@ -25,7 +41,10 @@ class UsageActivitySummaryOut(UniversalBaseModel): Average generated line count per activity bucket, rounded to 1 decimal. """ - peak: typing.Optional[UsageActivityPeakOut] = None + peak: typing.Optional[UsageActivityPeakOut] = pydantic.Field(default=None) + """ + The bucket with the highest line count, or `null` when there is no activity. + """ 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/usage_activity_user_out.py b/src/onepin/types/usage_activity_user_out.py index 6a32216..24eeb57 100644 --- a/src/onepin/types/usage_activity_user_out.py +++ b/src/onepin/types/usage_activity_user_out.py @@ -7,8 +7,15 @@ class UsageActivityUserOut(UniversalBaseModel): - id: str - name: str + id: str = pydantic.Field() + """ + User ID of the event actor. + """ + + name: str = pydantic.Field() + """ + Display name of the event actor. + """ 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/usage_by_language_out.py b/src/onepin/types/usage_by_language_out.py index dfe3a13..4fbe963 100644 --- a/src/onepin/types/usage_by_language_out.py +++ b/src/onepin/types/usage_by_language_out.py @@ -16,12 +16,35 @@ class UsageByLanguageOut(UniversalBaseModel): Rolling range used for rows, or null when activity_view supplies the effective period. """ - activity_view: typing.Optional[UsageByLanguageOutActivityView] = None - timezone: str - period: UsagePeriodOut - languages: typing.Optional[typing.List[UsageLanguageRowOut]] = None - total_credits: typing.Optional[int] = None - untagged_credits: typing.Optional[int] = None + activity_view: typing.Optional[UsageByLanguageOutActivityView] = pydantic.Field(default=None) + """ + Activity view period applied when `range` is null. + """ + + timezone: str = pydantic.Field() + """ + IANA timezone used for period boundary computation. + """ + + period: UsagePeriodOut = pydantic.Field() + """ + Absolute UTC start and end of the reporting period. + """ + + languages: typing.Optional[typing.List[UsageLanguageRowOut]] = pydantic.Field(default=None) + """ + Per-language rows ordered by credit consumption descending. + """ + + total_credits: typing.Optional[int] = pydantic.Field(default=None) + """ + Total credits across all languages in the period. + """ + + untagged_credits: typing.Optional[int] = pydantic.Field(default=None) + """ + Credits that could not be attributed to a specific language. + """ 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/usage_characters_out.py b/src/onepin/types/usage_characters_out.py index b4b9e43..17afcf8 100644 --- a/src/onepin/types/usage_characters_out.py +++ b/src/onepin/types/usage_characters_out.py @@ -7,7 +7,10 @@ class UsageCharactersOut(UniversalBaseModel): - total: int + total: int = pydantic.Field() + """ + Total characters processed across all workflow runs in the period. + """ 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/usage_credits_out.py b/src/onepin/types/usage_credits_out.py index 80af669..e2ac1a2 100644 --- a/src/onepin/types/usage_credits_out.py +++ b/src/onepin/types/usage_credits_out.py @@ -12,8 +12,15 @@ class UsageCreditsOut(UniversalBaseModel): Credits used for the authenticated user's current billing period; daily and activity buckets remain workspace-scoped. """ - quota: typing.Optional[int] = None - percent: typing.Optional[float] = None + quota: typing.Optional[int] = pydantic.Field(default=None) + """ + Credit quota for the current billing period, or `null` if unlimited. + """ + + percent: typing.Optional[float] = pydantic.Field(default=None) + """ + Credits used as a fraction of quota (0–100), or `null` when quota is unlimited. + """ 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/usage_daily_out.py b/src/onepin/types/usage_daily_out.py index c082822..e241861 100644 --- a/src/onepin/types/usage_daily_out.py +++ b/src/onepin/types/usage_daily_out.py @@ -7,11 +7,30 @@ class UsageDailyOut(UniversalBaseModel): - date: str - credits: int - characters: int - lines: int - runs: int + date: str = pydantic.Field() + """ + Local calendar date for this bucket in `YYYY-MM-DD` format, computed in the requested timezone. + """ + + credits: int = pydantic.Field() + """ + Credits consumed on this date. + """ + + characters: int = pydantic.Field() + """ + Characters processed on this date. + """ + + lines: int = pydantic.Field() + """ + Script lines generated on this date. + """ + + runs: int = pydantic.Field() + """ + Workflow runs started on this date. + """ 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/usage_language_row_out.py b/src/onepin/types/usage_language_row_out.py index 7e283f0..945fc1a 100644 --- a/src/onepin/types/usage_language_row_out.py +++ b/src/onepin/types/usage_language_row_out.py @@ -7,17 +7,40 @@ class UsageLanguageRowOut(UniversalBaseModel): - locale_code: str - display_name: str + locale_code: str = pydantic.Field() + """ + BCP-47 locale code (e.g. `en-us`, `ko-kr`). + """ + + display_name: str = pydantic.Field() + """ + Human-readable language name (e.g. `English (US)`). + """ + share: float = pydantic.Field() """ - Fractional share in the range 0..1; multiply by 100 for percent display. + Fractional share of workspace usage in the range 0..1; multiply by 100 for percent display. + """ + + credits: int = pydantic.Field() + """ + Credits consumed for this language in the period. """ - credits: int - characters: int - lines: int - allocation_basis: str + characters: int = pydantic.Field() + """ + Characters processed for this language in the period. + """ + + lines: int = pydantic.Field() + """ + Script lines generated for this language in the period. + """ + + allocation_basis: str = pydantic.Field() + """ + How credit share is allocated for this language (e.g. `characters`). + """ 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/usage_lines_out.py b/src/onepin/types/usage_lines_out.py index c04fde7..db16251 100644 --- a/src/onepin/types/usage_lines_out.py +++ b/src/onepin/types/usage_lines_out.py @@ -7,8 +7,15 @@ class UsageLinesOut(UniversalBaseModel): - total: int - avg_chars_per_line: typing.Optional[float] = None + total: int = pydantic.Field() + """ + Total script lines generated in the period. + """ + + avg_chars_per_line: typing.Optional[float] = pydantic.Field(default=None) + """ + Average character count per generated line, or `null` when no lines exist. + """ 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/usage_period_out.py b/src/onepin/types/usage_period_out.py index 82ac398..3f96bf0 100644 --- a/src/onepin/types/usage_period_out.py +++ b/src/onepin/types/usage_period_out.py @@ -8,8 +8,15 @@ class UsagePeriodOut(UniversalBaseModel): - start: dt.datetime - end: dt.datetime + start: dt.datetime = pydantic.Field() + """ + Start of the reporting period (inclusive), in UTC. + """ + + end: dt.datetime = pydantic.Field() + """ + End of the reporting period (exclusive), in UTC. + """ 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/usage_runs_out.py b/src/onepin/types/usage_runs_out.py index 18188de..dd348e1 100644 --- a/src/onepin/types/usage_runs_out.py +++ b/src/onepin/types/usage_runs_out.py @@ -7,13 +7,40 @@ class UsageRunsOut(UniversalBaseModel): - total: int - completed: int - failed: int - cancelled: int - running: int - pending: int - paused: typing.Optional[int] = None + total: int = pydantic.Field() + """ + Total workflow runs initiated in the period. + """ + + completed: int = pydantic.Field() + """ + Runs that finished successfully. + """ + + failed: int = pydantic.Field() + """ + Runs that terminated with an error. + """ + + cancelled: int = pydantic.Field() + """ + Runs that were cancelled by the user or system. + """ + + running: int = pydantic.Field() + """ + Runs currently in progress. + """ + + pending: int = pydantic.Field() + """ + Runs queued but not yet started. + """ + + paused: typing.Optional[int] = pydantic.Field(default=None) + """ + Runs currently paused awaiting manual review. + """ 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/usage_summary_out.py b/src/onepin/types/usage_summary_out.py index 6631f43..24a0331 100644 --- a/src/onepin/types/usage_summary_out.py +++ b/src/onepin/types/usage_summary_out.py @@ -16,16 +16,55 @@ class UsageSummaryOut(UniversalBaseModel): - range: UsageSummaryOutRange - activity_view: UsageSummaryOutActivityView - timezone: str - period: UsagePeriodOut - credits: UsageCreditsOut - characters: UsageCharactersOut - lines: UsageLinesOut - runs: UsageRunsOut - activity: UsageActivitySummaryOut - daily: typing.Optional[typing.List[UsageDailyOut]] = None + range: UsageSummaryOutRange = pydantic.Field() + """ + Rolling window applied to aggregate totals: `30d`, `60d`, or `90d`. + """ + + activity_view: UsageSummaryOutActivityView = pydantic.Field() + """ + Chart bucketing period applied to the `activity` series. + """ + + timezone: str = pydantic.Field() + """ + IANA timezone used for local day/week/month boundary computation. + """ + + period: UsagePeriodOut = pydantic.Field() + """ + Absolute UTC start and end of the rolling window. + """ + + credits: UsageCreditsOut = pydantic.Field() + """ + Credit consumption and quota for the authenticated user's billing period. + """ + + characters: UsageCharactersOut = pydantic.Field() + """ + Total characters processed across the workspace in the rolling window. + """ + + lines: UsageLinesOut = pydantic.Field() + """ + Total script lines generated across the workspace in the rolling window. + """ + + runs: UsageRunsOut = pydantic.Field() + """ + Workflow run counts by status across the workspace in the rolling window. + """ + + activity: UsageActivitySummaryOut = pydantic.Field() + """ + Bucketed activity chart data for the selected `activity_view`. + """ + + daily: typing.Optional[typing.List[UsageDailyOut]] = pydantic.Field(default=None) + """ + Per-calendar-day breakdown for the rolling window, ordered oldest first. + """ 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/voice_facet_item.py b/src/onepin/types/voice_facet_item.py new file mode 100644 index 0000000..dee2a2b --- /dev/null +++ b/src/onepin/types/voice_facet_item.py @@ -0,0 +1,41 @@ +# 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 VoiceFacetItem(UniversalBaseModel): + """ + One selectable filter option (chip) for the voice browser filter bar. + + ``value`` is the exact token the caller passes back to ``GET /voices`` + (a provider/model key, a lowercase BCP-47 locale, or an enum value like + ``female``). ``label`` is the display name for provider/model facets and + ``None`` for languages and enum dimensions — the FE holds those labels (it + renders locale flags/names from the code via ``Intl``). ``count`` is the + number of voices matching ``value`` under the current request context + (tab/workspace scope + every OTHER active filter — see ``VoiceFacetsOut``). + + For the ``languages`` and ``models`` dimensions ``count`` reflects only voices + that *explicitly declare* ``value`` in their ``supported_languages`` / + ``supported_models`` array. It does NOT include "general-use" platform voices + (a catalog gap — platform voices with no declared locales/models) which + ``GET /voices`` matches against *every* ``language`` / ``model`` filter. So a + chip's ``count`` can be lower than the row count + ``GET /voices?language=`` / ``?model=`` actually returns. + """ + + value: str + label: typing.Optional[str] = None + count: int + + 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/voice_facets_out.py b/src/onepin/types/voice_facets_out.py new file mode 100644 index 0000000..f15d356 --- /dev/null +++ b/src/onepin/types/voice_facets_out.py @@ -0,0 +1,48 @@ +# 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 .voice_facet_item import VoiceFacetItem + + +class VoiceFacetsOut(UniversalBaseModel): + """ + Filter options for the voice browser, one ``VoiceFacetItem[]`` per chip. + + Two families of dimension: + + * **Data-driven** — ``providers``, ``models``, ``languages``: only values + actually present in the scoped voices are returned (count is always ≥ 1; + count-0 values are omitted). Every value is guaranteed to be a valid + ``GET /voices`` filter (provider/model restricted to the enabled catalog, + language to the supported-locale allowlist), so selecting one never yields + a 422 or empty page. Sorted count DESC, then value ASC. + * **Enum** — ``genders``, ``ages``, ``categories``, ``accents``: the FULL + fixed enum is always returned in natural enum order, including count-0 + values (the FE greys those out). ``label`` is ``None`` (the FE owns enum + labels). + + ``count`` is context-aware (faceted search): each dimension's counts apply + every OTHER active filter but exclude that dimension's own selection, so a + chip's number reflects "results if I also pick this" without the dimension + suppressing its own alternatives. + """ + + providers: typing.List[VoiceFacetItem] + models: typing.List[VoiceFacetItem] + languages: typing.List[VoiceFacetItem] + genders: typing.List[VoiceFacetItem] + ages: typing.List[VoiceFacetItem] + categories: typing.List[VoiceFacetItem] + accents: typing.List[VoiceFacetItem] + + 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/voice_out.py b/src/onepin/types/voice_out.py index 23887a6..5f4409c 100644 --- a/src/onepin/types/voice_out.py +++ b/src/onepin/types/voice_out.py @@ -13,29 +13,120 @@ class VoiceOut(UniversalBaseModel): - id: str - name: str - provider: str - provider_voice_id: str - model: typing.Optional[str] = None - description: typing.Optional[str] = None - is_active: bool - gender: typing.Optional[VoiceGender] = None - accent: typing.Optional[VoiceAccent] = None - age: typing.Optional[VoiceAge] = None - category: typing.Optional[VoiceCategory] = None - color: typing.Optional[str] = None - tags: typing.Optional[typing.List[str]] = None - descriptor: typing.Optional[str] = None - uses_count: typing.Optional[int] = None - user_id: typing.Optional[str] = None - source: typing.Optional[VoiceSource] = None - duration_seconds: typing.Optional[float] = None - sample_url: typing.Optional[str] = None - supported_languages: typing.Optional[typing.List[str]] = None - is_favorite: typing.Optional[bool] = None - created_at: dt.datetime - updated_at: dt.datetime + id: str = pydantic.Field() + """ + Unique voice identifier. + """ + + name: str = pydantic.Field() + """ + Display name of the voice. + """ + + provider: str = pydantic.Field() + """ + Speech synthesis provider code for this voice. + """ + + provider_voice_id: str = pydantic.Field() + """ + Provider-assigned voice identifier used when submitting synthesis requests. + """ + + description: typing.Optional[str] = pydantic.Field(default=None) + """ + Human-readable description of the voice's character and style. + """ + + is_active: bool = pydantic.Field() + """ + Whether the voice is available for use. Inactive voices are not returned by list or synthesis endpoints. + """ + + gender: typing.Optional[VoiceGender] = pydantic.Field(default=None) + """ + Perceived gender presentation of the voice. + """ + + accent: typing.Optional[VoiceAccent] = pydantic.Field(default=None) + """ + Accent of the voice, if classified. + """ + + age: typing.Optional[VoiceAge] = pydantic.Field(default=None) + """ + Perceived age range of the voice, if classified. + """ + + category: typing.Optional[VoiceCategory] = pydantic.Field(default=None) + """ + Intended use-case category (e.g. narration, conversational). + """ + + color: typing.Optional[str] = pydantic.Field(default=None) + """ + Brand color associated with the voice in the UI, as a hex string. + """ + + tags: typing.Optional[typing.List[str]] = pydantic.Field(default=None) + """ + Freeform keyword tags for filtering and search. + """ + + descriptor: typing.Optional[str] = pydantic.Field(default=None) + """ + Short one-line voice personality descriptor. + """ + + uses_count: typing.Optional[int] = pydantic.Field(default=None) + """ + Number of times this voice has been used in workflow runs across the platform. + """ + + user_id: typing.Optional[str] = pydantic.Field(default=None) + """ + Owner user ID for workspace-cloned or user-created voices. Null for platform voices. + """ + + source: typing.Optional[VoiceSource] = pydantic.Field(default=None) + """ + Origin of the voice: `platform` for system-provided voices, `workspace` for voices added or cloned by the workspace. + """ + + duration_seconds: typing.Optional[float] = pydantic.Field(default=None) + """ + Duration of the audio sample in seconds, if available. + """ + + sample_url: typing.Optional[str] = pydantic.Field(default=None) + """ + Time-limited presigned URL for the audio preview sample. Valid for 1 hour; regenerate by fetching the voice again. + """ + + supported_languages: typing.Optional[typing.List[str]] = pydantic.Field(default=None) + """ + BCP-47 language codes this voice supports. Null for platform voices means the voice is treated as general-use across all locales. + """ + + supported_models: typing.Optional[typing.List[str]] = pydantic.Field(default=None) + """ + Model identifiers this voice is compatible with. Null means compatible with all available models for the provider. + """ + + is_favorite: typing.Optional[bool] = pydantic.Field(default=None) + """ + Whether this voice is in the current workspace's favorites list. + """ + + created_at: dt.datetime = pydantic.Field() + """ + When the voice was added to the platform or workspace. + """ + + updated_at: dt.datetime = pydantic.Field() + """ + When the voice record was last updated. + """ 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/voice_similar_out.py b/src/onepin/types/voice_similar_out.py index 87bbf1f..ee5c80f 100644 --- a/src/onepin/types/voice_similar_out.py +++ b/src/onepin/types/voice_similar_out.py @@ -13,30 +13,125 @@ class VoiceSimilarOut(UniversalBaseModel): - id: str - name: str - provider: str - provider_voice_id: str - model: typing.Optional[str] = None - description: typing.Optional[str] = None - is_active: bool - gender: typing.Optional[VoiceGender] = None - accent: typing.Optional[VoiceAccent] = None - age: typing.Optional[VoiceAge] = None - category: typing.Optional[VoiceCategory] = None - color: typing.Optional[str] = None - tags: typing.Optional[typing.List[str]] = None - descriptor: typing.Optional[str] = None - uses_count: typing.Optional[int] = None - user_id: typing.Optional[str] = None - source: typing.Optional[VoiceSource] = None - duration_seconds: typing.Optional[float] = None - sample_url: typing.Optional[str] = None - supported_languages: typing.Optional[typing.List[str]] = None - is_favorite: typing.Optional[bool] = None - created_at: dt.datetime - updated_at: dt.datetime - similarity_score: float + id: str = pydantic.Field() + """ + Unique voice identifier. + """ + + name: str = pydantic.Field() + """ + Display name of the voice. + """ + + provider: str = pydantic.Field() + """ + Speech synthesis provider code for this voice. + """ + + provider_voice_id: str = pydantic.Field() + """ + Provider-assigned voice identifier used when submitting synthesis requests. + """ + + description: typing.Optional[str] = pydantic.Field(default=None) + """ + Human-readable description of the voice's character and style. + """ + + is_active: bool = pydantic.Field() + """ + Whether the voice is available for use. Inactive voices are not returned by list or synthesis endpoints. + """ + + gender: typing.Optional[VoiceGender] = pydantic.Field(default=None) + """ + Perceived gender presentation of the voice. + """ + + accent: typing.Optional[VoiceAccent] = pydantic.Field(default=None) + """ + Accent of the voice, if classified. + """ + + age: typing.Optional[VoiceAge] = pydantic.Field(default=None) + """ + Perceived age range of the voice, if classified. + """ + + category: typing.Optional[VoiceCategory] = pydantic.Field(default=None) + """ + Intended use-case category (e.g. narration, conversational). + """ + + color: typing.Optional[str] = pydantic.Field(default=None) + """ + Brand color associated with the voice in the UI, as a hex string. + """ + + tags: typing.Optional[typing.List[str]] = pydantic.Field(default=None) + """ + Freeform keyword tags for filtering and search. + """ + + descriptor: typing.Optional[str] = pydantic.Field(default=None) + """ + Short one-line voice personality descriptor. + """ + + uses_count: typing.Optional[int] = pydantic.Field(default=None) + """ + Number of times this voice has been used in workflow runs across the platform. + """ + + user_id: typing.Optional[str] = pydantic.Field(default=None) + """ + Owner user ID for workspace-cloned or user-created voices. Null for platform voices. + """ + + source: typing.Optional[VoiceSource] = pydantic.Field(default=None) + """ + Origin of the voice: `platform` for system-provided voices, `workspace` for voices added or cloned by the workspace. + """ + + duration_seconds: typing.Optional[float] = pydantic.Field(default=None) + """ + Duration of the audio sample in seconds, if available. + """ + + sample_url: typing.Optional[str] = pydantic.Field(default=None) + """ + Time-limited presigned URL for the audio preview sample. Valid for 1 hour; regenerate by fetching the voice again. + """ + + supported_languages: typing.Optional[typing.List[str]] = pydantic.Field(default=None) + """ + BCP-47 language codes this voice supports. Null for platform voices means the voice is treated as general-use across all locales. + """ + + supported_models: typing.Optional[typing.List[str]] = pydantic.Field(default=None) + """ + Model identifiers this voice is compatible with. Null means compatible with all available models for the provider. + """ + + is_favorite: typing.Optional[bool] = pydantic.Field(default=None) + """ + Whether this voice is in the current workspace's favorites list. + """ + + created_at: dt.datetime = pydantic.Field() + """ + When the voice was added to the platform or workspace. + """ + + updated_at: dt.datetime = pydantic.Field() + """ + When the voice record was last updated. + """ + + similarity_score: float = pydantic.Field() + """ + Acoustic similarity to the reference voice, from 0 (least similar) to 1 (identical). + """ 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_list_item.py b/src/onepin/types/workflow_list_item.py index 52fbb03..b745eae 100644 --- a/src/onepin/types/workflow_list_item.py +++ b/src/onepin/types/workflow_list_item.py @@ -8,17 +8,49 @@ class WorkflowListItem(UniversalBaseModel): - id: str - user_id: str - name: str - description: typing.Optional[str] = None - created_at: dt.datetime - updated_at: dt.datetime - runs_count: typing.Optional[int] = None - last_run_at: typing.Optional[dt.datetime] = None + id: str = pydantic.Field() + """ + Unique workflow identifier. + """ + + user_id: str = pydantic.Field() + """ + ID of the user who created the workflow. + """ + + name: str = pydantic.Field() + """ + Human-readable workflow name. + """ + + description: typing.Optional[str] = pydantic.Field(default=None) + """ + Optional workflow description. + """ + + created_at: dt.datetime = pydantic.Field() + """ + When the workflow was created (UTC). + """ + + updated_at: dt.datetime = pydantic.Field() + """ + When the workflow was last modified (UTC). + """ + + runs_count: typing.Optional[int] = pydantic.Field(default=None) + """ + Total number of runs ever started for this workflow. + """ + + last_run_at: typing.Optional[dt.datetime] = pydantic.Field(default=None) + """ + When the most recent run was created. Null if never run. + """ + last_run_status: typing.Optional[str] = pydantic.Field(default=None) """ - Raw RunStatus of the most recent run. One of: pending, running, completed, failed, cancelled. + Raw RunStatus of the most recent run. One of: `pending`, `running`, `completed`, `failed`, `cancelled`, `paused`. Null if never run. """ if IS_PYDANTIC_V2: diff --git a/src/onepin/types/workflow_out.py b/src/onepin/types/workflow_out.py index 8550ce2..46e9e87 100644 --- a/src/onepin/types/workflow_out.py +++ b/src/onepin/types/workflow_out.py @@ -8,18 +8,54 @@ class WorkflowOut(UniversalBaseModel): - id: str - user_id: str - name: str - description: typing.Optional[str] = None - definition: typing.Dict[str, typing.Any] - created_at: dt.datetime - updated_at: dt.datetime - runs_count: typing.Optional[int] = None - last_run_at: typing.Optional[dt.datetime] = None + id: str = pydantic.Field() + """ + Unique workflow identifier. + """ + + user_id: str = pydantic.Field() + """ + ID of the user who created the workflow. + """ + + name: str = pydantic.Field() + """ + Human-readable workflow name. + """ + + description: typing.Optional[str] = pydantic.Field(default=None) + """ + Optional workflow description. + """ + + definition: typing.Dict[str, typing.Any] = pydantic.Field() + """ + Full workflow graph and execution config as stored (config-migrated on read). + """ + + created_at: dt.datetime = pydantic.Field() + """ + When the workflow was created (UTC). + """ + + updated_at: dt.datetime = pydantic.Field() + """ + When the workflow was last modified (UTC). + """ + + runs_count: typing.Optional[int] = pydantic.Field(default=None) + """ + Total number of runs ever started for this workflow. + """ + + last_run_at: typing.Optional[dt.datetime] = pydantic.Field(default=None) + """ + When the most recent run was created. Null if never run. + """ + last_run_status: typing.Optional[str] = pydantic.Field(default=None) """ - Raw RunStatus of the most recent run. One of: pending, running, completed, failed, cancelled. + Raw RunStatus of the most recent run. One of: `pending`, `running`, `completed`, `failed`, `cancelled`, `paused`. Null if never run. """ if IS_PYDANTIC_V2: diff --git a/src/onepin/types/workspace_invite_out.py b/src/onepin/types/workspace_invite_out.py index 3226a92..9668030 100644 --- a/src/onepin/types/workspace_invite_out.py +++ b/src/onepin/types/workspace_invite_out.py @@ -8,13 +8,40 @@ class WorkspaceInviteOut(UniversalBaseModel): - id: str - email: str - role: str - status: str - expires_at: dt.datetime - invited_by: str - created_at: dt.datetime + id: str = pydantic.Field() + """ + Unique invite ID. + """ + + email: str = pydantic.Field() + """ + Email address the invite was sent to. + """ + + role: str = pydantic.Field() + """ + Role the invitee will receive on acceptance: `admin`, `editor`, or `viewer`. + """ + + status: str = pydantic.Field() + """ + Current invite status: `pending`, `accepted`, `revoked`, or `expired`. + """ + + expires_at: dt.datetime = pydantic.Field() + """ + When the invite expires (UTC). Invites have a 14-day TTL from creation. + """ + + invited_by: str = pydantic.Field() + """ + User ID of the admin who created the invite. + """ + + created_at: dt.datetime = pydantic.Field() + """ + When the invite was created (UTC). + """ 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/workspace_member_out.py b/src/onepin/types/workspace_member_out.py index 613afc2..c6d0518 100644 --- a/src/onepin/types/workspace_member_out.py +++ b/src/onepin/types/workspace_member_out.py @@ -8,15 +8,50 @@ class WorkspaceMemberOut(UniversalBaseModel): - id: str - user_id: typing.Optional[str] = None - email: str - first_name: typing.Optional[str] = None - last_name: typing.Optional[str] = None - image_url: typing.Optional[str] = None - role: str - last_active_at: typing.Optional[dt.datetime] = None - status: str + id: str = pydantic.Field() + """ + Member or invite record ID. + """ + + user_id: typing.Optional[str] = pydantic.Field(default=None) + """ + User ID of the member. Null for pending invites (invitee has not yet accepted). + """ + + email: str = pydantic.Field() + """ + Email address. For active members this is their primary account email; for invites it is the address the invite was sent to. + """ + + first_name: typing.Optional[str] = pydantic.Field(default=None) + """ + First name. Null for pending invites. + """ + + last_name: typing.Optional[str] = pydantic.Field(default=None) + """ + Last name. Null for pending invites. + """ + + image_url: typing.Optional[str] = pydantic.Field(default=None) + """ + Profile image URL. Null for pending invites. + """ + + role: str = pydantic.Field() + """ + Workspace role: `admin`, `editor`, or `viewer`. + """ + + last_active_at: typing.Optional[dt.datetime] = pydantic.Field(default=None) + """ + When the member last accessed the workspace (UTC). Null for pending invites. + """ + + status: str = pydantic.Field() + """ + Membership status: `active` for confirmed members, `invited` for pending invites. + """ 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/workspace_member_role_update.py b/src/onepin/types/workspace_member_role_update.py index df5c26e..6baa10c 100644 --- a/src/onepin/types/workspace_member_role_update.py +++ b/src/onepin/types/workspace_member_role_update.py @@ -8,7 +8,10 @@ class WorkspaceMemberRoleUpdate(UniversalBaseModel): - role: WorkspaceRole + role: WorkspaceRole = pydantic.Field() + """ + New role to assign: `admin`, `editor`, or `viewer`. + """ 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/workspace_out.py b/src/onepin/types/workspace_out.py index d192f69..6838dfb 100644 --- a/src/onepin/types/workspace_out.py +++ b/src/onepin/types/workspace_out.py @@ -8,13 +8,50 @@ class WorkspaceOut(UniversalBaseModel): - id: str - name: str - slug: str - color_idx: int - created_by: str - created_at: dt.datetime - updated_at: dt.datetime + id: str = pydantic.Field() + """ + Unique workspace identifier. Pass as `X-Workspace-Id` header to scope resource requests. + """ + + name: str = pydantic.Field() + """ + Human-readable workspace name. + """ + + slug: str = pydantic.Field() + """ + URL-safe workspace identifier (lowercase kebab-case). + """ + + color_idx: int = pydantic.Field() + """ + Index into the workspace color palette. + """ + + created_by: str = pydantic.Field() + """ + User ID of the workspace owner. + """ + + created_at: dt.datetime = pydantic.Field() + """ + When the workspace was created (UTC). + """ + + updated_at: dt.datetime = pydantic.Field() + """ + When the workspace was last modified (UTC). + """ + + routing_price_sensitivity: float = pydantic.Field() + """ + Voice-selection price/quality balance (0.0 = pure quality, 1.0 = pure price, 0.5 = balanced). + """ + + routing_llm_fit: bool = pydantic.Field() + """ + Whether automatic voice selection also weighs content fit. + """ 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/workspace_runs_stats_out.py b/src/onepin/types/workspace_runs_stats_out.py deleted file mode 100644 index b6339c9..0000000 --- a/src/onepin/types/workspace_runs_stats_out.py +++ /dev/null @@ -1,25 +0,0 @@ -# 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 WorkspaceRunsStatsOut(UniversalBaseModel): - total: int - pending: int - running: int - completed: int - failed: int - cancelled: int - paused: int - - 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/workspace_settings_out.py b/src/onepin/types/workspace_settings_out.py index cffb1ba..ca48744 100644 --- a/src/onepin/types/workspace_settings_out.py +++ b/src/onepin/types/workspace_settings_out.py @@ -7,8 +7,15 @@ class WorkspaceSettingsOut(UniversalBaseModel): - default_language: typing.Optional[str] = None - theme: typing.Optional[str] = None + default_language: typing.Optional[str] = pydantic.Field(default=None) + """ + Default locale code for new workflow nodes (e.g. `en-US`). Null if not yet configured. + """ + + theme: typing.Optional[str] = pydantic.Field(default=None) + """ + Workspace display theme identifier. Null if not yet configured. + """ 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/workspace_workflows_stats_out.py b/src/onepin/types/workspace_workflows_stats_out.py deleted file mode 100644 index 7107801..0000000 --- a/src/onepin/types/workspace_workflows_stats_out.py +++ /dev/null @@ -1,36 +0,0 @@ -# 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 WorkspaceWorkflowsStatsOut(UniversalBaseModel): - """ - Workflow counts grouped by derived ``WorkflowListStatus`` for a workspace. - - **Bucket semantics (POD-634):** ``completed`` means FINISHED — its latest - run ended (completed, failed, or cancelled). ``failed`` counts failed-only - and is therefore a SUBSET of ``completed`` (a failed run is counted in - both). The buckets OVERLAP, so their sum can exceed ``total``; the UI must - rely on ``total`` for the headline count, not the sum of buckets. - - ``paused`` is always 0 — paused workflows fold into ``running`` (POD-417: a parked run is still active work). - """ - - total: int - draft: int - running: int - completed: int - failed: int - paused: int - - 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 07385b0..52336ca 100644 --- a/src/onepin/uploads/client.py +++ b/src/onepin/uploads/client.py @@ -39,13 +39,35 @@ def create( request_options: typing.Optional[RequestOptions] = None, ) -> ApiResponseUploadCreateResponse: """ - Request a presigned URL for uploading a file. + Request a presigned URL to upload a file to object storage (step 1 of 2). + + The two-step upload flow: + 1. `POST /uploads` — register the file and receive a short-lived `upload_url`. + PUT your file bytes directly to that URL (do not send them to this API). + 2. `POST /uploads/{id}` — confirm the upload completed and bind the file to a + resource (e.g. a workflow). The file is moved to its final location and the + upload record transitions from `pending` to `uploaded`. + + `category` controls which file formats are accepted: + - `script` — text-based formats (txt, srt, csv, json, xliff, docx) + - `dictionary` — audio formats (mp3, wav, m4a, ogg, webm) + + The presigned URL expires within a short window (see `upload_url` TTL in the + response). If the URL expires before the PUT completes, discard this upload + record and start over with a fresh `POST /uploads` call. + + `X-Workspace-Id` is optional but recommended for workspace-scoped storage + quota tracking. API keys with a bound workspace attach automatically. + + Dual-auth: Bearer JWT or API key (scope `uploads:write`). Parameters ---------- filename : str + Original filename including extension (e.g. `script.txt`). Must include a file extension. category : UploadRequestCategory + File category. Determines which formats are accepted: `script` for text formats (txt, srt, csv, json, xliff, docx); `dictionary` for audio formats (mp3, wav, m4a, ogg, webm). workspace_id : typing.Optional[str] @@ -84,20 +106,37 @@ def confirm( request_options: typing.Optional[RequestOptions] = None, ) -> ApiResponseUploadOut: """ - Confirm upload and move file to final location. + Confirm a completed upload and bind it to a resource (step 2 of 2). + + Call this after successfully PUTting your file to the presigned URL returned + by `POST /uploads`. Provide `context_type` and `context_id` to associate the + file with an existing resource (currently `workflow` is the supported context + type). The file is moved to its final location and `status` transitions from + `pending` to `uploaded`. - POD-301: gates the file size against `storage_bytes_per_workspace` whenever - the upload binds to a workspace-scoped resource (header OR derived from - context_id). Records the storage_charge event in the same transaction as the - upload row update. + This endpoint is idempotent: if the upload was already confirmed, the current + state is returned without re-processing. + + Storage quota is checked against the workspace at confirm time. If confirming + would exceed the workspace storage limit, a 402 is returned and the file + remains in its staging location (the upload record stays `pending` so you can + delete the staging file and try a smaller file). + + Binding to a workspace-scoped resource requires the caller to be a member of + that workspace. Workspace is inferred from the resource when `X-Workspace-Id` + is omitted. + + Dual-auth: Bearer JWT or API key (scope `uploads:write`). Parameters ---------- upload_id : str context_type : UploadConfirmRequestContextType + Type of resource this upload is being attached to. Currently only `workflow` is supported. 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. workspace_id : typing.Optional[str] @@ -139,17 +178,21 @@ def delete( request_options: typing.Optional[RequestOptions] = None, ) -> ApiResponseDict: """ - Delete an upload and its S3 object. + Delete an upload and its associated file. + + Permanently removes the upload record and schedules the stored file for + deletion. The record is removed first; the file is cleaned up asynchronously + after the response so storage removal only happens after a successful commit. + + If the upload was previously confirmed against a workspace-scoped resource, + the consumed storage bytes are released back to the workspace quota, keeping + the workspace storage counter accurate. - DB record is deleted first (committed on response). S3 cleanup runs - after the response via a background task so the file is only removed - once the DB commit succeeds. + Callers can delete uploads in any state (`pending` or `uploaded`). Deleting + a `pending` upload (e.g. after an expired presigned URL) is the correct way + to clean up an abandoned upload attempt. - POD-301: if the upload was confirmed against a workspace-scoped resource, - release the bytes back to that workspace's storage counter. Without this, - storage_bytes_used drifts upward forever and customers stay capped after - deleting files. Read upload state BEFORE delete_for_user — the row is gone - after that call. + Dual-auth: Bearer JWT or API key (scope `uploads:write`). Parameters ---------- @@ -204,13 +247,35 @@ async def create( request_options: typing.Optional[RequestOptions] = None, ) -> ApiResponseUploadCreateResponse: """ - Request a presigned URL for uploading a file. + Request a presigned URL to upload a file to object storage (step 1 of 2). + + The two-step upload flow: + 1. `POST /uploads` — register the file and receive a short-lived `upload_url`. + PUT your file bytes directly to that URL (do not send them to this API). + 2. `POST /uploads/{id}` — confirm the upload completed and bind the file to a + resource (e.g. a workflow). The file is moved to its final location and the + upload record transitions from `pending` to `uploaded`. + + `category` controls which file formats are accepted: + - `script` — text-based formats (txt, srt, csv, json, xliff, docx) + - `dictionary` — audio formats (mp3, wav, m4a, ogg, webm) + + The presigned URL expires within a short window (see `upload_url` TTL in the + response). If the URL expires before the PUT completes, discard this upload + record and start over with a fresh `POST /uploads` call. + + `X-Workspace-Id` is optional but recommended for workspace-scoped storage + quota tracking. API keys with a bound workspace attach automatically. + + Dual-auth: Bearer JWT or API key (scope `uploads:write`). Parameters ---------- filename : str + Original filename including extension (e.g. `script.txt`). Must include a file extension. category : UploadRequestCategory + File category. Determines which formats are accepted: `script` for text formats (txt, srt, csv, json, xliff, docx); `dictionary` for audio formats (mp3, wav, m4a, ogg, webm). workspace_id : typing.Optional[str] @@ -257,20 +322,37 @@ async def confirm( request_options: typing.Optional[RequestOptions] = None, ) -> ApiResponseUploadOut: """ - Confirm upload and move file to final location. + Confirm a completed upload and bind it to a resource (step 2 of 2). + + Call this after successfully PUTting your file to the presigned URL returned + by `POST /uploads`. Provide `context_type` and `context_id` to associate the + file with an existing resource (currently `workflow` is the supported context + type). The file is moved to its final location and `status` transitions from + `pending` to `uploaded`. - POD-301: gates the file size against `storage_bytes_per_workspace` whenever - the upload binds to a workspace-scoped resource (header OR derived from - context_id). Records the storage_charge event in the same transaction as the - upload row update. + This endpoint is idempotent: if the upload was already confirmed, the current + state is returned without re-processing. + + Storage quota is checked against the workspace at confirm time. If confirming + would exceed the workspace storage limit, a 402 is returned and the file + remains in its staging location (the upload record stays `pending` so you can + delete the staging file and try a smaller file). + + Binding to a workspace-scoped resource requires the caller to be a member of + that workspace. Workspace is inferred from the resource when `X-Workspace-Id` + is omitted. + + Dual-auth: Bearer JWT or API key (scope `uploads:write`). Parameters ---------- upload_id : str context_type : UploadConfirmRequestContextType + Type of resource this upload is being attached to. Currently only `workflow` is supported. 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. workspace_id : typing.Optional[str] @@ -320,17 +402,21 @@ async def delete( request_options: typing.Optional[RequestOptions] = None, ) -> ApiResponseDict: """ - Delete an upload and its S3 object. + Delete an upload and its associated file. + + Permanently removes the upload record and schedules the stored file for + deletion. The record is removed first; the file is cleaned up asynchronously + after the response so storage removal only happens after a successful commit. + + If the upload was previously confirmed against a workspace-scoped resource, + the consumed storage bytes are released back to the workspace quota, keeping + the workspace storage counter accurate. - DB record is deleted first (committed on response). S3 cleanup runs - after the response via a background task so the file is only removed - once the DB commit succeeds. + Callers can delete uploads in any state (`pending` or `uploaded`). Deleting + a `pending` upload (e.g. after an expired presigned URL) is the correct way + to clean up an abandoned upload attempt. - POD-301: if the upload was confirmed against a workspace-scoped resource, - release the bytes back to that workspace's storage counter. Without this, - storage_bytes_used drifts upward forever and customers stay capped after - deleting files. Read upload state BEFORE delete_for_user — the row is gone - after that call. + Dual-auth: Bearer JWT or API key (scope `uploads:write`). Parameters ---------- diff --git a/src/onepin/uploads/raw_client.py b/src/onepin/uploads/raw_client.py index ab5c715..c91f170 100644 --- a/src/onepin/uploads/raw_client.py +++ b/src/onepin/uploads/raw_client.py @@ -35,13 +35,35 @@ def create( request_options: typing.Optional[RequestOptions] = None, ) -> HttpResponse[ApiResponseUploadCreateResponse]: """ - Request a presigned URL for uploading a file. + Request a presigned URL to upload a file to object storage (step 1 of 2). + + The two-step upload flow: + 1. `POST /uploads` — register the file and receive a short-lived `upload_url`. + PUT your file bytes directly to that URL (do not send them to this API). + 2. `POST /uploads/{id}` — confirm the upload completed and bind the file to a + resource (e.g. a workflow). The file is moved to its final location and the + upload record transitions from `pending` to `uploaded`. + + `category` controls which file formats are accepted: + - `script` — text-based formats (txt, srt, csv, json, xliff, docx) + - `dictionary` — audio formats (mp3, wav, m4a, ogg, webm) + + The presigned URL expires within a short window (see `upload_url` TTL in the + response). If the URL expires before the PUT completes, discard this upload + record and start over with a fresh `POST /uploads` call. + + `X-Workspace-Id` is optional but recommended for workspace-scoped storage + quota tracking. API keys with a bound workspace attach automatically. + + Dual-auth: Bearer JWT or API key (scope `uploads:write`). Parameters ---------- filename : str + Original filename including extension (e.g. `script.txt`). Must include a file extension. category : UploadRequestCategory + File category. Determines which formats are accepted: `script` for text formats (txt, srt, csv, json, xliff, docx); `dictionary` for audio formats (mp3, wav, m4a, ogg, webm). workspace_id : typing.Optional[str] @@ -107,20 +129,37 @@ def confirm( request_options: typing.Optional[RequestOptions] = None, ) -> HttpResponse[ApiResponseUploadOut]: """ - Confirm upload and move file to final location. + Confirm a completed upload and bind it to a resource (step 2 of 2). + + Call this after successfully PUTting your file to the presigned URL returned + by `POST /uploads`. Provide `context_type` and `context_id` to associate the + file with an existing resource (currently `workflow` is the supported context + type). The file is moved to its final location and `status` transitions from + `pending` to `uploaded`. - POD-301: gates the file size against `storage_bytes_per_workspace` whenever - the upload binds to a workspace-scoped resource (header OR derived from - context_id). Records the storage_charge event in the same transaction as the - upload row update. + This endpoint is idempotent: if the upload was already confirmed, the current + state is returned without re-processing. + + Storage quota is checked against the workspace at confirm time. If confirming + would exceed the workspace storage limit, a 402 is returned and the file + remains in its staging location (the upload record stays `pending` so you can + delete the staging file and try a smaller file). + + Binding to a workspace-scoped resource requires the caller to be a member of + that workspace. Workspace is inferred from the resource when `X-Workspace-Id` + is omitted. + + Dual-auth: Bearer JWT or API key (scope `uploads:write`). Parameters ---------- upload_id : str context_type : UploadConfirmRequestContextType + Type of resource this upload is being attached to. Currently only `workflow` is supported. 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. workspace_id : typing.Optional[str] @@ -184,17 +223,21 @@ def delete( request_options: typing.Optional[RequestOptions] = None, ) -> HttpResponse[ApiResponseDict]: """ - Delete an upload and its S3 object. + Delete an upload and its associated file. + + Permanently removes the upload record and schedules the stored file for + deletion. The record is removed first; the file is cleaned up asynchronously + after the response so storage removal only happens after a successful commit. + + If the upload was previously confirmed against a workspace-scoped resource, + the consumed storage bytes are released back to the workspace quota, keeping + the workspace storage counter accurate. - DB record is deleted first (committed on response). S3 cleanup runs - after the response via a background task so the file is only removed - once the DB commit succeeds. + Callers can delete uploads in any state (`pending` or `uploaded`). Deleting + a `pending` upload (e.g. after an expired presigned URL) is the correct way + to clean up an abandoned upload attempt. - POD-301: if the upload was confirmed against a workspace-scoped resource, - release the bytes back to that workspace's storage counter. Without this, - storage_bytes_used drifts upward forever and customers stay capped after - deleting files. Read upload state BEFORE delete_for_user — the row is gone - after that call. + Dual-auth: Bearer JWT or API key (scope `uploads:write`). Parameters ---------- @@ -262,13 +305,35 @@ async def create( request_options: typing.Optional[RequestOptions] = None, ) -> AsyncHttpResponse[ApiResponseUploadCreateResponse]: """ - Request a presigned URL for uploading a file. + Request a presigned URL to upload a file to object storage (step 1 of 2). + + The two-step upload flow: + 1. `POST /uploads` — register the file and receive a short-lived `upload_url`. + PUT your file bytes directly to that URL (do not send them to this API). + 2. `POST /uploads/{id}` — confirm the upload completed and bind the file to a + resource (e.g. a workflow). The file is moved to its final location and the + upload record transitions from `pending` to `uploaded`. + + `category` controls which file formats are accepted: + - `script` — text-based formats (txt, srt, csv, json, xliff, docx) + - `dictionary` — audio formats (mp3, wav, m4a, ogg, webm) + + The presigned URL expires within a short window (see `upload_url` TTL in the + response). If the URL expires before the PUT completes, discard this upload + record and start over with a fresh `POST /uploads` call. + + `X-Workspace-Id` is optional but recommended for workspace-scoped storage + quota tracking. API keys with a bound workspace attach automatically. + + Dual-auth: Bearer JWT or API key (scope `uploads:write`). Parameters ---------- filename : str + Original filename including extension (e.g. `script.txt`). Must include a file extension. category : UploadRequestCategory + File category. Determines which formats are accepted: `script` for text formats (txt, srt, csv, json, xliff, docx); `dictionary` for audio formats (mp3, wav, m4a, ogg, webm). workspace_id : typing.Optional[str] @@ -334,20 +399,37 @@ async def confirm( request_options: typing.Optional[RequestOptions] = None, ) -> AsyncHttpResponse[ApiResponseUploadOut]: """ - Confirm upload and move file to final location. + Confirm a completed upload and bind it to a resource (step 2 of 2). + + Call this after successfully PUTting your file to the presigned URL returned + by `POST /uploads`. Provide `context_type` and `context_id` to associate the + file with an existing resource (currently `workflow` is the supported context + type). The file is moved to its final location and `status` transitions from + `pending` to `uploaded`. - POD-301: gates the file size against `storage_bytes_per_workspace` whenever - the upload binds to a workspace-scoped resource (header OR derived from - context_id). Records the storage_charge event in the same transaction as the - upload row update. + This endpoint is idempotent: if the upload was already confirmed, the current + state is returned without re-processing. + + Storage quota is checked against the workspace at confirm time. If confirming + would exceed the workspace storage limit, a 402 is returned and the file + remains in its staging location (the upload record stays `pending` so you can + delete the staging file and try a smaller file). + + Binding to a workspace-scoped resource requires the caller to be a member of + that workspace. Workspace is inferred from the resource when `X-Workspace-Id` + is omitted. + + Dual-auth: Bearer JWT or API key (scope `uploads:write`). Parameters ---------- upload_id : str context_type : UploadConfirmRequestContextType + Type of resource this upload is being attached to. Currently only `workflow` is supported. 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. workspace_id : typing.Optional[str] @@ -411,17 +493,21 @@ async def delete( request_options: typing.Optional[RequestOptions] = None, ) -> AsyncHttpResponse[ApiResponseDict]: """ - Delete an upload and its S3 object. + Delete an upload and its associated file. + + Permanently removes the upload record and schedules the stored file for + deletion. The record is removed first; the file is cleaned up asynchronously + after the response so storage removal only happens after a successful commit. + + If the upload was previously confirmed against a workspace-scoped resource, + the consumed storage bytes are released back to the workspace quota, keeping + the workspace storage counter accurate. - DB record is deleted first (committed on response). S3 cleanup runs - after the response via a background task so the file is only removed - once the DB commit succeeds. + Callers can delete uploads in any state (`pending` or `uploaded`). Deleting + a `pending` upload (e.g. after an expired presigned URL) is the correct way + to clean up an abandoned upload attempt. - POD-301: if the upload was confirmed against a workspace-scoped resource, - release the bytes back to that workspace's storage counter. Without this, - storage_bytes_used drifts upward forever and customers stay capped after - deleting files. Read upload state BEFORE delete_for_user — the row is gone - after that call. + Dual-auth: Bearer JWT or API key (scope `uploads:write`). Parameters ---------- diff --git a/src/onepin/usage/client.py b/src/onepin/usage/client.py index 49aea9d..7d217fd 100644 --- a/src/onepin/usage/client.py +++ b/src/onepin/usage/client.py @@ -47,7 +47,24 @@ def usage_summary( request_options: typing.Optional[RequestOptions] = None, ) -> ApiResponseUsageSummaryOut: """ - Return workspace usage totals plus tab-specific aggregate activity buckets. + Return aggregated usage totals and activity chart data for the workspace. + + Combines credit consumption, character and line counts, and workflow run + statistics for the requested rolling window (`range`) with a chart-ready + activity series (`activity`) bucketed by `activity_view`. + + The `credits.used` field reflects the authenticated user's own billing-period + consumption; all other aggregate fields (characters, lines, runs, daily + buckets, activity buckets) are workspace-scoped across all members. + + Date boundaries are computed in the supplied `timezone` (IANA, e.g. + `America/New_York`) so "today" and "this week" align with the caller's local + calendar. Defaults to UTC. + + Use `GET /usage/by-language` for a language-level breakdown, or + `GET /usage/activity` for the event-by-event feed. + + Dual-auth: Bearer JWT or API key (scope `workspace:read`). Parameters ---------- @@ -98,11 +115,21 @@ def usage_by_language( request_options: typing.Optional[RequestOptions] = None, ) -> ApiResponseUsageByLanguageOut: """ - Return workspace generated-audio usage grouped by language. + Return workspace audio generation usage broken down by language. + + Each row represents one locale with its share of total credit and character + consumption. `share` is a 0..1 fraction of workspace-wide usage for the + period; multiply by 100 for a percentage. + + Period selection: supply `activity_view` to align the language rows with + the same period shown on the Usage dashboard chart (daily = last 7 local + days, weekly = last 12 Monday-start weeks, monthly = last 12 months). When + `activity_view` is provided, `range` is ignored and `range` in the response + is `null`. Omit `activity_view` to use the rolling `range` window instead. - ``share`` is a 0..1 fraction. When ``activity_view`` is supplied, rows use - that tab's local-calendar period; otherwise they preserve legacy ``range`` - behavior. + Date boundaries are computed in the supplied `timezone` (IANA). Defaults to UTC. + + Dual-auth: Bearer JWT or API key (scope `workspace:read`). Parameters ---------- @@ -156,7 +183,25 @@ def usage_activity( request_options: typing.Optional[RequestOptions] = None, ) -> ApiListResponseUsageActivityOut: """ - Return the workspace usage activity feed with stable action filters and cursor pagination. + Return the workspace activity feed as a cursor-paginated event list. + + Each item represents a discrete workspace event (workflow run, voice generated, + template applied, member invited, API key created, settings changed). Events are + ordered newest-first within the requested rolling window. + + Filtering: + - `type` narrows to a single action kind (e.g. `workflow_run`). + - `user_id` restricts to events triggered by a specific workspace member; + returns 404 if the user is not a member of this workspace. + - Both filters can be combined. + + Pagination: pass the `cursor` value from a previous response to retrieve the + next page. An absent or null `cursor` in the response means no further pages + exist. Page size is controlled by `limit` (1–100, default 20). + + Date boundaries are computed in the supplied `timezone` (IANA). Defaults to UTC. + + Dual-auth: Bearer JWT or API key (scope `workspace:read`). Parameters ---------- @@ -234,7 +279,24 @@ async def usage_summary( request_options: typing.Optional[RequestOptions] = None, ) -> ApiResponseUsageSummaryOut: """ - Return workspace usage totals plus tab-specific aggregate activity buckets. + Return aggregated usage totals and activity chart data for the workspace. + + Combines credit consumption, character and line counts, and workflow run + statistics for the requested rolling window (`range`) with a chart-ready + activity series (`activity`) bucketed by `activity_view`. + + The `credits.used` field reflects the authenticated user's own billing-period + consumption; all other aggregate fields (characters, lines, runs, daily + buckets, activity buckets) are workspace-scoped across all members. + + Date boundaries are computed in the supplied `timezone` (IANA, e.g. + `America/New_York`) so "today" and "this week" align with the caller's local + calendar. Defaults to UTC. + + Use `GET /usage/by-language` for a language-level breakdown, or + `GET /usage/activity` for the event-by-event feed. + + Dual-auth: Bearer JWT or API key (scope `workspace:read`). Parameters ---------- @@ -293,11 +355,21 @@ async def usage_by_language( request_options: typing.Optional[RequestOptions] = None, ) -> ApiResponseUsageByLanguageOut: """ - Return workspace generated-audio usage grouped by language. + Return workspace audio generation usage broken down by language. + + Each row represents one locale with its share of total credit and character + consumption. `share` is a 0..1 fraction of workspace-wide usage for the + period; multiply by 100 for a percentage. + + Period selection: supply `activity_view` to align the language rows with + the same period shown on the Usage dashboard chart (daily = last 7 local + days, weekly = last 12 Monday-start weeks, monthly = last 12 months). When + `activity_view` is provided, `range` is ignored and `range` in the response + is `null`. Omit `activity_view` to use the rolling `range` window instead. - ``share`` is a 0..1 fraction. When ``activity_view`` is supplied, rows use - that tab's local-calendar period; otherwise they preserve legacy ``range`` - behavior. + Date boundaries are computed in the supplied `timezone` (IANA). Defaults to UTC. + + Dual-auth: Bearer JWT or API key (scope `workspace:read`). Parameters ---------- @@ -359,7 +431,25 @@ async def usage_activity( request_options: typing.Optional[RequestOptions] = None, ) -> ApiListResponseUsageActivityOut: """ - Return the workspace usage activity feed with stable action filters and cursor pagination. + Return the workspace activity feed as a cursor-paginated event list. + + Each item represents a discrete workspace event (workflow run, voice generated, + template applied, member invited, API key created, settings changed). Events are + ordered newest-first within the requested rolling window. + + Filtering: + - `type` narrows to a single action kind (e.g. `workflow_run`). + - `user_id` restricts to events triggered by a specific workspace member; + returns 404 if the user is not a member of this workspace. + - Both filters can be combined. + + Pagination: pass the `cursor` value from a previous response to retrieve the + next page. An absent or null `cursor` in the response means no further pages + exist. Page size is controlled by `limit` (1–100, default 20). + + Date boundaries are computed in the supplied `timezone` (IANA). Defaults to UTC. + + Dual-auth: Bearer JWT or API key (scope `workspace:read`). Parameters ---------- diff --git a/src/onepin/usage/raw_client.py b/src/onepin/usage/raw_client.py index c8058d4..009698b 100644 --- a/src/onepin/usage/raw_client.py +++ b/src/onepin/usage/raw_client.py @@ -42,7 +42,24 @@ def usage_summary( request_options: typing.Optional[RequestOptions] = None, ) -> HttpResponse[ApiResponseUsageSummaryOut]: """ - Return workspace usage totals plus tab-specific aggregate activity buckets. + Return aggregated usage totals and activity chart data for the workspace. + + Combines credit consumption, character and line counts, and workflow run + statistics for the requested rolling window (`range`) with a chart-ready + activity series (`activity`) bucketed by `activity_view`. + + The `credits.used` field reflects the authenticated user's own billing-period + consumption; all other aggregate fields (characters, lines, runs, daily + buckets, activity buckets) are workspace-scoped across all members. + + Date boundaries are computed in the supplied `timezone` (IANA, e.g. + `America/New_York`) so "today" and "this week" align with the caller's local + calendar. Defaults to UTC. + + Use `GET /usage/by-language` for a language-level breakdown, or + `GET /usage/activity` for the event-by-event feed. + + Dual-auth: Bearer JWT or API key (scope `workspace:read`). Parameters ---------- @@ -118,11 +135,21 @@ def usage_by_language( request_options: typing.Optional[RequestOptions] = None, ) -> HttpResponse[ApiResponseUsageByLanguageOut]: """ - Return workspace generated-audio usage grouped by language. + Return workspace audio generation usage broken down by language. + + Each row represents one locale with its share of total credit and character + consumption. `share` is a 0..1 fraction of workspace-wide usage for the + period; multiply by 100 for a percentage. + + Period selection: supply `activity_view` to align the language rows with + the same period shown on the Usage dashboard chart (daily = last 7 local + days, weekly = last 12 Monday-start weeks, monthly = last 12 months). When + `activity_view` is provided, `range` is ignored and `range` in the response + is `null`. Omit `activity_view` to use the rolling `range` window instead. - ``share`` is a 0..1 fraction. When ``activity_view`` is supplied, rows use - that tab's local-calendar period; otherwise they preserve legacy ``range`` - behavior. + Date boundaries are computed in the supplied `timezone` (IANA). Defaults to UTC. + + Dual-auth: Bearer JWT or API key (scope `workspace:read`). Parameters ---------- @@ -201,7 +228,25 @@ def usage_activity( request_options: typing.Optional[RequestOptions] = None, ) -> HttpResponse[ApiListResponseUsageActivityOut]: """ - Return the workspace usage activity feed with stable action filters and cursor pagination. + Return the workspace activity feed as a cursor-paginated event list. + + Each item represents a discrete workspace event (workflow run, voice generated, + template applied, member invited, API key created, settings changed). Events are + ordered newest-first within the requested rolling window. + + Filtering: + - `type` narrows to a single action kind (e.g. `workflow_run`). + - `user_id` restricts to events triggered by a specific workspace member; + returns 404 if the user is not a member of this workspace. + - Both filters can be combined. + + Pagination: pass the `cursor` value from a previous response to retrieve the + next page. An absent or null `cursor` in the response means no further pages + exist. Page size is controlled by `limit` (1–100, default 20). + + Date boundaries are computed in the supplied `timezone` (IANA). Defaults to UTC. + + Dual-auth: Bearer JWT or API key (scope `workspace:read`). Parameters ---------- @@ -293,7 +338,24 @@ async def usage_summary( request_options: typing.Optional[RequestOptions] = None, ) -> AsyncHttpResponse[ApiResponseUsageSummaryOut]: """ - Return workspace usage totals plus tab-specific aggregate activity buckets. + Return aggregated usage totals and activity chart data for the workspace. + + Combines credit consumption, character and line counts, and workflow run + statistics for the requested rolling window (`range`) with a chart-ready + activity series (`activity`) bucketed by `activity_view`. + + The `credits.used` field reflects the authenticated user's own billing-period + consumption; all other aggregate fields (characters, lines, runs, daily + buckets, activity buckets) are workspace-scoped across all members. + + Date boundaries are computed in the supplied `timezone` (IANA, e.g. + `America/New_York`) so "today" and "this week" align with the caller's local + calendar. Defaults to UTC. + + Use `GET /usage/by-language` for a language-level breakdown, or + `GET /usage/activity` for the event-by-event feed. + + Dual-auth: Bearer JWT or API key (scope `workspace:read`). Parameters ---------- @@ -369,11 +431,21 @@ async def usage_by_language( request_options: typing.Optional[RequestOptions] = None, ) -> AsyncHttpResponse[ApiResponseUsageByLanguageOut]: """ - Return workspace generated-audio usage grouped by language. + Return workspace audio generation usage broken down by language. + + Each row represents one locale with its share of total credit and character + consumption. `share` is a 0..1 fraction of workspace-wide usage for the + period; multiply by 100 for a percentage. + + Period selection: supply `activity_view` to align the language rows with + the same period shown on the Usage dashboard chart (daily = last 7 local + days, weekly = last 12 Monday-start weeks, monthly = last 12 months). When + `activity_view` is provided, `range` is ignored and `range` in the response + is `null`. Omit `activity_view` to use the rolling `range` window instead. - ``share`` is a 0..1 fraction. When ``activity_view`` is supplied, rows use - that tab's local-calendar period; otherwise they preserve legacy ``range`` - behavior. + Date boundaries are computed in the supplied `timezone` (IANA). Defaults to UTC. + + Dual-auth: Bearer JWT or API key (scope `workspace:read`). Parameters ---------- @@ -452,7 +524,25 @@ async def usage_activity( request_options: typing.Optional[RequestOptions] = None, ) -> AsyncHttpResponse[ApiListResponseUsageActivityOut]: """ - Return the workspace usage activity feed with stable action filters and cursor pagination. + Return the workspace activity feed as a cursor-paginated event list. + + Each item represents a discrete workspace event (workflow run, voice generated, + template applied, member invited, API key created, settings changed). Events are + ordered newest-first within the requested rolling window. + + Filtering: + - `type` narrows to a single action kind (e.g. `workflow_run`). + - `user_id` restricts to events triggered by a specific workspace member; + returns 404 if the user is not a member of this workspace. + - Both filters can be combined. + + Pagination: pass the `cursor` value from a previous response to retrieve the + next page. An absent or null `cursor` in the response means no further pages + exist. Page size is controlled by `limit` (1–100, default 20). + + Date boundaries are computed in the supplied `timezone` (IANA). Defaults to UTC. + + Dual-auth: Bearer JWT or API key (scope `workspace:read`). Parameters ---------- diff --git a/src/onepin/users/client.py b/src/onepin/users/client.py index 7c7af6b..bf12397 100644 --- a/src/onepin/users/client.py +++ b/src/onepin/users/client.py @@ -6,16 +6,8 @@ from ..core.request_options import RequestOptions from ..types.api_list_response_template_out import ApiListResponseTemplateOut from ..types.api_response_balance_response import ApiResponseBalanceResponse -from ..types.api_response_customer_subscription_response import ApiResponseCustomerSubscriptionResponse -from ..types.api_response_dict import ApiResponseDict from ..types.api_response_email_notification_preferences_out import ApiResponseEmailNotificationPreferencesOut -from ..types.api_response_invoice_list_response import ApiResponseInvoiceListResponse -from ..types.api_response_list_payment_method_response import ApiResponseListPaymentMethodResponse from ..types.api_response_plan_limits import ApiResponsePlanLimits -from ..types.api_response_setup_intent_response import ApiResponseSetupIntentResponse -from ..types.api_response_union_customer_subscription_response_none_type import ( - ApiResponseUnionCustomerSubscriptionResponseNoneType, -) from ..types.template_category import TemplateCategory from .raw_client import AsyncRawUsersClient, RawUsersClient from .types.list_my_templates_api_v1users_me_templates_get_request_sort import ( @@ -41,280 +33,18 @@ def with_raw_response(self) -> RawUsersClient: """ return self._raw_client - def get_current_subscription( - self, *, request_options: typing.Optional[RequestOptions] = None - ) -> ApiResponseUnionCustomerSubscriptionResponseNoneType: - """ - Get the current user's active subscription. - - Parameters - ---------- - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ApiResponseUnionCustomerSubscriptionResponseNoneType - Successful Response - - Examples - -------- - from onepin import OnePinClient - - client = OnePinClient( - token="YOUR_TOKEN", - ) - client.users.get_current_subscription() - """ - _response = self._raw_client.get_current_subscription(request_options=request_options) - return _response.data - - def subscribe( - self, *, plan_id: str, request_options: typing.Optional[RequestOptions] = None - ) -> ApiResponseCustomerSubscriptionResponse: - """ - Create a subscription using the default payment method. - - Parameters - ---------- - plan_id : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ApiResponseCustomerSubscriptionResponse - Successful Response - - Examples - -------- - from onepin import OnePinClient - - client = OnePinClient( - token="YOUR_TOKEN", - ) - client.users.subscribe( - plan_id="plan_id", - ) - """ - _response = self._raw_client.subscribe(plan_id=plan_id, request_options=request_options) - return _response.data - - def cancel_subscription( - self, *, request_options: typing.Optional[RequestOptions] = None - ) -> ApiResponseCustomerSubscriptionResponse: - """ - Cancel the current user's subscription at period end. - - Parameters - ---------- - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ApiResponseCustomerSubscriptionResponse - Successful Response - - Examples - -------- - from onepin import OnePinClient - - client = OnePinClient( - token="YOUR_TOKEN", - ) - client.users.cancel_subscription() - """ - _response = self._raw_client.cancel_subscription(request_options=request_options) - return _response.data - - def change_plan( - self, *, new_plan_id: str, request_options: typing.Optional[RequestOptions] = None - ) -> ApiResponseCustomerSubscriptionResponse: - """ - Switch the current user's subscription to a different plan. - - Parameters - ---------- - new_plan_id : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ApiResponseCustomerSubscriptionResponse - Successful Response - - Examples - -------- - from onepin import OnePinClient - - client = OnePinClient( - token="YOUR_TOKEN", - ) - client.users.change_plan( - new_plan_id="new_plan_id", - ) - """ - _response = self._raw_client.change_plan(new_plan_id=new_plan_id, request_options=request_options) - return _response.data - - def cancel_scheduled_change( - self, *, request_options: typing.Optional[RequestOptions] = None - ) -> ApiResponseCustomerSubscriptionResponse: - """ - Cancel a scheduled plan downgrade. - - Parameters - ---------- - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ApiResponseCustomerSubscriptionResponse - Successful Response - - Examples - -------- - from onepin import OnePinClient - - client = OnePinClient( - token="YOUR_TOKEN", - ) - client.users.cancel_scheduled_change() - """ - _response = self._raw_client.cancel_scheduled_change(request_options=request_options) - return _response.data - - def list_payment_methods( - self, *, request_options: typing.Optional[RequestOptions] = None - ) -> ApiResponseListPaymentMethodResponse: - """ - List the current user's saved payment methods. - - Parameters - ---------- - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ApiResponseListPaymentMethodResponse - Successful Response - - Examples - -------- - from onepin import OnePinClient - - client = OnePinClient( - token="YOUR_TOKEN", - ) - client.users.list_payment_methods() - """ - _response = self._raw_client.list_payment_methods(request_options=request_options) - return _response.data - - def add_payment_method( - self, *, request_options: typing.Optional[RequestOptions] = None - ) -> ApiResponseSetupIntentResponse: - """ - Create a Stripe SetupIntent to add a new payment method. - - Parameters - ---------- - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ApiResponseSetupIntentResponse - Successful Response - - Examples - -------- - from onepin import OnePinClient - - client = OnePinClient( - token="YOUR_TOKEN", - ) - client.users.add_payment_method() - """ - _response = self._raw_client.add_payment_method(request_options=request_options) - return _response.data - - def delete_payment_method( - self, payment_method_id: str, *, request_options: typing.Optional[RequestOptions] = None - ) -> ApiResponseDict: - """ - Detach a payment method from the current user's account. - - Parameters - ---------- - payment_method_id : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ApiResponseDict - Successful Response - - Examples - -------- - from onepin import OnePinClient - - client = OnePinClient( - token="YOUR_TOKEN", - ) - client.users.delete_payment_method( - payment_method_id="payment_method_id", - ) - """ - _response = self._raw_client.delete_payment_method(payment_method_id, request_options=request_options) - return _response.data - - def set_default_payment_method( - self, payment_method_id: str, *, request_options: typing.Optional[RequestOptions] = None - ) -> ApiResponseDict: - """ - Set a payment method as the default for the current user. - - Parameters - ---------- - payment_method_id : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ApiResponseDict - Successful Response - - Examples - -------- - from onepin import OnePinClient - - client = OnePinClient( - token="YOUR_TOKEN", - ) - client.users.set_default_payment_method( - payment_method_id="payment_method_id", - ) - """ - _response = self._raw_client.set_default_payment_method(payment_method_id, request_options=request_options) - return _response.data - def get_my_credits(self, *, request_options: typing.Optional[RequestOptions] = None) -> ApiResponseBalanceResponse: """ - Return the current user's credit balance + monthly grant + period anchor. + Return the caller's current credit balance and billing period details. - Free-tier users have no Subscription row; the response falls back to the - canonical FREE Plan (1000 credits/mo, calendar-month boundary). + `balance` is the authoritative gate value: use it to decide whether to + attempt a workflow run. `remaining` is a display convenience derived from + settled ledger entries and may temporarily exceed `balance` while a workflow + run holds an open reserve. `used` reflects credits consumed in the current + billing period. `plan_grant` is the total monthly credit allowance for the + caller's plan, enabling a "X / Y used" display. `period_start` and + `period_end` mark the boundaries of the current billing window; free-tier + callers use a calendar-month boundary. Parameters ---------- @@ -340,7 +70,14 @@ def get_my_credits(self, *, request_options: typing.Optional[RequestOptions] = N def get_my_plan_limits(self, *, request_options: typing.Optional[RequestOptions] = None) -> ApiResponsePlanLimits: """ - Return the typed plan limits for the current user (FE plan-card UI consumer). + 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. Parameters ---------- @@ -364,49 +101,15 @@ def get_my_plan_limits(self, *, request_options: typing.Optional[RequestOptions] _response = self._raw_client.get_my_plan_limits(request_options=request_options) return _response.data - def list_invoices( - self, - *, - limit: typing.Optional[int] = None, - starting_after: typing.Optional[str] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> ApiResponseInvoiceListResponse: - """ - List invoices for the current user. - - Parameters - ---------- - limit : typing.Optional[int] - - starting_after : typing.Optional[str] - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ApiResponseInvoiceListResponse - Successful Response - - Examples - -------- - from onepin import OnePinClient - - client = OnePinClient( - token="YOUR_TOKEN", - ) - client.users.list_invoices() - """ - _response = self._raw_client.list_invoices( - limit=limit, starting_after=starting_after, request_options=request_options - ) - return _response.data - def get_current_notification_preferences( self, *, request_options: typing.Optional[RequestOptions] = None ) -> ApiResponseEmailNotificationPreferencesOut: """ - Get the current user's email notification preferences. + Return the caller's current email notification settings. + + Each boolean field corresponds to a notification category. `true` means the + caller will receive that email; `false` means they have opted out. Use + `PATCH /me/notification-preferences` to change individual preferences. Parameters ---------- @@ -438,13 +141,19 @@ def update_current_notification_preferences( request_options: typing.Optional[RequestOptions] = None, ) -> ApiResponseEmailNotificationPreferencesOut: """ - Partially update the current user's email notification preferences. + Partially update the caller's email notification preferences. + + Send only the fields you want to change; omitted fields are left unchanged. + All provided fields must be boolean — explicit `null` values are rejected + with a 422. Returns the full updated preference object. Parameters ---------- completed_generation_email : typing.Optional[bool] + Set to true to enable or false to disable completion emails. Omit to leave unchanged. failed_generation_email : typing.Optional[bool] + Set to true to enable or false to disable failure emails. Omit to leave unchanged. request_options : typing.Optional[RequestOptions] Request-specific configuration. @@ -483,12 +192,13 @@ def list_my_templates( request_options: typing.Optional[RequestOptions] = None, ) -> ApiListResponseTemplateOut: """ - List templates the current user created in the current workspace. + List workflow templates created by the caller in the current workspace. - Scoped on both `(workspace_id, created_by)` so when workspaces become - multi-user this endpoint keeps returning only the caller's own rows — - other workspace members' public/starter templates surface via the - gallery endpoint (`GET /api/v1/templates`) instead. + Returns only templates owned by the caller; templates shared by other + workspace members or platform starter templates are not included — use + `GET /api/v1/templates` for the full gallery. Supports offset-based + pagination via `offset` / `limit`. Combine `category`, `search`, and + `favorites_only` to narrow results; multiple `category` values are OR'd. Parameters ---------- @@ -496,12 +206,16 @@ def list_my_templates( Repeat for OR, e.g. ?category=media&category=creative search : typing.Optional[str] + Full-text search against template name and description. sort : typing.Optional[ListMyTemplatesApiV1UsersMeTemplatesGetRequestSort] + Sort order: `recent` (last updated), `name` (A–Z), or `uses` (most used). offset : typing.Optional[int] + Number of results to skip for pagination. limit : typing.Optional[int] + Maximum number of results to return (1–100). favorites_only : typing.Optional[bool] @@ -552,11 +266,20 @@ def with_raw_response(self) -> AsyncRawUsersClient: """ return self._raw_client - async def get_current_subscription( + async def get_my_credits( self, *, request_options: typing.Optional[RequestOptions] = None - ) -> ApiResponseUnionCustomerSubscriptionResponseNoneType: + ) -> ApiResponseBalanceResponse: """ - Get the current user's active subscription. + Return the caller's current credit balance and billing period details. + + `balance` is the authoritative gate value: use it to decide whether to + attempt a workflow run. `remaining` is a display convenience derived from + settled ledger entries and may temporarily exceed `balance` while a workflow + run holds an open reserve. `used` reflects credits consumed in the current + billing period. `plan_grant` is the total monthly credit allowance for the + caller's plan, enabling a "X / Y used" display. `period_start` and + `period_end` mark the boundaries of the current billing window; free-tier + callers use a calendar-month boundary. Parameters ---------- @@ -565,7 +288,7 @@ async def get_current_subscription( Returns ------- - ApiResponseUnionCustomerSubscriptionResponseNoneType + ApiResponseBalanceResponse Successful Response Examples @@ -580,30 +303,35 @@ async def get_current_subscription( async def main() -> None: - await client.users.get_current_subscription() + await client.users.get_my_credits() asyncio.run(main()) """ - _response = await self._raw_client.get_current_subscription(request_options=request_options) + _response = await self._raw_client.get_my_credits(request_options=request_options) return _response.data - async def subscribe( - self, *, plan_id: str, request_options: typing.Optional[RequestOptions] = None - ) -> ApiResponseCustomerSubscriptionResponse: + async def get_my_plan_limits( + self, *, request_options: typing.Optional[RequestOptions] = None + ) -> ApiResponsePlanLimits: """ - Create a subscription using the default payment method. + 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. Parameters ---------- - plan_id : str - request_options : typing.Optional[RequestOptions] Request-specific configuration. Returns ------- - ApiResponseCustomerSubscriptionResponse + ApiResponsePlanLimits Successful Response Examples @@ -618,21 +346,23 @@ async def subscribe( async def main() -> None: - await client.users.subscribe( - plan_id="plan_id", - ) + await client.users.get_my_plan_limits() asyncio.run(main()) """ - _response = await self._raw_client.subscribe(plan_id=plan_id, request_options=request_options) + _response = await self._raw_client.get_my_plan_limits(request_options=request_options) return _response.data - async def cancel_subscription( + async def get_current_notification_preferences( self, *, request_options: typing.Optional[RequestOptions] = None - ) -> ApiResponseCustomerSubscriptionResponse: + ) -> ApiResponseEmailNotificationPreferencesOut: """ - Cancel the current user's subscription at period end. + Return the caller's current email notification settings. + + Each boolean field corresponds to a notification category. `true` means the + caller will receive that email; `false` means they have opted out. Use + `PATCH /me/notification-preferences` to change individual preferences. Parameters ---------- @@ -641,7 +371,7 @@ async def cancel_subscription( Returns ------- - ApiResponseCustomerSubscriptionResponse + ApiResponseEmailNotificationPreferencesOut Successful Response Examples @@ -656,399 +386,12 @@ async def cancel_subscription( async def main() -> None: - await client.users.cancel_subscription() + await client.users.get_current_notification_preferences() asyncio.run(main()) """ - _response = await self._raw_client.cancel_subscription(request_options=request_options) - return _response.data - - async def change_plan( - self, *, new_plan_id: str, request_options: typing.Optional[RequestOptions] = None - ) -> ApiResponseCustomerSubscriptionResponse: - """ - Switch the current user's subscription to a different plan. - - Parameters - ---------- - new_plan_id : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ApiResponseCustomerSubscriptionResponse - Successful Response - - Examples - -------- - import asyncio - - from onepin import AsyncOnePinClient - - client = AsyncOnePinClient( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.users.change_plan( - new_plan_id="new_plan_id", - ) - - - asyncio.run(main()) - """ - _response = await self._raw_client.change_plan(new_plan_id=new_plan_id, request_options=request_options) - return _response.data - - async def cancel_scheduled_change( - self, *, request_options: typing.Optional[RequestOptions] = None - ) -> ApiResponseCustomerSubscriptionResponse: - """ - Cancel a scheduled plan downgrade. - - Parameters - ---------- - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ApiResponseCustomerSubscriptionResponse - Successful Response - - Examples - -------- - import asyncio - - from onepin import AsyncOnePinClient - - client = AsyncOnePinClient( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.users.cancel_scheduled_change() - - - asyncio.run(main()) - """ - _response = await self._raw_client.cancel_scheduled_change(request_options=request_options) - return _response.data - - async def list_payment_methods( - self, *, request_options: typing.Optional[RequestOptions] = None - ) -> ApiResponseListPaymentMethodResponse: - """ - List the current user's saved payment methods. - - Parameters - ---------- - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ApiResponseListPaymentMethodResponse - Successful Response - - Examples - -------- - import asyncio - - from onepin import AsyncOnePinClient - - client = AsyncOnePinClient( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.users.list_payment_methods() - - - asyncio.run(main()) - """ - _response = await self._raw_client.list_payment_methods(request_options=request_options) - return _response.data - - async def add_payment_method( - self, *, request_options: typing.Optional[RequestOptions] = None - ) -> ApiResponseSetupIntentResponse: - """ - Create a Stripe SetupIntent to add a new payment method. - - Parameters - ---------- - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ApiResponseSetupIntentResponse - Successful Response - - Examples - -------- - import asyncio - - from onepin import AsyncOnePinClient - - client = AsyncOnePinClient( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.users.add_payment_method() - - - asyncio.run(main()) - """ - _response = await self._raw_client.add_payment_method(request_options=request_options) - return _response.data - - async def delete_payment_method( - self, payment_method_id: str, *, request_options: typing.Optional[RequestOptions] = None - ) -> ApiResponseDict: - """ - Detach a payment method from the current user's account. - - Parameters - ---------- - payment_method_id : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ApiResponseDict - Successful Response - - Examples - -------- - import asyncio - - from onepin import AsyncOnePinClient - - client = AsyncOnePinClient( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.users.delete_payment_method( - payment_method_id="payment_method_id", - ) - - - asyncio.run(main()) - """ - _response = await self._raw_client.delete_payment_method(payment_method_id, request_options=request_options) - return _response.data - - async def set_default_payment_method( - self, payment_method_id: str, *, request_options: typing.Optional[RequestOptions] = None - ) -> ApiResponseDict: - """ - Set a payment method as the default for the current user. - - Parameters - ---------- - payment_method_id : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ApiResponseDict - Successful Response - - Examples - -------- - import asyncio - - from onepin import AsyncOnePinClient - - client = AsyncOnePinClient( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.users.set_default_payment_method( - payment_method_id="payment_method_id", - ) - - - asyncio.run(main()) - """ - _response = await self._raw_client.set_default_payment_method( - payment_method_id, request_options=request_options - ) - return _response.data - - async def get_my_credits( - self, *, request_options: typing.Optional[RequestOptions] = None - ) -> ApiResponseBalanceResponse: - """ - Return the current user's credit balance + monthly grant + period anchor. - - Free-tier users have no Subscription row; the response falls back to the - canonical FREE Plan (1000 credits/mo, calendar-month boundary). - - Parameters - ---------- - 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.users.get_my_credits() - - - asyncio.run(main()) - """ - _response = await self._raw_client.get_my_credits(request_options=request_options) - return _response.data - - async def get_my_plan_limits( - self, *, request_options: typing.Optional[RequestOptions] = None - ) -> ApiResponsePlanLimits: - """ - Return the typed plan limits for the current user (FE plan-card UI consumer). - - Parameters - ---------- - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ApiResponsePlanLimits - Successful Response - - Examples - -------- - import asyncio - - from onepin import AsyncOnePinClient - - client = AsyncOnePinClient( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.users.get_my_plan_limits() - - - asyncio.run(main()) - """ - _response = await self._raw_client.get_my_plan_limits(request_options=request_options) - return _response.data - - async def list_invoices( - self, - *, - limit: typing.Optional[int] = None, - starting_after: typing.Optional[str] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> ApiResponseInvoiceListResponse: - """ - List invoices for the current user. - - Parameters - ---------- - limit : typing.Optional[int] - - starting_after : typing.Optional[str] - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ApiResponseInvoiceListResponse - Successful Response - - Examples - -------- - import asyncio - - from onepin import AsyncOnePinClient - - client = AsyncOnePinClient( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.users.list_invoices() - - - asyncio.run(main()) - """ - _response = await self._raw_client.list_invoices( - limit=limit, starting_after=starting_after, request_options=request_options - ) - return _response.data - - async def get_current_notification_preferences( - self, *, request_options: typing.Optional[RequestOptions] = None - ) -> ApiResponseEmailNotificationPreferencesOut: - """ - Get the current user's email notification preferences. - - Parameters - ---------- - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ApiResponseEmailNotificationPreferencesOut - Successful Response - - Examples - -------- - import asyncio - - from onepin import AsyncOnePinClient - - client = AsyncOnePinClient( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.users.get_current_notification_preferences() - - - asyncio.run(main()) - """ - _response = await self._raw_client.get_current_notification_preferences(request_options=request_options) + _response = await self._raw_client.get_current_notification_preferences(request_options=request_options) return _response.data async def update_current_notification_preferences( @@ -1059,13 +402,19 @@ async def update_current_notification_preferences( request_options: typing.Optional[RequestOptions] = None, ) -> ApiResponseEmailNotificationPreferencesOut: """ - Partially update the current user's email notification preferences. + Partially update the caller's email notification preferences. + + Send only the fields you want to change; omitted fields are left unchanged. + All provided fields must be boolean — explicit `null` values are rejected + with a 422. Returns the full updated preference object. Parameters ---------- completed_generation_email : typing.Optional[bool] + Set to true to enable or false to disable completion emails. Omit to leave unchanged. failed_generation_email : typing.Optional[bool] + Set to true to enable or false to disable failure emails. Omit to leave unchanged. request_options : typing.Optional[RequestOptions] Request-specific configuration. @@ -1112,12 +461,13 @@ async def list_my_templates( request_options: typing.Optional[RequestOptions] = None, ) -> ApiListResponseTemplateOut: """ - List templates the current user created in the current workspace. + List workflow templates created by the caller in the current workspace. - Scoped on both `(workspace_id, created_by)` so when workspaces become - multi-user this endpoint keeps returning only the caller's own rows — - other workspace members' public/starter templates surface via the - gallery endpoint (`GET /api/v1/templates`) instead. + Returns only templates owned by the caller; templates shared by other + workspace members or platform starter templates are not included — use + `GET /api/v1/templates` for the full gallery. Supports offset-based + pagination via `offset` / `limit`. Combine `category`, `search`, and + `favorites_only` to narrow results; multiple `category` values are OR'd. Parameters ---------- @@ -1125,12 +475,16 @@ async def list_my_templates( Repeat for OR, e.g. ?category=media&category=creative search : typing.Optional[str] + Full-text search against template name and description. sort : typing.Optional[ListMyTemplatesApiV1UsersMeTemplatesGetRequestSort] + Sort order: `recent` (last updated), `name` (A–Z), or `uses` (most used). offset : typing.Optional[int] + Number of results to skip for pagination. limit : typing.Optional[int] + Maximum number of results to return (1–100). favorites_only : typing.Optional[bool] diff --git a/src/onepin/users/raw_client.py b/src/onepin/users/raw_client.py index a4620fd..e252fe4 100644 --- a/src/onepin/users/raw_client.py +++ b/src/onepin/users/raw_client.py @@ -6,23 +6,14 @@ from ..core.api_error import ApiError from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper from ..core.http_response import AsyncHttpResponse, HttpResponse -from ..core.jsonable_encoder import encode_path_param from ..core.parse_error import ParsingError from ..core.pydantic_utilities import parse_obj_as from ..core.request_options import RequestOptions from ..errors.unprocessable_entity_error import UnprocessableEntityError from ..types.api_list_response_template_out import ApiListResponseTemplateOut from ..types.api_response_balance_response import ApiResponseBalanceResponse -from ..types.api_response_customer_subscription_response import ApiResponseCustomerSubscriptionResponse -from ..types.api_response_dict import ApiResponseDict from ..types.api_response_email_notification_preferences_out import ApiResponseEmailNotificationPreferencesOut -from ..types.api_response_invoice_list_response import ApiResponseInvoiceListResponse -from ..types.api_response_list_payment_method_response import ApiResponseListPaymentMethodResponse from ..types.api_response_plan_limits import ApiResponsePlanLimits -from ..types.api_response_setup_intent_response import ApiResponseSetupIntentResponse -from ..types.api_response_union_customer_subscription_response_none_type import ( - ApiResponseUnionCustomerSubscriptionResponseNoneType, -) from ..types.template_category import TemplateCategory from .types.list_my_templates_api_v1users_me_templates_get_request_sort import ( ListMyTemplatesApiV1UsersMeTemplatesGetRequestSort, @@ -33,1075 +24,50 @@ OMIT = typing.cast(typing.Any, ...) -class RawUsersClient: - def __init__(self, *, client_wrapper: SyncClientWrapper): - self._client_wrapper = client_wrapper - - def get_current_subscription( - self, *, request_options: typing.Optional[RequestOptions] = None - ) -> HttpResponse[ApiResponseUnionCustomerSubscriptionResponseNoneType]: - """ - Get the current user's active subscription. - - Parameters - ---------- - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - HttpResponse[ApiResponseUnionCustomerSubscriptionResponseNoneType] - Successful Response - """ - _response = self._client_wrapper.httpx_client.request( - "api/v1/users/me/subscription", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - ApiResponseUnionCustomerSubscriptionResponseNoneType, - parse_obj_as( - type_=ApiResponseUnionCustomerSubscriptionResponseNoneType, # 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 subscribe( - self, *, plan_id: str, request_options: typing.Optional[RequestOptions] = None - ) -> HttpResponse[ApiResponseCustomerSubscriptionResponse]: - """ - Create a subscription using the default payment method. - - Parameters - ---------- - plan_id : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - HttpResponse[ApiResponseCustomerSubscriptionResponse] - Successful Response - """ - _response = self._client_wrapper.httpx_client.request( - "api/v1/users/me/subscription", - method="POST", - json={ - "plan_id": plan_id, - }, - headers={ - "content-type": "application/json", - }, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - ApiResponseCustomerSubscriptionResponse, - parse_obj_as( - type_=ApiResponseCustomerSubscriptionResponse, # 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 cancel_subscription( - self, *, request_options: typing.Optional[RequestOptions] = None - ) -> HttpResponse[ApiResponseCustomerSubscriptionResponse]: - """ - Cancel the current user's subscription at period end. - - Parameters - ---------- - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - HttpResponse[ApiResponseCustomerSubscriptionResponse] - Successful Response - """ - _response = self._client_wrapper.httpx_client.request( - "api/v1/users/me/subscription/cancel", - method="POST", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - ApiResponseCustomerSubscriptionResponse, - parse_obj_as( - type_=ApiResponseCustomerSubscriptionResponse, # 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 change_plan( - self, *, new_plan_id: str, request_options: typing.Optional[RequestOptions] = None - ) -> HttpResponse[ApiResponseCustomerSubscriptionResponse]: - """ - Switch the current user's subscription to a different plan. - - Parameters - ---------- - new_plan_id : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - HttpResponse[ApiResponseCustomerSubscriptionResponse] - Successful Response - """ - _response = self._client_wrapper.httpx_client.request( - "api/v1/users/me/subscription/change-plan", - method="POST", - json={ - "new_plan_id": new_plan_id, - }, - headers={ - "content-type": "application/json", - }, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - ApiResponseCustomerSubscriptionResponse, - parse_obj_as( - type_=ApiResponseCustomerSubscriptionResponse, # 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 cancel_scheduled_change( - self, *, request_options: typing.Optional[RequestOptions] = None - ) -> HttpResponse[ApiResponseCustomerSubscriptionResponse]: - """ - Cancel a scheduled plan downgrade. - - Parameters - ---------- - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - HttpResponse[ApiResponseCustomerSubscriptionResponse] - Successful Response - """ - _response = self._client_wrapper.httpx_client.request( - "api/v1/users/me/subscription/cancel-scheduled-change", - method="POST", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - ApiResponseCustomerSubscriptionResponse, - parse_obj_as( - type_=ApiResponseCustomerSubscriptionResponse, # 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 list_payment_methods( - self, *, request_options: typing.Optional[RequestOptions] = None - ) -> HttpResponse[ApiResponseListPaymentMethodResponse]: - """ - List the current user's saved payment methods. - - Parameters - ---------- - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - HttpResponse[ApiResponseListPaymentMethodResponse] - Successful Response - """ - _response = self._client_wrapper.httpx_client.request( - "api/v1/users/me/payment-methods", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - ApiResponseListPaymentMethodResponse, - parse_obj_as( - type_=ApiResponseListPaymentMethodResponse, # 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 add_payment_method( - self, *, request_options: typing.Optional[RequestOptions] = None - ) -> HttpResponse[ApiResponseSetupIntentResponse]: - """ - Create a Stripe SetupIntent to add a new payment method. - - Parameters - ---------- - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - HttpResponse[ApiResponseSetupIntentResponse] - Successful Response - """ - _response = self._client_wrapper.httpx_client.request( - "api/v1/users/me/payment-methods", - method="POST", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - ApiResponseSetupIntentResponse, - parse_obj_as( - type_=ApiResponseSetupIntentResponse, # 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 delete_payment_method( - self, payment_method_id: str, *, request_options: typing.Optional[RequestOptions] = None - ) -> HttpResponse[ApiResponseDict]: - """ - Detach a payment method from the current user's account. - - Parameters - ---------- - payment_method_id : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - HttpResponse[ApiResponseDict] - Successful Response - """ - _response = self._client_wrapper.httpx_client.request( - f"api/v1/users/me/payment-methods/{encode_path_param(payment_method_id)}", - method="DELETE", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - ApiResponseDict, - parse_obj_as( - type_=ApiResponseDict, # 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 set_default_payment_method( - self, payment_method_id: str, *, request_options: typing.Optional[RequestOptions] = None - ) -> HttpResponse[ApiResponseDict]: - """ - Set a payment method as the default for the current user. - - Parameters - ---------- - payment_method_id : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - HttpResponse[ApiResponseDict] - Successful Response - """ - _response = self._client_wrapper.httpx_client.request( - f"api/v1/users/me/payment-methods/{encode_path_param(payment_method_id)}/default", - method="PUT", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - ApiResponseDict, - parse_obj_as( - type_=ApiResponseDict, # 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_my_credits( - self, *, request_options: typing.Optional[RequestOptions] = None - ) -> HttpResponse[ApiResponseBalanceResponse]: - """ - Return the current user's credit balance + monthly grant + period anchor. - - Free-tier users have no Subscription row; the response falls back to the - canonical FREE Plan (1000 credits/mo, calendar-month boundary). - - Parameters - ---------- - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - HttpResponse[ApiResponseBalanceResponse] - Successful Response - """ - _response = self._client_wrapper.httpx_client.request( - "api/v1/users/me/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 == 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_my_plan_limits( - self, *, request_options: typing.Optional[RequestOptions] = None - ) -> HttpResponse[ApiResponsePlanLimits]: - """ - Return the typed plan limits for the current user (FE plan-card UI consumer). - - Parameters - ---------- - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - HttpResponse[ApiResponsePlanLimits] - Successful Response - """ - _response = self._client_wrapper.httpx_client.request( - "api/v1/users/me/limits", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - ApiResponsePlanLimits, - parse_obj_as( - type_=ApiResponsePlanLimits, # 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 list_invoices( - self, - *, - limit: typing.Optional[int] = None, - starting_after: typing.Optional[str] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> HttpResponse[ApiResponseInvoiceListResponse]: - """ - List invoices for the current user. - - Parameters - ---------- - limit : typing.Optional[int] - - starting_after : typing.Optional[str] - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - HttpResponse[ApiResponseInvoiceListResponse] - Successful Response - """ - _response = self._client_wrapper.httpx_client.request( - "api/v1/users/me/invoices", - method="GET", - params={ - "limit": limit, - "starting_after": starting_after, - }, - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - ApiResponseInvoiceListResponse, - parse_obj_as( - type_=ApiResponseInvoiceListResponse, # 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_current_notification_preferences( - self, *, request_options: typing.Optional[RequestOptions] = None - ) -> HttpResponse[ApiResponseEmailNotificationPreferencesOut]: - """ - Get the current user's email notification preferences. - - Parameters - ---------- - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - HttpResponse[ApiResponseEmailNotificationPreferencesOut] - Successful Response - """ - _response = self._client_wrapper.httpx_client.request( - "api/v1/users/me/notification-preferences", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - ApiResponseEmailNotificationPreferencesOut, - parse_obj_as( - type_=ApiResponseEmailNotificationPreferencesOut, # 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 update_current_notification_preferences( - self, - *, - completed_generation_email: typing.Optional[bool] = OMIT, - failed_generation_email: typing.Optional[bool] = OMIT, - request_options: typing.Optional[RequestOptions] = None, - ) -> HttpResponse[ApiResponseEmailNotificationPreferencesOut]: - """ - Partially update the current user's email notification preferences. - - Parameters - ---------- - completed_generation_email : typing.Optional[bool] - - failed_generation_email : typing.Optional[bool] - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - HttpResponse[ApiResponseEmailNotificationPreferencesOut] - Successful Response - """ - _response = self._client_wrapper.httpx_client.request( - "api/v1/users/me/notification-preferences", - method="PATCH", - json={ - "completed_generation_email": completed_generation_email, - "failed_generation_email": failed_generation_email, - }, - headers={ - "content-type": "application/json", - }, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - ApiResponseEmailNotificationPreferencesOut, - parse_obj_as( - type_=ApiResponseEmailNotificationPreferencesOut, # 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 list_my_templates( - self, - *, - category: typing.Optional[typing.Sequence[TemplateCategory]] = None, - search: typing.Optional[str] = None, - sort: typing.Optional[ListMyTemplatesApiV1UsersMeTemplatesGetRequestSort] = None, - offset: typing.Optional[int] = None, - limit: typing.Optional[int] = None, - favorites_only: typing.Optional[bool] = None, - workspace_id: typing.Optional[str] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> HttpResponse[ApiListResponseTemplateOut]: - """ - List templates the current user created in the current workspace. - - Scoped on both `(workspace_id, created_by)` so when workspaces become - multi-user this endpoint keeps returning only the caller's own rows — - other workspace members' public/starter templates surface via the - gallery endpoint (`GET /api/v1/templates`) instead. - - Parameters - ---------- - category : typing.Optional[typing.Sequence[TemplateCategory]] - Repeat for OR, e.g. ?category=media&category=creative - - search : typing.Optional[str] - - sort : typing.Optional[ListMyTemplatesApiV1UsersMeTemplatesGetRequestSort] - - offset : typing.Optional[int] - - limit : typing.Optional[int] - - favorites_only : typing.Optional[bool] - - workspace_id : typing.Optional[str] - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - HttpResponse[ApiListResponseTemplateOut] - Successful Response - """ - _response = self._client_wrapper.httpx_client.request( - "api/v1/users/me/templates", - method="GET", - params={ - "category": category, - "search": search, - "sort": sort, - "offset": offset, - "limit": limit, - "favorites_only": favorites_only, - }, - 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( - ApiListResponseTemplateOut, - parse_obj_as( - type_=ApiListResponseTemplateOut, # 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) - - -class AsyncRawUsersClient: - def __init__(self, *, client_wrapper: AsyncClientWrapper): - self._client_wrapper = client_wrapper - - async def get_current_subscription( - self, *, request_options: typing.Optional[RequestOptions] = None - ) -> AsyncHttpResponse[ApiResponseUnionCustomerSubscriptionResponseNoneType]: - """ - Get the current user's active subscription. - - Parameters - ---------- - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AsyncHttpResponse[ApiResponseUnionCustomerSubscriptionResponseNoneType] - Successful Response - """ - _response = await self._client_wrapper.httpx_client.request( - "api/v1/users/me/subscription", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - ApiResponseUnionCustomerSubscriptionResponseNoneType, - parse_obj_as( - type_=ApiResponseUnionCustomerSubscriptionResponseNoneType, # 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 subscribe( - self, *, plan_id: str, request_options: typing.Optional[RequestOptions] = None - ) -> AsyncHttpResponse[ApiResponseCustomerSubscriptionResponse]: - """ - Create a subscription using the default payment method. - - Parameters - ---------- - plan_id : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AsyncHttpResponse[ApiResponseCustomerSubscriptionResponse] - Successful Response - """ - _response = await self._client_wrapper.httpx_client.request( - "api/v1/users/me/subscription", - method="POST", - json={ - "plan_id": plan_id, - }, - headers={ - "content-type": "application/json", - }, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - ApiResponseCustomerSubscriptionResponse, - parse_obj_as( - type_=ApiResponseCustomerSubscriptionResponse, # 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 cancel_subscription( - self, *, request_options: typing.Optional[RequestOptions] = None - ) -> AsyncHttpResponse[ApiResponseCustomerSubscriptionResponse]: - """ - Cancel the current user's subscription at period end. - - Parameters - ---------- - request_options : typing.Optional[RequestOptions] - Request-specific configuration. +class RawUsersClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._client_wrapper = client_wrapper - Returns - ------- - AsyncHttpResponse[ApiResponseCustomerSubscriptionResponse] - Successful Response + def get_my_credits( + self, *, request_options: typing.Optional[RequestOptions] = None + ) -> HttpResponse[ApiResponseBalanceResponse]: """ - _response = await self._client_wrapper.httpx_client.request( - "api/v1/users/me/subscription/cancel", - method="POST", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - ApiResponseCustomerSubscriptionResponse, - parse_obj_as( - type_=ApiResponseCustomerSubscriptionResponse, # 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) + Return the caller's current credit balance and billing period details. - async def change_plan( - self, *, new_plan_id: str, request_options: typing.Optional[RequestOptions] = None - ) -> AsyncHttpResponse[ApiResponseCustomerSubscriptionResponse]: - """ - Switch the current user's subscription to a different plan. + `balance` is the authoritative gate value: use it to decide whether to + attempt a workflow run. `remaining` is a display convenience derived from + settled ledger entries and may temporarily exceed `balance` while a workflow + run holds an open reserve. `used` reflects credits consumed in the current + billing period. `plan_grant` is the total monthly credit allowance for the + caller's plan, enabling a "X / Y used" display. `period_start` and + `period_end` mark the boundaries of the current billing window; free-tier + callers use a calendar-month boundary. Parameters ---------- - new_plan_id : str - request_options : typing.Optional[RequestOptions] Request-specific configuration. Returns ------- - AsyncHttpResponse[ApiResponseCustomerSubscriptionResponse] + HttpResponse[ApiResponseBalanceResponse] Successful Response """ - _response = await self._client_wrapper.httpx_client.request( - "api/v1/users/me/subscription/change-plan", - method="POST", - json={ - "new_plan_id": new_plan_id, - }, - headers={ - "content-type": "application/json", - }, + _response = self._client_wrapper.httpx_client.request( + "api/v1/users/me/credits", + method="GET", request_options=request_options, - omit=OMIT, ) try: if 200 <= _response.status_code < 300: _data = typing.cast( - ApiResponseCustomerSubscriptionResponse, + ApiResponseBalanceResponse, parse_obj_as( - type_=ApiResponseCustomerSubscriptionResponse, # type: ignore + type_=ApiResponseBalanceResponse, # type: ignore object_=_response.json(), ), ) - return AsyncHttpResponse(response=_response, data=_data) + return HttpResponse(response=_response, data=_data) if _response.status_code == 422: raise UnprocessableEntityError( headers=dict(_response.headers), @@ -1122,11 +88,18 @@ async def change_plan( ) raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) - async def cancel_scheduled_change( + def get_my_plan_limits( self, *, request_options: typing.Optional[RequestOptions] = None - ) -> AsyncHttpResponse[ApiResponseCustomerSubscriptionResponse]: + ) -> HttpResponse[ApiResponsePlanLimits]: """ - Cancel a scheduled plan downgrade. + 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. Parameters ---------- @@ -1135,24 +108,24 @@ async def cancel_scheduled_change( Returns ------- - AsyncHttpResponse[ApiResponseCustomerSubscriptionResponse] + HttpResponse[ApiResponsePlanLimits] Successful Response """ - _response = await self._client_wrapper.httpx_client.request( - "api/v1/users/me/subscription/cancel-scheduled-change", - method="POST", + _response = self._client_wrapper.httpx_client.request( + "api/v1/users/me/limits", + method="GET", request_options=request_options, ) try: if 200 <= _response.status_code < 300: _data = typing.cast( - ApiResponseCustomerSubscriptionResponse, + ApiResponsePlanLimits, parse_obj_as( - type_=ApiResponseCustomerSubscriptionResponse, # type: ignore + type_=ApiResponsePlanLimits, # type: ignore object_=_response.json(), ), ) - return AsyncHttpResponse(response=_response, data=_data) + return HttpResponse(response=_response, data=_data) if _response.status_code == 422: raise UnprocessableEntityError( headers=dict(_response.headers), @@ -1173,11 +146,15 @@ async def cancel_scheduled_change( ) raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) - async def list_payment_methods( + def get_current_notification_preferences( self, *, request_options: typing.Optional[RequestOptions] = None - ) -> AsyncHttpResponse[ApiResponseListPaymentMethodResponse]: + ) -> HttpResponse[ApiResponseEmailNotificationPreferencesOut]: """ - List the current user's saved payment methods. + Return the caller's current email notification settings. + + Each boolean field corresponds to a notification category. `true` means the + caller will receive that email; `false` means they have opted out. Use + `PATCH /me/notification-preferences` to change individual preferences. Parameters ---------- @@ -1186,24 +163,24 @@ async def list_payment_methods( Returns ------- - AsyncHttpResponse[ApiResponseListPaymentMethodResponse] + HttpResponse[ApiResponseEmailNotificationPreferencesOut] Successful Response """ - _response = await self._client_wrapper.httpx_client.request( - "api/v1/users/me/payment-methods", + _response = self._client_wrapper.httpx_client.request( + "api/v1/users/me/notification-preferences", method="GET", request_options=request_options, ) try: if 200 <= _response.status_code < 300: _data = typing.cast( - ApiResponseListPaymentMethodResponse, + ApiResponseEmailNotificationPreferencesOut, parse_obj_as( - type_=ApiResponseListPaymentMethodResponse, # type: ignore + type_=ApiResponseEmailNotificationPreferencesOut, # type: ignore object_=_response.json(), ), ) - return AsyncHttpResponse(response=_response, data=_data) + return HttpResponse(response=_response, data=_data) if _response.status_code == 422: raise UnprocessableEntityError( headers=dict(_response.headers), @@ -1224,37 +201,59 @@ async def list_payment_methods( ) raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) - async def add_payment_method( - self, *, request_options: typing.Optional[RequestOptions] = None - ) -> AsyncHttpResponse[ApiResponseSetupIntentResponse]: + def update_current_notification_preferences( + self, + *, + completed_generation_email: typing.Optional[bool] = OMIT, + failed_generation_email: typing.Optional[bool] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[ApiResponseEmailNotificationPreferencesOut]: """ - Create a Stripe SetupIntent to add a new payment method. + Partially update the caller's email notification preferences. + + Send only the fields you want to change; omitted fields are left unchanged. + All provided fields must be boolean — explicit `null` values are rejected + with a 422. Returns the full updated preference object. Parameters ---------- + completed_generation_email : typing.Optional[bool] + Set to true to enable or false to disable completion emails. Omit to leave unchanged. + + failed_generation_email : typing.Optional[bool] + Set to true to enable or false to disable failure emails. Omit to leave unchanged. + request_options : typing.Optional[RequestOptions] Request-specific configuration. Returns ------- - AsyncHttpResponse[ApiResponseSetupIntentResponse] + HttpResponse[ApiResponseEmailNotificationPreferencesOut] Successful Response """ - _response = await self._client_wrapper.httpx_client.request( - "api/v1/users/me/payment-methods", - method="POST", + _response = self._client_wrapper.httpx_client.request( + "api/v1/users/me/notification-preferences", + method="PATCH", + json={ + "completed_generation_email": completed_generation_email, + "failed_generation_email": failed_generation_email, + }, + headers={ + "content-type": "application/json", + }, request_options=request_options, + omit=OMIT, ) try: if 200 <= _response.status_code < 300: _data = typing.cast( - ApiResponseSetupIntentResponse, + ApiResponseEmailNotificationPreferencesOut, parse_obj_as( - type_=ApiResponseSetupIntentResponse, # type: ignore + type_=ApiResponseEmailNotificationPreferencesOut, # type: ignore object_=_response.json(), ), ) - return AsyncHttpResponse(response=_response, data=_data) + return HttpResponse(response=_response, data=_data) if _response.status_code == 422: raise UnprocessableEntityError( headers=dict(_response.headers), @@ -1275,92 +274,82 @@ async def add_payment_method( ) raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) - async def delete_payment_method( - self, payment_method_id: str, *, request_options: typing.Optional[RequestOptions] = None - ) -> AsyncHttpResponse[ApiResponseDict]: + def list_my_templates( + self, + *, + category: typing.Optional[typing.Sequence[TemplateCategory]] = None, + search: typing.Optional[str] = None, + sort: typing.Optional[ListMyTemplatesApiV1UsersMeTemplatesGetRequestSort] = None, + offset: typing.Optional[int] = None, + limit: typing.Optional[int] = None, + favorites_only: typing.Optional[bool] = None, + workspace_id: typing.Optional[str] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[ApiListResponseTemplateOut]: """ - Detach a payment method from the current user's account. + List workflow templates created by the caller in the current workspace. + + Returns only templates owned by the caller; templates shared by other + workspace members or platform starter templates are not included — use + `GET /api/v1/templates` for the full gallery. Supports offset-based + pagination via `offset` / `limit`. Combine `category`, `search`, and + `favorites_only` to narrow results; multiple `category` values are OR'd. Parameters ---------- - payment_method_id : str + category : typing.Optional[typing.Sequence[TemplateCategory]] + Repeat for OR, e.g. ?category=media&category=creative - request_options : typing.Optional[RequestOptions] - Request-specific configuration. + search : typing.Optional[str] + Full-text search against template name and description. - Returns - ------- - AsyncHttpResponse[ApiResponseDict] - Successful Response - """ - _response = await self._client_wrapper.httpx_client.request( - f"api/v1/users/me/payment-methods/{encode_path_param(payment_method_id)}", - method="DELETE", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - ApiResponseDict, - parse_obj_as( - type_=ApiResponseDict, # 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) + sort : typing.Optional[ListMyTemplatesApiV1UsersMeTemplatesGetRequestSort] + Sort order: `recent` (last updated), `name` (A–Z), or `uses` (most used). - async def set_default_payment_method( - self, payment_method_id: str, *, request_options: typing.Optional[RequestOptions] = None - ) -> AsyncHttpResponse[ApiResponseDict]: - """ - Set a payment method as the default for the current user. + offset : typing.Optional[int] + Number of results to skip for pagination. - Parameters - ---------- - payment_method_id : str + limit : typing.Optional[int] + Maximum number of results to return (1–100). + + favorites_only : typing.Optional[bool] + + workspace_id : typing.Optional[str] request_options : typing.Optional[RequestOptions] Request-specific configuration. Returns ------- - AsyncHttpResponse[ApiResponseDict] + HttpResponse[ApiListResponseTemplateOut] Successful Response """ - _response = await self._client_wrapper.httpx_client.request( - f"api/v1/users/me/payment-methods/{encode_path_param(payment_method_id)}/default", - method="PUT", + _response = self._client_wrapper.httpx_client.request( + "api/v1/users/me/templates", + method="GET", + params={ + "category": category, + "search": search, + "sort": sort, + "offset": offset, + "limit": limit, + "favorites_only": favorites_only, + }, + 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( - ApiResponseDict, + ApiListResponseTemplateOut, parse_obj_as( - type_=ApiResponseDict, # type: ignore + type_=ApiListResponseTemplateOut, # type: ignore object_=_response.json(), ), ) - return AsyncHttpResponse(response=_response, data=_data) + return HttpResponse(response=_response, data=_data) if _response.status_code == 422: raise UnprocessableEntityError( headers=dict(_response.headers), @@ -1381,14 +370,25 @@ async def set_default_payment_method( ) raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + +class AsyncRawUsersClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._client_wrapper = client_wrapper + async def get_my_credits( self, *, request_options: typing.Optional[RequestOptions] = None ) -> AsyncHttpResponse[ApiResponseBalanceResponse]: """ - Return the current user's credit balance + monthly grant + period anchor. + Return the caller's current credit balance and billing period details. - Free-tier users have no Subscription row; the response falls back to the - canonical FREE Plan (1000 credits/mo, calendar-month boundary). + `balance` is the authoritative gate value: use it to decide whether to + attempt a workflow run. `remaining` is a display convenience derived from + settled ledger entries and may temporarily exceed `balance` while a workflow + run holds an open reserve. `used` reflects credits consumed in the current + billing period. `plan_grant` is the total monthly credit allowance for the + caller's plan, enabling a "X / Y used" display. `period_start` and + `period_end` mark the boundaries of the current billing window; free-tier + callers use a calendar-month boundary. Parameters ---------- @@ -1439,7 +439,14 @@ async def get_my_plan_limits( self, *, request_options: typing.Optional[RequestOptions] = None ) -> AsyncHttpResponse[ApiResponsePlanLimits]: """ - Return the typed plan limits for the current user (FE plan-card UI consumer). + 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. Parameters ---------- @@ -1486,74 +493,15 @@ async def get_my_plan_limits( ) raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) - async def list_invoices( - self, - *, - limit: typing.Optional[int] = None, - starting_after: typing.Optional[str] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> AsyncHttpResponse[ApiResponseInvoiceListResponse]: - """ - List invoices for the current user. - - Parameters - ---------- - limit : typing.Optional[int] - - starting_after : typing.Optional[str] - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AsyncHttpResponse[ApiResponseInvoiceListResponse] - Successful Response - """ - _response = await self._client_wrapper.httpx_client.request( - "api/v1/users/me/invoices", - method="GET", - params={ - "limit": limit, - "starting_after": starting_after, - }, - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - ApiResponseInvoiceListResponse, - parse_obj_as( - type_=ApiResponseInvoiceListResponse, # 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_current_notification_preferences( self, *, request_options: typing.Optional[RequestOptions] = None ) -> AsyncHttpResponse[ApiResponseEmailNotificationPreferencesOut]: """ - Get the current user's email notification preferences. + Return the caller's current email notification settings. + + Each boolean field corresponds to a notification category. `true` means the + caller will receive that email; `false` means they have opted out. Use + `PATCH /me/notification-preferences` to change individual preferences. Parameters ---------- @@ -1608,13 +556,19 @@ async def update_current_notification_preferences( request_options: typing.Optional[RequestOptions] = None, ) -> AsyncHttpResponse[ApiResponseEmailNotificationPreferencesOut]: """ - Partially update the current user's email notification preferences. + Partially update the caller's email notification preferences. + + Send only the fields you want to change; omitted fields are left unchanged. + All provided fields must be boolean — explicit `null` values are rejected + with a 422. Returns the full updated preference object. Parameters ---------- completed_generation_email : typing.Optional[bool] + Set to true to enable or false to disable completion emails. Omit to leave unchanged. failed_generation_email : typing.Optional[bool] + Set to true to enable or false to disable failure emails. Omit to leave unchanged. request_options : typing.Optional[RequestOptions] Request-specific configuration. @@ -1680,12 +634,13 @@ async def list_my_templates( request_options: typing.Optional[RequestOptions] = None, ) -> AsyncHttpResponse[ApiListResponseTemplateOut]: """ - List templates the current user created in the current workspace. + List workflow templates created by the caller in the current workspace. - Scoped on both `(workspace_id, created_by)` so when workspaces become - multi-user this endpoint keeps returning only the caller's own rows — - other workspace members' public/starter templates surface via the - gallery endpoint (`GET /api/v1/templates`) instead. + Returns only templates owned by the caller; templates shared by other + workspace members or platform starter templates are not included — use + `GET /api/v1/templates` for the full gallery. Supports offset-based + pagination via `offset` / `limit`. Combine `category`, `search`, and + `favorites_only` to narrow results; multiple `category` values are OR'd. Parameters ---------- @@ -1693,12 +648,16 @@ async def list_my_templates( Repeat for OR, e.g. ?category=media&category=creative search : typing.Optional[str] + Full-text search against template name and description. sort : typing.Optional[ListMyTemplatesApiV1UsersMeTemplatesGetRequestSort] + Sort order: `recent` (last updated), `name` (A–Z), or `uses` (most used). offset : typing.Optional[int] + Number of results to skip for pagination. limit : typing.Optional[int] + Maximum number of results to return (1–100). favorites_only : typing.Optional[bool] diff --git a/src/onepin/voices/__init__.py b/src/onepin/voices/__init__.py index e6889a8..2a2b490 100644 --- a/src/onepin/voices/__init__.py +++ b/src/onepin/voices/__init__.py @@ -7,6 +7,7 @@ if typing.TYPE_CHECKING: from .types import ( + GetVoiceFacetsApiV1VoicesFacetsGetRequestSourceItem, ListVoicesRequestLanguageItem, ListVoicesRequestOrderItem, ListVoicesRequestProviderItem, @@ -14,6 +15,7 @@ ListVoicesRequestSourceItem, ) _dynamic_imports: typing.Dict[str, str] = { + "GetVoiceFacetsApiV1VoicesFacetsGetRequestSourceItem": ".types", "ListVoicesRequestLanguageItem": ".types", "ListVoicesRequestOrderItem": ".types", "ListVoicesRequestProviderItem": ".types", @@ -44,6 +46,7 @@ def __dir__(): __all__ = [ + "GetVoiceFacetsApiV1VoicesFacetsGetRequestSourceItem", "ListVoicesRequestLanguageItem", "ListVoicesRequestOrderItem", "ListVoicesRequestProviderItem", diff --git a/src/onepin/voices/client.py b/src/onepin/voices/client.py index 5b878b7..7982af1 100644 --- a/src/onepin/voices/client.py +++ b/src/onepin/voices/client.py @@ -7,12 +7,16 @@ from ..types.api_counted_list_response_voice_out import ApiCountedListResponseVoiceOut from ..types.api_list_response_voice_similar_out import ApiListResponseVoiceSimilarOut from ..types.api_response_dict import ApiResponseDict +from ..types.api_response_voice_facets_out import ApiResponseVoiceFacetsOut from ..types.api_response_voice_out import ApiResponseVoiceOut from ..types.voice_accent import VoiceAccent from ..types.voice_age import VoiceAge from ..types.voice_category import VoiceCategory from ..types.voice_gender import VoiceGender from .raw_client import AsyncRawVoicesClient, RawVoicesClient +from .types.get_voice_facets_api_v1voices_facets_get_request_source_item import ( + GetVoiceFacetsApiV1VoicesFacetsGetRequestSourceItem, +) 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 @@ -62,11 +66,11 @@ def list( `?gender=female&gender=neutral&category=narration&source=platform&source=workspace`. Filters combine across fields with AND; within a field, values OR. - `language` uses Postgres `?|` (exists-any) against `voices.supported_languages`. - Platform voices with NULL `supported_languages` (catalog gaps) are treated - as general-use and match every locale filter. User-uploaded / cloned voices - with NULL stay excluded — NULL there means "language unknown" pending the - clone flow's language detection. + `language` matches a voice when any of its declared locales matches any + requested value. Platform voices with no declared locales (catalog gaps) + are treated as general-use and match every language filter. User-uploaded + / cloned voices with no declared locales are excluded — that state means + "language unknown" pending the clone flow's language detection. Multi-sort: `sort` and `order` are parallel lists. `?sort=uses_count&sort=name&order=desc&order=asc` orders primarily by uses_count DESC, secondarily by name ASC. When `order` @@ -79,13 +83,16 @@ def list( Parameters ---------- offset : typing.Optional[int] + Number of results to skip for pagination. limit : typing.Optional[int] + Maximum number of results to return (1–100). favorites_only : typing.Optional[bool] + When true, return only voices in the workspace's favorites list. source : typing.Optional[typing.Sequence[ListVoicesRequestSourceItem]] - Repeat for OR across scopes + Repeat for OR across scopes: `platform` for system-provided voices, `workspace` for workspace-owned voices. gender : typing.Optional[typing.Sequence[VoiceGender]] Repeat for OR @@ -100,6 +107,7 @@ def list( Repeat for OR search : typing.Optional[str] + Full-text search against voice name, description, and tags. sort : typing.Optional[typing.Sequence[ListVoicesRequestSortItem]] Repeat for multi-sort. Pairs with `order` index-wise. @@ -155,6 +163,116 @@ def list( ) return _response.data + def get_voice_facets( + self, + *, + favorites_only: typing.Optional[bool] = None, + source: typing.Optional[typing.Sequence[GetVoiceFacetsApiV1VoicesFacetsGetRequestSourceItem]] = None, + gender: typing.Optional[typing.Sequence[VoiceGender]] = None, + age: typing.Optional[typing.Sequence[VoiceAge]] = None, + category: typing.Optional[typing.Sequence[VoiceCategory]] = None, + accent: typing.Optional[typing.Sequence[VoiceAccent]] = None, + search: typing.Optional[str] = None, + provider: typing.Optional[typing.Sequence[str]] = None, + model: typing.Optional[typing.Sequence[str]] = None, + language: typing.Optional[typing.Sequence[str]] = None, + workspace_id: typing.Optional[str] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> ApiResponseVoiceFacetsOut: + """ + Filter-bar options (chips) for the voice browser, one list per dimension. + + Returns `providers`, `models`, `languages` (data-driven) plus `genders`, + `ages`, `categories`, `accents` (fixed enums) as `VoiceFacetItem[]` so the FE + builds the whole filter bar — with per-chip count badges — in a single request + instead of hardcoding option lists (mirrors `GET /dictionary/languages`). Each + item is `{value, label, count}`: `value` is passed straight back to + `GET /voices`; `label` is the display name for providers/models and `null` + elsewhere (the FE owns language + enum labels); `count` is the number of + matching voices. For `languages`/`models`, `count` counts only voices that + explicitly declare the value — "general-use" platform voices (no declared + locales/models) that `GET /voices` matches against every language/model filter + are not counted, so a chip's count can be lower than the `GET /voices` result. + + Accepts the SAME filters as `GET /voices` (tab scope `source`/`favorites_only`, + plus `provider`/`model`/`language`/`gender`/`age`/`category`/`accent`/`search`). + `count` is context-aware (faceted search): each dimension's counts apply every + OTHER active filter but exclude that dimension's own selection — e.g. with + `provider=elevenlabs` the language counts are scoped to ElevenLabs, while the + provider chips still show every provider so the caller can switch. + + Count-0 policy: data-driven dimensions omit count-0 values (only present ones, + each a valid `GET /voices` filter — providers/models restricted to the enabled + catalog, languages to the supported-locale allowlist, so a chip never 422s). + Enum dimensions always return the full enum in natural order, count-0 included, + for the FE to grey out. + + Parameters + ---------- + favorites_only : typing.Optional[bool] + Favorites tab scope + + source : typing.Optional[typing.Sequence[GetVoiceFacetsApiV1VoicesFacetsGetRequestSourceItem]] + Tab scope — repeat for OR, same values as GET /voices (e.g. platform, workspace) + + gender : typing.Optional[typing.Sequence[VoiceGender]] + Repeat for OR + + age : typing.Optional[typing.Sequence[VoiceAge]] + Repeat for OR + + category : typing.Optional[typing.Sequence[VoiceCategory]] + Repeat for OR + + accent : typing.Optional[typing.Sequence[VoiceAccent]] + Repeat for OR + + search : typing.Optional[str] + + provider : typing.Optional[typing.Sequence[str]] + Repeat for OR, e.g. ?provider=elevenlabs&provider=rime + + model : typing.Optional[typing.Sequence[str]] + Repeat for OR. Filters platform voices by TTS model, e.g. ?model=arcana&model=sonic-2 + + language : typing.Optional[typing.Sequence[str]] + Repeat for OR, e.g. ?language=en-us&language=ko-kr + + workspace_id : typing.Optional[str] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ApiResponseVoiceFacetsOut + Successful Response + + Examples + -------- + from onepin import OnePinClient + + client = OnePinClient( + token="YOUR_TOKEN", + ) + client.voices.get_voice_facets() + """ + _response = self._raw_client.get_voice_facets( + favorites_only=favorites_only, + source=source, + gender=gender, + age=age, + category=category, + accent=accent, + search=search, + provider=provider, + model=model, + language=language, + workspace_id=workspace_id, + request_options=request_options, + ) + return _response.data + def get( self, voice_id: str, @@ -163,7 +281,13 @@ def get( request_options: typing.Optional[RequestOptions] = None, ) -> ApiResponseVoiceOut: """ - Get a voice by ID, scoped to caller workspace + platform voices. + Fetch a single voice by its ID. + + Returns both platform (system-wide) voices and voices that belong to the + caller's workspace. Returns 404 when the voice does not exist or is not + accessible to the caller's workspace. The `sample_url` field is a + time-limited presigned URL valid for 1 hour; regenerate it by calling this + endpoint again rather than caching it long-term. Parameters ---------- @@ -203,13 +327,23 @@ def similar( request_options: typing.Optional[RequestOptions] = None, ) -> ApiListResponseVoiceSimilarOut: """ - Return voices nearest to a reference voice embedding. + Return voices acoustically similar to a reference voice. + + Results are ranked by semantic similarity score (descending) and include the + reference voice's workspace voices and all platform voices. Each result + includes a `similarity_score` between 0 and 1. Optionally filter by one or + more `language` BCP-47 codes (repeat the parameter for OR semantics); up to + 16 language values are accepted. Returns 503 when the reference voice has no + embedding yet — retry after the indicated `Retry-After` interval. Prefer this + endpoint over `GET /voices` with manual filtering when building a + "voices like this" recommendation UI. Parameters ---------- voice_id : str limit : typing.Optional[int] + Number of similar voices to return (1–50). language : typing.Optional[typing.Sequence[str]] Repeat for OR, e.g. ?language=en-us&language=ko-kr @@ -248,7 +382,12 @@ def favorite_voice( request_options: typing.Optional[RequestOptions] = None, ) -> ApiResponseVoiceOut: """ - Mark a voice as a workspace favorite. + Add a voice to the current workspace's favorites. + + Favorites are workspace-scoped, not per-user: all members of the workspace + see the same favorited set. Idempotent — favoriting a voice that is already + favorited succeeds without error. Returns the voice with `is_favorite=true`. + Requires the caller to have at least editor role in the workspace. Parameters ---------- @@ -288,7 +427,11 @@ def unfavorite_voice( request_options: typing.Optional[RequestOptions] = None, ) -> ApiResponseDict: """ - Remove a voice from workspace favorites. + Remove a voice from the current workspace's favorites. + + Idempotent — removing a voice that is not currently favorited succeeds + without error. Requires the caller to have at least editor role in the + workspace. Parameters ---------- @@ -363,11 +506,11 @@ async def list( `?gender=female&gender=neutral&category=narration&source=platform&source=workspace`. Filters combine across fields with AND; within a field, values OR. - `language` uses Postgres `?|` (exists-any) against `voices.supported_languages`. - Platform voices with NULL `supported_languages` (catalog gaps) are treated - as general-use and match every locale filter. User-uploaded / cloned voices - with NULL stay excluded — NULL there means "language unknown" pending the - clone flow's language detection. + `language` matches a voice when any of its declared locales matches any + requested value. Platform voices with no declared locales (catalog gaps) + are treated as general-use and match every language filter. User-uploaded + / cloned voices with no declared locales are excluded — that state means + "language unknown" pending the clone flow's language detection. Multi-sort: `sort` and `order` are parallel lists. `?sort=uses_count&sort=name&order=desc&order=asc` orders primarily by uses_count DESC, secondarily by name ASC. When `order` @@ -380,13 +523,16 @@ async def list( Parameters ---------- offset : typing.Optional[int] + Number of results to skip for pagination. limit : typing.Optional[int] + Maximum number of results to return (1–100). favorites_only : typing.Optional[bool] + When true, return only voices in the workspace's favorites list. source : typing.Optional[typing.Sequence[ListVoicesRequestSourceItem]] - Repeat for OR across scopes + Repeat for OR across scopes: `platform` for system-provided voices, `workspace` for workspace-owned voices. gender : typing.Optional[typing.Sequence[VoiceGender]] Repeat for OR @@ -401,6 +547,7 @@ async def list( Repeat for OR search : typing.Optional[str] + Full-text search against voice name, description, and tags. sort : typing.Optional[typing.Sequence[ListVoicesRequestSortItem]] Repeat for multi-sort. Pairs with `order` index-wise. @@ -464,6 +611,124 @@ async def main() -> None: ) return _response.data + async def get_voice_facets( + self, + *, + favorites_only: typing.Optional[bool] = None, + source: typing.Optional[typing.Sequence[GetVoiceFacetsApiV1VoicesFacetsGetRequestSourceItem]] = None, + gender: typing.Optional[typing.Sequence[VoiceGender]] = None, + age: typing.Optional[typing.Sequence[VoiceAge]] = None, + category: typing.Optional[typing.Sequence[VoiceCategory]] = None, + accent: typing.Optional[typing.Sequence[VoiceAccent]] = None, + search: typing.Optional[str] = None, + provider: typing.Optional[typing.Sequence[str]] = None, + model: typing.Optional[typing.Sequence[str]] = None, + language: typing.Optional[typing.Sequence[str]] = None, + workspace_id: typing.Optional[str] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> ApiResponseVoiceFacetsOut: + """ + Filter-bar options (chips) for the voice browser, one list per dimension. + + Returns `providers`, `models`, `languages` (data-driven) plus `genders`, + `ages`, `categories`, `accents` (fixed enums) as `VoiceFacetItem[]` so the FE + builds the whole filter bar — with per-chip count badges — in a single request + instead of hardcoding option lists (mirrors `GET /dictionary/languages`). Each + item is `{value, label, count}`: `value` is passed straight back to + `GET /voices`; `label` is the display name for providers/models and `null` + elsewhere (the FE owns language + enum labels); `count` is the number of + matching voices. For `languages`/`models`, `count` counts only voices that + explicitly declare the value — "general-use" platform voices (no declared + locales/models) that `GET /voices` matches against every language/model filter + are not counted, so a chip's count can be lower than the `GET /voices` result. + + Accepts the SAME filters as `GET /voices` (tab scope `source`/`favorites_only`, + plus `provider`/`model`/`language`/`gender`/`age`/`category`/`accent`/`search`). + `count` is context-aware (faceted search): each dimension's counts apply every + OTHER active filter but exclude that dimension's own selection — e.g. with + `provider=elevenlabs` the language counts are scoped to ElevenLabs, while the + provider chips still show every provider so the caller can switch. + + Count-0 policy: data-driven dimensions omit count-0 values (only present ones, + each a valid `GET /voices` filter — providers/models restricted to the enabled + catalog, languages to the supported-locale allowlist, so a chip never 422s). + Enum dimensions always return the full enum in natural order, count-0 included, + for the FE to grey out. + + Parameters + ---------- + favorites_only : typing.Optional[bool] + Favorites tab scope + + source : typing.Optional[typing.Sequence[GetVoiceFacetsApiV1VoicesFacetsGetRequestSourceItem]] + Tab scope — repeat for OR, same values as GET /voices (e.g. platform, workspace) + + gender : typing.Optional[typing.Sequence[VoiceGender]] + Repeat for OR + + age : typing.Optional[typing.Sequence[VoiceAge]] + Repeat for OR + + category : typing.Optional[typing.Sequence[VoiceCategory]] + Repeat for OR + + accent : typing.Optional[typing.Sequence[VoiceAccent]] + Repeat for OR + + search : typing.Optional[str] + + provider : typing.Optional[typing.Sequence[str]] + Repeat for OR, e.g. ?provider=elevenlabs&provider=rime + + model : typing.Optional[typing.Sequence[str]] + Repeat for OR. Filters platform voices by TTS model, e.g. ?model=arcana&model=sonic-2 + + language : typing.Optional[typing.Sequence[str]] + Repeat for OR, e.g. ?language=en-us&language=ko-kr + + workspace_id : typing.Optional[str] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ApiResponseVoiceFacetsOut + Successful Response + + Examples + -------- + import asyncio + + from onepin import AsyncOnePinClient + + client = AsyncOnePinClient( + token="YOUR_TOKEN", + ) + + + async def main() -> None: + await client.voices.get_voice_facets() + + + asyncio.run(main()) + """ + _response = await self._raw_client.get_voice_facets( + favorites_only=favorites_only, + source=source, + gender=gender, + age=age, + category=category, + accent=accent, + search=search, + provider=provider, + model=model, + language=language, + workspace_id=workspace_id, + request_options=request_options, + ) + return _response.data + async def get( self, voice_id: str, @@ -472,7 +737,13 @@ async def get( request_options: typing.Optional[RequestOptions] = None, ) -> ApiResponseVoiceOut: """ - Get a voice by ID, scoped to caller workspace + platform voices. + Fetch a single voice by its ID. + + Returns both platform (system-wide) voices and voices that belong to the + caller's workspace. Returns 404 when the voice does not exist or is not + accessible to the caller's workspace. The `sample_url` field is a + time-limited presigned URL valid for 1 hour; regenerate it by calling this + endpoint again rather than caching it long-term. Parameters ---------- @@ -520,13 +791,23 @@ async def similar( request_options: typing.Optional[RequestOptions] = None, ) -> ApiListResponseVoiceSimilarOut: """ - Return voices nearest to a reference voice embedding. + Return voices acoustically similar to a reference voice. + + Results are ranked by semantic similarity score (descending) and include the + reference voice's workspace voices and all platform voices. Each result + includes a `similarity_score` between 0 and 1. Optionally filter by one or + more `language` BCP-47 codes (repeat the parameter for OR semantics); up to + 16 language values are accepted. Returns 503 when the reference voice has no + embedding yet — retry after the indicated `Retry-After` interval. Prefer this + endpoint over `GET /voices` with manual filtering when building a + "voices like this" recommendation UI. Parameters ---------- voice_id : str limit : typing.Optional[int] + Number of similar voices to return (1–50). language : typing.Optional[typing.Sequence[str]] Repeat for OR, e.g. ?language=en-us&language=ko-kr @@ -573,7 +854,12 @@ async def favorite_voice( request_options: typing.Optional[RequestOptions] = None, ) -> ApiResponseVoiceOut: """ - Mark a voice as a workspace favorite. + Add a voice to the current workspace's favorites. + + Favorites are workspace-scoped, not per-user: all members of the workspace + see the same favorited set. Idempotent — favoriting a voice that is already + favorited succeeds without error. Returns the voice with `is_favorite=true`. + Requires the caller to have at least editor role in the workspace. Parameters ---------- @@ -621,7 +907,11 @@ async def unfavorite_voice( request_options: typing.Optional[RequestOptions] = None, ) -> ApiResponseDict: """ - Remove a voice from workspace favorites. + Remove a voice from the current workspace's favorites. + + Idempotent — removing a voice that is not currently favorited succeeds + without error. Requires the caller to have at least editor role in the + workspace. Parameters ---------- diff --git a/src/onepin/voices/raw_client.py b/src/onepin/voices/raw_client.py index 9a47e57..3894067 100644 --- a/src/onepin/voices/raw_client.py +++ b/src/onepin/voices/raw_client.py @@ -14,11 +14,15 @@ from ..types.api_counted_list_response_voice_out import ApiCountedListResponseVoiceOut from ..types.api_list_response_voice_similar_out import ApiListResponseVoiceSimilarOut from ..types.api_response_dict import ApiResponseDict +from ..types.api_response_voice_facets_out import ApiResponseVoiceFacetsOut from ..types.api_response_voice_out import ApiResponseVoiceOut from ..types.voice_accent import VoiceAccent from ..types.voice_age import VoiceAge from ..types.voice_category import VoiceCategory from ..types.voice_gender import VoiceGender +from .types.get_voice_facets_api_v1voices_facets_get_request_source_item import ( + GetVoiceFacetsApiV1VoicesFacetsGetRequestSourceItem, +) 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 @@ -58,11 +62,11 @@ def list( `?gender=female&gender=neutral&category=narration&source=platform&source=workspace`. Filters combine across fields with AND; within a field, values OR. - `language` uses Postgres `?|` (exists-any) against `voices.supported_languages`. - Platform voices with NULL `supported_languages` (catalog gaps) are treated - as general-use and match every locale filter. User-uploaded / cloned voices - with NULL stay excluded — NULL there means "language unknown" pending the - clone flow's language detection. + `language` matches a voice when any of its declared locales matches any + requested value. Platform voices with no declared locales (catalog gaps) + are treated as general-use and match every language filter. User-uploaded + / cloned voices with no declared locales are excluded — that state means + "language unknown" pending the clone flow's language detection. Multi-sort: `sort` and `order` are parallel lists. `?sort=uses_count&sort=name&order=desc&order=asc` orders primarily by uses_count DESC, secondarily by name ASC. When `order` @@ -75,13 +79,16 @@ def list( Parameters ---------- offset : typing.Optional[int] + Number of results to skip for pagination. limit : typing.Optional[int] + Maximum number of results to return (1–100). favorites_only : typing.Optional[bool] + When true, return only voices in the workspace's favorites list. source : typing.Optional[typing.Sequence[ListVoicesRequestSourceItem]] - Repeat for OR across scopes + Repeat for OR across scopes: `platform` for system-provided voices, `workspace` for workspace-owned voices. gender : typing.Optional[typing.Sequence[VoiceGender]] Repeat for OR @@ -96,6 +103,7 @@ def list( Repeat for OR search : typing.Optional[str] + Full-text search against voice name, description, and tags. sort : typing.Optional[typing.Sequence[ListVoicesRequestSortItem]] Repeat for multi-sort. Pairs with `order` index-wise. @@ -176,6 +184,141 @@ def list( ) raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + def get_voice_facets( + self, + *, + favorites_only: typing.Optional[bool] = None, + source: typing.Optional[typing.Sequence[GetVoiceFacetsApiV1VoicesFacetsGetRequestSourceItem]] = None, + gender: typing.Optional[typing.Sequence[VoiceGender]] = None, + age: typing.Optional[typing.Sequence[VoiceAge]] = None, + category: typing.Optional[typing.Sequence[VoiceCategory]] = None, + accent: typing.Optional[typing.Sequence[VoiceAccent]] = None, + search: typing.Optional[str] = None, + provider: typing.Optional[typing.Sequence[str]] = None, + model: typing.Optional[typing.Sequence[str]] = None, + language: typing.Optional[typing.Sequence[str]] = None, + workspace_id: typing.Optional[str] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[ApiResponseVoiceFacetsOut]: + """ + Filter-bar options (chips) for the voice browser, one list per dimension. + + Returns `providers`, `models`, `languages` (data-driven) plus `genders`, + `ages`, `categories`, `accents` (fixed enums) as `VoiceFacetItem[]` so the FE + builds the whole filter bar — with per-chip count badges — in a single request + instead of hardcoding option lists (mirrors `GET /dictionary/languages`). Each + item is `{value, label, count}`: `value` is passed straight back to + `GET /voices`; `label` is the display name for providers/models and `null` + elsewhere (the FE owns language + enum labels); `count` is the number of + matching voices. For `languages`/`models`, `count` counts only voices that + explicitly declare the value — "general-use" platform voices (no declared + locales/models) that `GET /voices` matches against every language/model filter + are not counted, so a chip's count can be lower than the `GET /voices` result. + + Accepts the SAME filters as `GET /voices` (tab scope `source`/`favorites_only`, + plus `provider`/`model`/`language`/`gender`/`age`/`category`/`accent`/`search`). + `count` is context-aware (faceted search): each dimension's counts apply every + OTHER active filter but exclude that dimension's own selection — e.g. with + `provider=elevenlabs` the language counts are scoped to ElevenLabs, while the + provider chips still show every provider so the caller can switch. + + Count-0 policy: data-driven dimensions omit count-0 values (only present ones, + each a valid `GET /voices` filter — providers/models restricted to the enabled + catalog, languages to the supported-locale allowlist, so a chip never 422s). + Enum dimensions always return the full enum in natural order, count-0 included, + for the FE to grey out. + + Parameters + ---------- + favorites_only : typing.Optional[bool] + Favorites tab scope + + source : typing.Optional[typing.Sequence[GetVoiceFacetsApiV1VoicesFacetsGetRequestSourceItem]] + Tab scope — repeat for OR, same values as GET /voices (e.g. platform, workspace) + + gender : typing.Optional[typing.Sequence[VoiceGender]] + Repeat for OR + + age : typing.Optional[typing.Sequence[VoiceAge]] + Repeat for OR + + category : typing.Optional[typing.Sequence[VoiceCategory]] + Repeat for OR + + accent : typing.Optional[typing.Sequence[VoiceAccent]] + Repeat for OR + + search : typing.Optional[str] + + provider : typing.Optional[typing.Sequence[str]] + Repeat for OR, e.g. ?provider=elevenlabs&provider=rime + + model : typing.Optional[typing.Sequence[str]] + Repeat for OR. Filters platform voices by TTS model, e.g. ?model=arcana&model=sonic-2 + + language : typing.Optional[typing.Sequence[str]] + Repeat for OR, e.g. ?language=en-us&language=ko-kr + + workspace_id : typing.Optional[str] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[ApiResponseVoiceFacetsOut] + Successful Response + """ + _response = self._client_wrapper.httpx_client.request( + "api/v1/voices/facets", + method="GET", + params={ + "favorites_only": favorites_only, + "source": source, + "gender": gender, + "age": age, + "category": category, + "accent": accent, + "search": search, + "provider": provider, + "model": model, + "language": language, + }, + 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( + ApiResponseVoiceFacetsOut, + parse_obj_as( + type_=ApiResponseVoiceFacetsOut, # 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, voice_id: str, @@ -184,7 +327,13 @@ def get( request_options: typing.Optional[RequestOptions] = None, ) -> HttpResponse[ApiResponseVoiceOut]: """ - Get a voice by ID, scoped to caller workspace + platform voices. + Fetch a single voice by its ID. + + Returns both platform (system-wide) voices and voices that belong to the + caller's workspace. Returns 404 when the voice does not exist or is not + accessible to the caller's workspace. The `sample_url` field is a + time-limited presigned URL valid for 1 hour; regenerate it by calling this + endpoint again rather than caching it long-term. Parameters ---------- @@ -248,13 +397,23 @@ def similar( request_options: typing.Optional[RequestOptions] = None, ) -> HttpResponse[ApiListResponseVoiceSimilarOut]: """ - Return voices nearest to a reference voice embedding. + Return voices acoustically similar to a reference voice. + + Results are ranked by semantic similarity score (descending) and include the + reference voice's workspace voices and all platform voices. Each result + includes a `similarity_score` between 0 and 1. Optionally filter by one or + more `language` BCP-47 codes (repeat the parameter for OR semantics); up to + 16 language values are accepted. Returns 503 when the reference voice has no + embedding yet — retry after the indicated `Retry-After` interval. Prefer this + endpoint over `GET /voices` with manual filtering when building a + "voices like this" recommendation UI. Parameters ---------- voice_id : str limit : typing.Optional[int] + Number of similar voices to return (1–50). language : typing.Optional[typing.Sequence[str]] Repeat for OR, e.g. ?language=en-us&language=ko-kr @@ -319,7 +478,12 @@ def favorite_voice( request_options: typing.Optional[RequestOptions] = None, ) -> HttpResponse[ApiResponseVoiceOut]: """ - Mark a voice as a workspace favorite. + Add a voice to the current workspace's favorites. + + Favorites are workspace-scoped, not per-user: all members of the workspace + see the same favorited set. Idempotent — favoriting a voice that is already + favorited succeeds without error. Returns the voice with `is_favorite=true`. + Requires the caller to have at least editor role in the workspace. Parameters ---------- @@ -381,7 +545,11 @@ def unfavorite_voice( request_options: typing.Optional[RequestOptions] = None, ) -> HttpResponse[ApiResponseDict]: """ - Remove a voice from workspace favorites. + Remove a voice from the current workspace's favorites. + + Idempotent — removing a voice that is not currently favorited succeeds + without error. Requires the caller to have at least editor role in the + workspace. Parameters ---------- @@ -467,11 +635,11 @@ async def list( `?gender=female&gender=neutral&category=narration&source=platform&source=workspace`. Filters combine across fields with AND; within a field, values OR. - `language` uses Postgres `?|` (exists-any) against `voices.supported_languages`. - Platform voices with NULL `supported_languages` (catalog gaps) are treated - as general-use and match every locale filter. User-uploaded / cloned voices - with NULL stay excluded — NULL there means "language unknown" pending the - clone flow's language detection. + `language` matches a voice when any of its declared locales matches any + requested value. Platform voices with no declared locales (catalog gaps) + are treated as general-use and match every language filter. User-uploaded + / cloned voices with no declared locales are excluded — that state means + "language unknown" pending the clone flow's language detection. Multi-sort: `sort` and `order` are parallel lists. `?sort=uses_count&sort=name&order=desc&order=asc` orders primarily by uses_count DESC, secondarily by name ASC. When `order` @@ -484,13 +652,16 @@ async def list( Parameters ---------- offset : typing.Optional[int] + Number of results to skip for pagination. limit : typing.Optional[int] + Maximum number of results to return (1–100). favorites_only : typing.Optional[bool] + When true, return only voices in the workspace's favorites list. source : typing.Optional[typing.Sequence[ListVoicesRequestSourceItem]] - Repeat for OR across scopes + Repeat for OR across scopes: `platform` for system-provided voices, `workspace` for workspace-owned voices. gender : typing.Optional[typing.Sequence[VoiceGender]] Repeat for OR @@ -505,6 +676,7 @@ async def list( Repeat for OR search : typing.Optional[str] + Full-text search against voice name, description, and tags. sort : typing.Optional[typing.Sequence[ListVoicesRequestSortItem]] Repeat for multi-sort. Pairs with `order` index-wise. @@ -585,6 +757,141 @@ async def list( ) raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + async def get_voice_facets( + self, + *, + favorites_only: typing.Optional[bool] = None, + source: typing.Optional[typing.Sequence[GetVoiceFacetsApiV1VoicesFacetsGetRequestSourceItem]] = None, + gender: typing.Optional[typing.Sequence[VoiceGender]] = None, + age: typing.Optional[typing.Sequence[VoiceAge]] = None, + category: typing.Optional[typing.Sequence[VoiceCategory]] = None, + accent: typing.Optional[typing.Sequence[VoiceAccent]] = None, + search: typing.Optional[str] = None, + provider: typing.Optional[typing.Sequence[str]] = None, + model: typing.Optional[typing.Sequence[str]] = None, + language: typing.Optional[typing.Sequence[str]] = None, + workspace_id: typing.Optional[str] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[ApiResponseVoiceFacetsOut]: + """ + Filter-bar options (chips) for the voice browser, one list per dimension. + + Returns `providers`, `models`, `languages` (data-driven) plus `genders`, + `ages`, `categories`, `accents` (fixed enums) as `VoiceFacetItem[]` so the FE + builds the whole filter bar — with per-chip count badges — in a single request + instead of hardcoding option lists (mirrors `GET /dictionary/languages`). Each + item is `{value, label, count}`: `value` is passed straight back to + `GET /voices`; `label` is the display name for providers/models and `null` + elsewhere (the FE owns language + enum labels); `count` is the number of + matching voices. For `languages`/`models`, `count` counts only voices that + explicitly declare the value — "general-use" platform voices (no declared + locales/models) that `GET /voices` matches against every language/model filter + are not counted, so a chip's count can be lower than the `GET /voices` result. + + Accepts the SAME filters as `GET /voices` (tab scope `source`/`favorites_only`, + plus `provider`/`model`/`language`/`gender`/`age`/`category`/`accent`/`search`). + `count` is context-aware (faceted search): each dimension's counts apply every + OTHER active filter but exclude that dimension's own selection — e.g. with + `provider=elevenlabs` the language counts are scoped to ElevenLabs, while the + provider chips still show every provider so the caller can switch. + + Count-0 policy: data-driven dimensions omit count-0 values (only present ones, + each a valid `GET /voices` filter — providers/models restricted to the enabled + catalog, languages to the supported-locale allowlist, so a chip never 422s). + Enum dimensions always return the full enum in natural order, count-0 included, + for the FE to grey out. + + Parameters + ---------- + favorites_only : typing.Optional[bool] + Favorites tab scope + + source : typing.Optional[typing.Sequence[GetVoiceFacetsApiV1VoicesFacetsGetRequestSourceItem]] + Tab scope — repeat for OR, same values as GET /voices (e.g. platform, workspace) + + gender : typing.Optional[typing.Sequence[VoiceGender]] + Repeat for OR + + age : typing.Optional[typing.Sequence[VoiceAge]] + Repeat for OR + + category : typing.Optional[typing.Sequence[VoiceCategory]] + Repeat for OR + + accent : typing.Optional[typing.Sequence[VoiceAccent]] + Repeat for OR + + search : typing.Optional[str] + + provider : typing.Optional[typing.Sequence[str]] + Repeat for OR, e.g. ?provider=elevenlabs&provider=rime + + model : typing.Optional[typing.Sequence[str]] + Repeat for OR. Filters platform voices by TTS model, e.g. ?model=arcana&model=sonic-2 + + language : typing.Optional[typing.Sequence[str]] + Repeat for OR, e.g. ?language=en-us&language=ko-kr + + workspace_id : typing.Optional[str] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[ApiResponseVoiceFacetsOut] + Successful Response + """ + _response = await self._client_wrapper.httpx_client.request( + "api/v1/voices/facets", + method="GET", + params={ + "favorites_only": favorites_only, + "source": source, + "gender": gender, + "age": age, + "category": category, + "accent": accent, + "search": search, + "provider": provider, + "model": model, + "language": language, + }, + 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( + ApiResponseVoiceFacetsOut, + parse_obj_as( + type_=ApiResponseVoiceFacetsOut, # 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, voice_id: str, @@ -593,7 +900,13 @@ async def get( request_options: typing.Optional[RequestOptions] = None, ) -> AsyncHttpResponse[ApiResponseVoiceOut]: """ - Get a voice by ID, scoped to caller workspace + platform voices. + Fetch a single voice by its ID. + + Returns both platform (system-wide) voices and voices that belong to the + caller's workspace. Returns 404 when the voice does not exist or is not + accessible to the caller's workspace. The `sample_url` field is a + time-limited presigned URL valid for 1 hour; regenerate it by calling this + endpoint again rather than caching it long-term. Parameters ---------- @@ -657,13 +970,23 @@ async def similar( request_options: typing.Optional[RequestOptions] = None, ) -> AsyncHttpResponse[ApiListResponseVoiceSimilarOut]: """ - Return voices nearest to a reference voice embedding. + Return voices acoustically similar to a reference voice. + + Results are ranked by semantic similarity score (descending) and include the + reference voice's workspace voices and all platform voices. Each result + includes a `similarity_score` between 0 and 1. Optionally filter by one or + more `language` BCP-47 codes (repeat the parameter for OR semantics); up to + 16 language values are accepted. Returns 503 when the reference voice has no + embedding yet — retry after the indicated `Retry-After` interval. Prefer this + endpoint over `GET /voices` with manual filtering when building a + "voices like this" recommendation UI. Parameters ---------- voice_id : str limit : typing.Optional[int] + Number of similar voices to return (1–50). language : typing.Optional[typing.Sequence[str]] Repeat for OR, e.g. ?language=en-us&language=ko-kr @@ -728,7 +1051,12 @@ async def favorite_voice( request_options: typing.Optional[RequestOptions] = None, ) -> AsyncHttpResponse[ApiResponseVoiceOut]: """ - Mark a voice as a workspace favorite. + Add a voice to the current workspace's favorites. + + Favorites are workspace-scoped, not per-user: all members of the workspace + see the same favorited set. Idempotent — favoriting a voice that is already + favorited succeeds without error. Returns the voice with `is_favorite=true`. + Requires the caller to have at least editor role in the workspace. Parameters ---------- @@ -790,7 +1118,11 @@ async def unfavorite_voice( request_options: typing.Optional[RequestOptions] = None, ) -> AsyncHttpResponse[ApiResponseDict]: """ - Remove a voice from workspace favorites. + Remove a voice from the current workspace's favorites. + + Idempotent — removing a voice that is not currently favorited succeeds + without error. Requires the caller to have at least editor role in the + workspace. Parameters ---------- diff --git a/src/onepin/voices/types/__init__.py b/src/onepin/voices/types/__init__.py index 7143e36..7582c29 100644 --- a/src/onepin/voices/types/__init__.py +++ b/src/onepin/voices/types/__init__.py @@ -6,12 +6,16 @@ from importlib import import_module if typing.TYPE_CHECKING: + from .get_voice_facets_api_v1voices_facets_get_request_source_item import ( + GetVoiceFacetsApiV1VoicesFacetsGetRequestSourceItem, + ) 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", @@ -42,6 +46,7 @@ def __dir__(): __all__ = [ + "GetVoiceFacetsApiV1VoicesFacetsGetRequestSourceItem", "ListVoicesRequestLanguageItem", "ListVoicesRequestOrderItem", "ListVoicesRequestProviderItem", diff --git a/src/onepin/voices/types/get_voice_facets_api_v1voices_facets_get_request_source_item.py b/src/onepin/voices/types/get_voice_facets_api_v1voices_facets_get_request_source_item.py new file mode 100644 index 0000000..fbd3cfe --- /dev/null +++ b/src/onepin/voices/types/get_voice_facets_api_v1voices_facets_get_request_source_item.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +GetVoiceFacetsApiV1VoicesFacetsGetRequestSourceItem = typing.Union[ + typing.Literal["platform", "recorded", "uploaded", "workspace"], typing.Any +] diff --git a/src/onepin/webhooks/__init__.py b/src/onepin/webhooks/__init__.py deleted file mode 100644 index 5cde020..0000000 --- a/src/onepin/webhooks/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -# isort: skip_file - diff --git a/src/onepin/webhooks/client.py b/src/onepin/webhooks/client.py deleted file mode 100644 index 48f8550..0000000 --- a/src/onepin/webhooks/client.py +++ /dev/null @@ -1,160 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper -from ..core.request_options import RequestOptions -from ..types.api_response_dict import ApiResponseDict -from .raw_client import AsyncRawWebhooksClient, RawWebhooksClient - - -class WebhooksClient: - def __init__(self, *, client_wrapper: SyncClientWrapper): - self._raw_client = RawWebhooksClient(client_wrapper=client_wrapper) - - @property - def with_raw_response(self) -> RawWebhooksClient: - """ - Retrieves a raw implementation of this client that returns raw responses. - - Returns - ------- - RawWebhooksClient - """ - return self._raw_client - - def clerk_webhook(self, *, request_options: typing.Optional[RequestOptions] = None) -> ApiResponseDict: - """ - Handle Clerk webhook events. Verifies svix signature. - - Parameters - ---------- - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ApiResponseDict - Successful Response - - Examples - -------- - from onepin import OnePinClient - - client = OnePinClient( - token="YOUR_TOKEN", - ) - client.webhooks.clerk_webhook() - """ - _response = self._raw_client.clerk_webhook(request_options=request_options) - return _response.data - - def stripe_webhook(self, *, request_options: typing.Optional[RequestOptions] = None) -> ApiResponseDict: - """ - Handle Stripe webhook events. Verifies Stripe-Signature header. - - Parameters - ---------- - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ApiResponseDict - Successful Response - - Examples - -------- - from onepin import OnePinClient - - client = OnePinClient( - token="YOUR_TOKEN", - ) - client.webhooks.stripe_webhook() - """ - _response = self._raw_client.stripe_webhook(request_options=request_options) - return _response.data - - -class AsyncWebhooksClient: - def __init__(self, *, client_wrapper: AsyncClientWrapper): - self._raw_client = AsyncRawWebhooksClient(client_wrapper=client_wrapper) - - @property - def with_raw_response(self) -> AsyncRawWebhooksClient: - """ - Retrieves a raw implementation of this client that returns raw responses. - - Returns - ------- - AsyncRawWebhooksClient - """ - return self._raw_client - - async def clerk_webhook(self, *, request_options: typing.Optional[RequestOptions] = None) -> ApiResponseDict: - """ - Handle Clerk webhook events. Verifies svix signature. - - Parameters - ---------- - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ApiResponseDict - Successful Response - - Examples - -------- - import asyncio - - from onepin import AsyncOnePinClient - - client = AsyncOnePinClient( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.webhooks.clerk_webhook() - - - asyncio.run(main()) - """ - _response = await self._raw_client.clerk_webhook(request_options=request_options) - return _response.data - - async def stripe_webhook(self, *, request_options: typing.Optional[RequestOptions] = None) -> ApiResponseDict: - """ - Handle Stripe webhook events. Verifies Stripe-Signature header. - - Parameters - ---------- - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ApiResponseDict - Successful Response - - Examples - -------- - import asyncio - - from onepin import AsyncOnePinClient - - client = AsyncOnePinClient( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.webhooks.stripe_webhook() - - - asyncio.run(main()) - """ - _response = await self._raw_client.stripe_webhook(request_options=request_options) - return _response.data diff --git a/src/onepin/webhooks/raw_client.py b/src/onepin/webhooks/raw_client.py deleted file mode 100644 index ce24511..0000000 --- a/src/onepin/webhooks/raw_client.py +++ /dev/null @@ -1,183 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing -from json.decoder import JSONDecodeError - -from ..core.api_error import ApiError -from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper -from ..core.http_response import AsyncHttpResponse, HttpResponse -from ..core.parse_error import ParsingError -from ..core.pydantic_utilities import parse_obj_as -from ..core.request_options import RequestOptions -from ..types.api_response_dict import ApiResponseDict -from pydantic import ValidationError - - -class RawWebhooksClient: - def __init__(self, *, client_wrapper: SyncClientWrapper): - self._client_wrapper = client_wrapper - - def clerk_webhook( - self, *, request_options: typing.Optional[RequestOptions] = None - ) -> HttpResponse[ApiResponseDict]: - """ - Handle Clerk webhook events. Verifies svix signature. - - Parameters - ---------- - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - HttpResponse[ApiResponseDict] - Successful Response - """ - _response = self._client_wrapper.httpx_client.request( - "webhooks/clerk", - method="POST", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - ApiResponseDict, - parse_obj_as( - type_=ApiResponseDict, # type: ignore - object_=_response.json(), - ), - ) - return HttpResponse(response=_response, data=_data) - _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 stripe_webhook( - self, *, request_options: typing.Optional[RequestOptions] = None - ) -> HttpResponse[ApiResponseDict]: - """ - Handle Stripe webhook events. Verifies Stripe-Signature header. - - Parameters - ---------- - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - HttpResponse[ApiResponseDict] - Successful Response - """ - _response = self._client_wrapper.httpx_client.request( - "webhooks/stripe", - method="POST", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - ApiResponseDict, - parse_obj_as( - type_=ApiResponseDict, # type: ignore - object_=_response.json(), - ), - ) - return HttpResponse(response=_response, data=_data) - _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) - - -class AsyncRawWebhooksClient: - def __init__(self, *, client_wrapper: AsyncClientWrapper): - self._client_wrapper = client_wrapper - - async def clerk_webhook( - self, *, request_options: typing.Optional[RequestOptions] = None - ) -> AsyncHttpResponse[ApiResponseDict]: - """ - Handle Clerk webhook events. Verifies svix signature. - - Parameters - ---------- - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AsyncHttpResponse[ApiResponseDict] - Successful Response - """ - _response = await self._client_wrapper.httpx_client.request( - "webhooks/clerk", - method="POST", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - ApiResponseDict, - parse_obj_as( - type_=ApiResponseDict, # type: ignore - object_=_response.json(), - ), - ) - return AsyncHttpResponse(response=_response, data=_data) - _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 stripe_webhook( - self, *, request_options: typing.Optional[RequestOptions] = None - ) -> AsyncHttpResponse[ApiResponseDict]: - """ - Handle Stripe webhook events. Verifies Stripe-Signature header. - - Parameters - ---------- - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AsyncHttpResponse[ApiResponseDict] - Successful Response - """ - _response = await self._client_wrapper.httpx_client.request( - "webhooks/stripe", - method="POST", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - ApiResponseDict, - parse_obj_as( - type_=ApiResponseDict, # type: ignore - object_=_response.json(), - ), - ) - return AsyncHttpResponse(response=_response, data=_data) - _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) diff --git a/src/onepin/workflows/client.py b/src/onepin/workflows/client.py index 174d465..c941e78 100644 --- a/src/onepin/workflows/client.py +++ b/src/onepin/workflows/client.py @@ -56,6 +56,7 @@ def list( order: typing.Optional[typing.Sequence[ListWorkflowsRequestOrderItem]] = None, last_run_after: typing.Optional[dt.datetime] = None, last_run_before: typing.Optional[dt.datetime] = None, + has_failed_run: typing.Optional[bool] = None, offset: typing.Optional[int] = None, limit: typing.Optional[int] = None, workspace_id: typing.Optional[str] = None, @@ -64,23 +65,42 @@ def list( """ List workflows in the current workspace. - Multi-sort: `sort` and `order` are parallel lists. + 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. + + **Status filter:** `status` narrows by the UI-derived state of the workflow's + most recent run. `completed` matches only workflows whose latest run succeeded + (completed-only), and `failed` matches only workflows whose latest run failed — + the two buckets are disjoint. A workflow whose latest run was `cancelled` matches + neither bucket and surfaces only in the unfiltered list. `running` matches active + (running or paused) workflows. `draft` matches workflows with no runs yet. + `paused` is accepted but currently returns no results. + + **Multi-sort:** `sort` and `order` are parallel query lists. `?sort=runs_count&sort=name&order=desc&order=asc` orders primarily by - runs_count DESC, secondarily by name ASC. When `order` is shorter than - `sort`, missing entries default per-field: - `name=asc, updated_at=desc, runs_count=desc`. When `sort` is omitted, - list defaults to `updated_at DESC`. Every sort path appends - `Workflow.id ASC` as a deterministic tiebreaker for pagination stability. + `runs_count DESC`, then by `name ASC`. When `order` has fewer entries than + `sort`, missing positions use per-field defaults (`name=asc`, + `updated_at=desc`, `runs_count=desc`). Omitting `sort` defaults to + `updated_at DESC`. A stable `id ASC` tiebreaker is always appended so + offset/limit pagination is consistent when sort keys tie. + + **Date range:** `last_run_after` / `last_run_before` filter by the time of + the most recent run. Both must be ISO 8601 with a UTC offset; a naive + datetime returns 422. An inverted range (`after > before`) also returns 422. - `paused` is accepted but currently returns an empty result because - backend pause state is not implemented. `last_run_status` is the raw - RunStatus value, including values like `pending` or `cancelled`, and - pagination `total` is the filtered total for the current query. + **Failure history:** `has_failed_run` is orthogonal to `status`. `status` + keys off the latest run only; `has_failed_run=true` matches workflows with a + `failed` run *anywhere* in their history, so a workflow whose latest run + completed still matches if an earlier run failed. It composes with the other + filters (AND). `cancelled` runs do not count as failures. + + `pagination.total` reflects the filtered count for the current query. Parameters ---------- status : typing.Optional[WorkflowListStatus] - UI workflow status filter. `completed` means FINISHED — it matches workflows whose latest run is completed, failed, or cancelled, so it overlaps `failed` on failed runs. `paused` is accepted for forward compatibility and currently returns no rows. + UI workflow status filter. `completed` matches workflows whose latest run succeeded (completed-only); `failed` matches failed-only — the two are disjoint. A workflow whose latest run was cancelled matches neither and appears only in the unfiltered list. `paused` is accepted for forward compatibility and currently returns no rows. search : typing.Optional[str] Case-insensitive search over name and description. @@ -92,14 +112,19 @@ def list( Parallel to sort[]; shorter is padded with per-field defaults. last_run_after : typing.Optional[dt.datetime] - Filter workflows whose last_run_at is at or after this ISO datetime. + Filter workflows whose last_run_at is at or after this ISO datetime (ISO 8601 with UTC offset required). last_run_before : typing.Optional[dt.datetime] - Filter workflows whose last_run_at is at or before this ISO datetime. + Filter workflows whose last_run_at is at or before this ISO datetime (ISO 8601 with UTC offset required). + + has_failed_run : typing.Optional[bool] + Filter by failure history — ORTHOGONAL to `status` (which is latest-run based). `true` returns only workflows with at least one run that ended in `failed` state anywhere in their history; a workflow whose latest run succeeded still matches if an earlier run failed. `false` returns only workflows that have never had a failed run. Composes (ANDs) with `status`/`search`/date filters. `cancelled` runs are not treated as failures. offset : typing.Optional[int] + Zero-based pagination offset. limit : typing.Optional[int] + Maximum items to return (1–100). workspace_id : typing.Optional[str] @@ -127,6 +152,7 @@ def list( order=order, last_run_after=last_run_after, last_run_before=last_run_before, + has_failed_run=has_failed_run, offset=offset, limit=limit, workspace_id=workspace_id, @@ -144,17 +170,29 @@ def create_workflow( request_options: typing.Optional[RequestOptions] = None, ) -> ApiResponseWorkflowOut: """ - Create a workflow. + Create a new workflow in the current workspace. + + Validates the workflow `definition` (graph structure, node types, edge + connectivity) before persisting. Returns 422 with structured details if + the definition fails validation. Requires at least `editor` role in the + workspace; viewers cannot create workflows. + + The `definition` contains a `graph` (nodes and edges) and an `execution` + block (ordered step list and execution params). Omitting `definition` + creates a workflow with an empty graph that can be edited later. Parameters ---------- name : str + Human-readable workflow name (1–200 characters, non-blank). workspace_id : typing.Optional[str] description : typing.Optional[str] + Optional description shown in the workflow list (max 5000 characters). definition : typing.Optional[WorkflowDefinitionInput] + Graph and execution config. Omit to create an empty workflow. request_options : typing.Optional[RequestOptions] Request-specific configuration. @@ -192,7 +230,16 @@ def get( request_options: typing.Optional[RequestOptions] = None, ) -> ApiResponseWorkflowOut: """ - Fetch a workflow by id. + Fetch a single workflow by ID. + + Returns the full workflow including its `definition` (graph nodes/edges and + execution config), aggregate run stats, and the latest run status. The + `definition` is returned with any backwards-compatible config migrations + applied, so node configs always reflect the current schema even if the + workflow was saved with an older version. + + Use `GET /workflows` to list multiple workflows without fetching their + full definitions. Parameters ---------- @@ -233,19 +280,28 @@ def update_workflow( request_options: typing.Optional[RequestOptions] = None, ) -> ApiResponseWorkflowOut: """ - Update a workflow. + Replace a workflow's name, description, and definition (full update). + + All fields in the request body are required. The `definition` is + validated before persisting; an invalid graph returns 422. Existing runs + are not affected — each run captures a `definition_snapshot` at start + time. Requires at least `editor` role. Use `PATCH` to update only + specific fields without supplying the full definition. Parameters ---------- workflow_id : str name : str + Human-readable workflow name (1–200 characters, non-blank). definition : WorkflowDefinitionInput + Full replacement graph and execution config. Must be valid. workspace_id : typing.Optional[str] description : typing.Optional[str] + Optional description (max 5000 characters). Pass null to clear. request_options : typing.Optional[RequestOptions] Request-specific configuration. @@ -286,7 +342,12 @@ def delete_workflow( request_options: typing.Optional[RequestOptions] = None, ) -> ApiResponseDict: """ - Soft-delete a workflow. + Delete a workflow and hide it from all list and get endpoints. + + The workflow is soft-deleted: its data (including runs and their outputs) + is retained for audit and GDPR-purge purposes but is no longer accessible + via the API. Subsequent `GET`, `PUT`, `PATCH`, or run requests on the + same ID return 404. Requires at least `editor` role. Parameters ---------- @@ -329,7 +390,14 @@ def patch_workflow( request_options: typing.Optional[RequestOptions] = None, ) -> ApiResponseWorkflowOut: """ - Partially update a workflow. Only fields present in the body are applied. + Partially update a workflow — only supplied fields are changed. + + Any combination of `name`, `description`, and `definition` may be + included; omitted fields are left unchanged. At least one field must be + present (empty body returns 422). If `definition` is provided it is fully + validated; an invalid graph returns 422. Requires at least `editor` role. + + Use `PUT` when replacing the full workflow definition in one operation. Parameters ---------- @@ -338,10 +406,13 @@ def patch_workflow( workspace_id : typing.Optional[str] name : typing.Optional[str] + New workflow name (1–200 characters). Omit to leave unchanged. description : typing.Optional[str] + New description (max 5000 characters). Omit to leave unchanged; pass null to clear. definition : typing.Optional[WorkflowDefinitionInput] + Replacement graph and execution config. Omit to leave unchanged; must be valid if supplied. request_options : typing.Optional[RequestOptions] Request-specific configuration. @@ -384,13 +455,19 @@ def list_workflow_uploads( """ List confirmed uploads attached to a workflow. + Returns only uploads that have been confirmed (fully transferred and + committed to the workflow). In-progress or abandoned uploads are excluded. + Each item includes a short-lived download URL for the uploaded file. + Parameters ---------- workflow_id : str offset : typing.Optional[int] + Zero-based pagination offset. limit : typing.Optional[int] + Maximum items to return (1–100). workspace_id : typing.Optional[str] @@ -426,7 +503,12 @@ def estimate_workflow( request_options: typing.Optional[RequestOptions] = None, ) -> ApiResponseEstimateResponse: """ - Estimate workflow credits without creating a run. + Estimate the credit cost of running a workflow without creating a run. + + Computes a breakdown of expected credits per node type based on the + workflow's current definition. No run is created, no credits are charged, + and no side effects occur. Equivalent to `POST /runs/preview`; prefer that + path in new integrations as it is co-located with the run lifecycle. Parameters ---------- @@ -466,7 +548,12 @@ def preview_run( request_options: typing.Optional[RequestOptions] = None, ) -> ApiResponseEstimateResponse: """ - Estimate workflow run credits without creating a run. + Dry-run credit estimate for a workflow — no run is created. + + Returns a per-node-type credit breakdown based on the workflow's current + definition. No run is enqueued, no credits are charged, and the workflow + state is not modified. Use this before calling `POST /runs` to confirm + the expected cost. Equivalent to `POST /estimate`. Parameters ---------- @@ -508,22 +595,32 @@ def runs_summary( request_options: typing.Optional[RequestOptions] = None, ) -> ApiResponseRunsSummaryOut: """ - Aggregate run-status counts plus pass_rate and average_duration_seconds. + Aggregate run statistics for a workflow over an optional date window. + + Returns per-status counts (`completed`, `failed`, `cancelled`, `pending`, + `running`, `paused`) plus two derived metrics: + + - `pass_rate`: `completed / (completed + failed + cancelled)`. `null` when + there are no terminal runs in the window. + - `average_duration_seconds`: mean of `completed_at - started_at` over + successfully completed runs only. `null` when no runs have completed. + + **Date range:** `from` / `to` filter by `created_at`. Both must be ISO 8601 + with a UTC offset; a naive datetime returns 422. An inverted range + (`from > to`) also returns 422. Omit both to aggregate over all runs. - ``pass_rate = completed / (completed + failed + cancelled)``; - ``None`` when no terminal runs. - ``average_duration_seconds = mean(completed_at - started_at)`` over - completed runs only; ``None`` when there are zero completed runs. + Use `GET /runs` with `?status=` filters for individual run details; this + endpoint is best for dashboard-style health metrics. Parameters ---------- workflow_id : str from_ : typing.Optional[dt.datetime] - Filter runs by created_at >= this ISO datetime. + Filter runs by created_at >= this ISO datetime (ISO 8601 with UTC offset required). to : typing.Optional[dt.datetime] - Filter runs by created_at <= this ISO datetime. + Filter runs by created_at <= this ISO datetime (ISO 8601 with UTC offset required). workspace_id : typing.Optional[str] @@ -560,7 +657,23 @@ def get_run_steps( request_options: typing.Optional[RequestOptions] = None, ) -> ApiResponseListWorkflowRunStepOut: """ - List steps for a workflow run. + List per-node execution steps for a workflow run. + + Returns one entry per node execution attempt, ordered by execution sequence. + Each step includes the node type, status, iteration number (for nodes that + are retried), start/completion timestamps, and the node's `result` output. + + For audio output nodes, `result` is hydrated with short-lived `playback_url` + 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. + + For a higher-level view with aggregated metrics (pass rates, audio duration + by language), use `GET /runs/{run_id}/overview`. For paginated, grouped + script+audio rows suitable for a data table, use `GET /runs/{run_id}/data`. Parameters ---------- @@ -606,6 +719,21 @@ def get_run_overview( """ Fetch server-computed overview aggregates for a workflow run. + Returns structured metric sections (e.g. audio duration totals, validation + 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. + + This endpoint is best suited for a summary/results view after a run + completes. It differs from the other run sub-resources as follows: + + - `GET /runs/{run_id}` — full run record including the raw definition snapshot. + - `GET /runs/{run_id}/status` — volatile status fields only; for polling. + - `GET /runs/{run_id}/steps` — flat per-node step log with audio playback URLs. + - `GET /runs/{run_id}/data` — paginated script+audio rows for a data table. + - `GET /runs/{run_id}/overview` (this endpoint) — pre-aggregated metrics and + node state map for a dashboard/overview panel. + Parameters ---------- workflow_id : str @@ -652,10 +780,26 @@ def get_run_data( request_options: typing.Optional[RequestOptions] = None, ) -> WorkflowRunDataResponse: """ - Fetch normalized grouped rows/cards for the run detail Data tab. + Paginated script-and-audio data rows for a completed workflow run. + + Returns grouped rows where each row represents one source script line. + Within each row, `cards` contain the per-language audio outputs, per-card + validation scores (word accuracy, naturalness), and short-lived audio + `playback_url` values (valid for 15 minutes). + + **Filtering:** + - `search` narrows which rows are returned based on their source script text. + - `language` narrows the `cards` list within each returned row to a single + locale. Rows with no matching cards are still returned (with empty `cards`), + and `pagination.total` always reflects the search-filtered row count + regardless of `language`. + + **Pagination:** `pagination.total` is scoped to the `search` filter only. - `pagination.total` is search-scoped and language-independent; language - filters only card lists, so returned rows may contain empty `cards`. + Response includes a `partial` field indicating whether any data is still + being computed (e.g. audio not yet generated, validation not yet scored). + This endpoint sets `Cache-Control: no-store` because playback URLs are + short-lived and data may change while a run is still in progress. Parameters ---------- @@ -664,14 +808,16 @@ def get_run_data( run_id : str search : typing.Optional[str] - Case-insensitive search over visible grouped source/script text. + Case-insensitive search over the source/script text of each row. language : typing.Optional[str] - Exact full-locale card filter. `_` is normalized to `-`; filtering cards preserves row visibility and pagination.total remains language-independent, so rows may return empty cards. + Exact full-locale code to filter cards within each row (e.g. `en-US`). `_` is normalized to `-`. Filtering is card-level only — rows remain visible even when all their cards are filtered out, and `pagination.total` is unaffected. offset : typing.Optional[int] + Zero-based pagination offset. limit : typing.Optional[int] + Maximum rows to return (1–100). workspace_id : typing.Optional[str] @@ -716,7 +862,20 @@ def download_run( request_options: typing.Optional[RequestOptions] = None, ) -> ApiResponseDownloadUrlOut: """ - Create a temporary download URL for a workflow run export. + Create a temporary download URL for a complete workflow run export. + + Returns a pre-signed URL pointing to a ZIP archive containing all audio + output files produced by the run. The URL is valid for 15 minutes + (`expires_at`). The archive is generated on first request and cached for + subsequent calls; re-calling this endpoint before expiry returns a new + URL for the same cached archive. + + Only available for runs in `completed` status — returns 409 for runs that + are still active or ended in `failed`/`cancelled`. Returns 404 if the run + produced no audio files. + + To download output from a single output node rather than the whole run, + use `GET /runs/{run_id}/nodes/{node_id}/download`. Parameters ---------- @@ -761,7 +920,20 @@ def download_run_node( request_options: typing.Optional[RequestOptions] = None, ) -> ApiResponseDownloadUrlOut: """ - Create a temporary download URL for a node-level workflow export. + Create a temporary download URL for a single output node's audio export. + + Returns a pre-signed URL for a ZIP archive containing the audio files + produced by one specific output node within the run. Useful when a + workflow has multiple output nodes and the caller wants only one node's + 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 + 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 + archive, use `GET /runs/{run_id}/download` instead. Parameters ---------- @@ -808,10 +980,17 @@ def pause_run( request_options: typing.Optional[RequestOptions] = None, ) -> ApiResponseWorkflowRunOut: """ - Pause a workflow run. + Pause an active workflow run at the next safe checkpoint. + + For a running run, the current wave of parallel nodes is allowed to finish + before the run parks (in-flight work is preserved, not abandoned). For a + pending run that has not yet started, it parks immediately. The run + transitions to `paused` status once drained; during the drain period, + `status` remains `running` with `pause_requested_at` set. - A running run finishes its current wave (preserving in-flight work) and parks at the - next wave boundary; a pending run parks immediately. Fire-and-forget and idempotent. + The operation is idempotent: pausing an already-paused run returns it + unchanged. A paused run can be resumed via `POST /runs/{run_id}/resume` + or permanently stopped via `POST /runs/{run_id}/cancel`. Parameters ---------- @@ -857,8 +1036,15 @@ def resume_run( """ Resume a paused workflow run from its last completed wave. - Best-effort: if the workflow already has another active run, or the caller is at their - concurrent-run limit, the run stays paused and a 409 explains why (retry later). + Transitions the run from `paused` back to `running` and schedules + execution to continue from where it left off — no nodes that already + completed are re-executed. + + Returns 409 if the workspace already has another active run for this + workflow, or if the caller is at the concurrent-run limit. In that case + the run stays `paused` and the caller can retry later. Only runs in + `paused` status can be resumed; attempting to resume a `running`, + `completed`, `failed`, or `cancelled` run returns 409. Parameters ---------- @@ -901,7 +1087,12 @@ def duplicate_workflow( request_options: typing.Optional[RequestOptions] = None, ) -> ApiResponseWorkflowOut: """ - Duplicate a workflow. + Create a copy of an existing workflow in the same workspace. + + The new workflow inherits the source's `name` (suffixed with " (Copy)"), + `description`, and `definition`. Runs from the original workflow are not + copied — the duplicate starts with zero runs. Requires at least `editor` + role. Returns 201 with the new workflow on success. Parameters ---------- @@ -968,6 +1159,7 @@ async def list( order: typing.Optional[typing.Sequence[ListWorkflowsRequestOrderItem]] = None, last_run_after: typing.Optional[dt.datetime] = None, last_run_before: typing.Optional[dt.datetime] = None, + has_failed_run: typing.Optional[bool] = None, offset: typing.Optional[int] = None, limit: typing.Optional[int] = None, workspace_id: typing.Optional[str] = None, @@ -976,23 +1168,42 @@ async def list( """ List workflows in the current workspace. - Multi-sort: `sort` and `order` are parallel lists. + 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. + + **Status filter:** `status` narrows by the UI-derived state of the workflow's + most recent run. `completed` matches only workflows whose latest run succeeded + (completed-only), and `failed` matches only workflows whose latest run failed — + the two buckets are disjoint. A workflow whose latest run was `cancelled` matches + neither bucket and surfaces only in the unfiltered list. `running` matches active + (running or paused) workflows. `draft` matches workflows with no runs yet. + `paused` is accepted but currently returns no results. + + **Multi-sort:** `sort` and `order` are parallel query lists. `?sort=runs_count&sort=name&order=desc&order=asc` orders primarily by - runs_count DESC, secondarily by name ASC. When `order` is shorter than - `sort`, missing entries default per-field: - `name=asc, updated_at=desc, runs_count=desc`. When `sort` is omitted, - list defaults to `updated_at DESC`. Every sort path appends - `Workflow.id ASC` as a deterministic tiebreaker for pagination stability. + `runs_count DESC`, then by `name ASC`. When `order` has fewer entries than + `sort`, missing positions use per-field defaults (`name=asc`, + `updated_at=desc`, `runs_count=desc`). Omitting `sort` defaults to + `updated_at DESC`. A stable `id ASC` tiebreaker is always appended so + offset/limit pagination is consistent when sort keys tie. + + **Date range:** `last_run_after` / `last_run_before` filter by the time of + the most recent run. Both must be ISO 8601 with a UTC offset; a naive + datetime returns 422. An inverted range (`after > before`) also returns 422. - `paused` is accepted but currently returns an empty result because - backend pause state is not implemented. `last_run_status` is the raw - RunStatus value, including values like `pending` or `cancelled`, and - pagination `total` is the filtered total for the current query. + **Failure history:** `has_failed_run` is orthogonal to `status`. `status` + keys off the latest run only; `has_failed_run=true` matches workflows with a + `failed` run *anywhere* in their history, so a workflow whose latest run + completed still matches if an earlier run failed. It composes with the other + filters (AND). `cancelled` runs do not count as failures. + + `pagination.total` reflects the filtered count for the current query. Parameters ---------- status : typing.Optional[WorkflowListStatus] - UI workflow status filter. `completed` means FINISHED — it matches workflows whose latest run is completed, failed, or cancelled, so it overlaps `failed` on failed runs. `paused` is accepted for forward compatibility and currently returns no rows. + UI workflow status filter. `completed` matches workflows whose latest run succeeded (completed-only); `failed` matches failed-only — the two are disjoint. A workflow whose latest run was cancelled matches neither and appears only in the unfiltered list. `paused` is accepted for forward compatibility and currently returns no rows. search : typing.Optional[str] Case-insensitive search over name and description. @@ -1004,14 +1215,19 @@ async def list( Parallel to sort[]; shorter is padded with per-field defaults. last_run_after : typing.Optional[dt.datetime] - Filter workflows whose last_run_at is at or after this ISO datetime. + Filter workflows whose last_run_at is at or after this ISO datetime (ISO 8601 with UTC offset required). last_run_before : typing.Optional[dt.datetime] - Filter workflows whose last_run_at is at or before this ISO datetime. + Filter workflows whose last_run_at is at or before this ISO datetime (ISO 8601 with UTC offset required). + + has_failed_run : typing.Optional[bool] + Filter by failure history — ORTHOGONAL to `status` (which is latest-run based). `true` returns only workflows with at least one run that ended in `failed` state anywhere in their history; a workflow whose latest run succeeded still matches if an earlier run failed. `false` returns only workflows that have never had a failed run. Composes (ANDs) with `status`/`search`/date filters. `cancelled` runs are not treated as failures. offset : typing.Optional[int] + Zero-based pagination offset. limit : typing.Optional[int] + Maximum items to return (1–100). workspace_id : typing.Optional[str] @@ -1047,6 +1263,7 @@ async def main() -> None: order=order, last_run_after=last_run_after, last_run_before=last_run_before, + has_failed_run=has_failed_run, offset=offset, limit=limit, workspace_id=workspace_id, @@ -1064,17 +1281,29 @@ async def create_workflow( request_options: typing.Optional[RequestOptions] = None, ) -> ApiResponseWorkflowOut: """ - Create a workflow. + Create a new workflow in the current workspace. + + Validates the workflow `definition` (graph structure, node types, edge + connectivity) before persisting. Returns 422 with structured details if + the definition fails validation. Requires at least `editor` role in the + workspace; viewers cannot create workflows. + + The `definition` contains a `graph` (nodes and edges) and an `execution` + block (ordered step list and execution params). Omitting `definition` + creates a workflow with an empty graph that can be edited later. Parameters ---------- name : str + Human-readable workflow name (1–200 characters, non-blank). workspace_id : typing.Optional[str] description : typing.Optional[str] + Optional description shown in the workflow list (max 5000 characters). definition : typing.Optional[WorkflowDefinitionInput] + Graph and execution config. Omit to create an empty workflow. request_options : typing.Optional[RequestOptions] Request-specific configuration. @@ -1120,7 +1349,16 @@ async def get( request_options: typing.Optional[RequestOptions] = None, ) -> ApiResponseWorkflowOut: """ - Fetch a workflow by id. + Fetch a single workflow by ID. + + Returns the full workflow including its `definition` (graph nodes/edges and + execution config), aggregate run stats, and the latest run status. The + `definition` is returned with any backwards-compatible config migrations + applied, so node configs always reflect the current schema even if the + workflow was saved with an older version. + + Use `GET /workflows` to list multiple workflows without fetching their + full definitions. Parameters ---------- @@ -1169,19 +1407,28 @@ async def update_workflow( request_options: typing.Optional[RequestOptions] = None, ) -> ApiResponseWorkflowOut: """ - Update a workflow. + Replace a workflow's name, description, and definition (full update). + + All fields in the request body are required. The `definition` is + validated before persisting; an invalid graph returns 422. Existing runs + are not affected — each run captures a `definition_snapshot` at start + time. Requires at least `editor` role. Use `PATCH` to update only + specific fields without supplying the full definition. Parameters ---------- workflow_id : str name : str + Human-readable workflow name (1–200 characters, non-blank). definition : WorkflowDefinitionInput + Full replacement graph and execution config. Must be valid. workspace_id : typing.Optional[str] description : typing.Optional[str] + Optional description (max 5000 characters). Pass null to clear. request_options : typing.Optional[RequestOptions] Request-specific configuration. @@ -1230,7 +1477,12 @@ async def delete_workflow( request_options: typing.Optional[RequestOptions] = None, ) -> ApiResponseDict: """ - Soft-delete a workflow. + Delete a workflow and hide it from all list and get endpoints. + + The workflow is soft-deleted: its data (including runs and their outputs) + is retained for audit and GDPR-purge purposes but is no longer accessible + via the API. Subsequent `GET`, `PUT`, `PATCH`, or run requests on the + same ID return 404. Requires at least `editor` role. Parameters ---------- @@ -1281,7 +1533,14 @@ async def patch_workflow( request_options: typing.Optional[RequestOptions] = None, ) -> ApiResponseWorkflowOut: """ - Partially update a workflow. Only fields present in the body are applied. + Partially update a workflow — only supplied fields are changed. + + Any combination of `name`, `description`, and `definition` may be + included; omitted fields are left unchanged. At least one field must be + present (empty body returns 422). If `definition` is provided it is fully + validated; an invalid graph returns 422. Requires at least `editor` role. + + Use `PUT` when replacing the full workflow definition in one operation. Parameters ---------- @@ -1290,10 +1549,13 @@ async def patch_workflow( workspace_id : typing.Optional[str] name : typing.Optional[str] + New workflow name (1–200 characters). Omit to leave unchanged. description : typing.Optional[str] + New description (max 5000 characters). Omit to leave unchanged; pass null to clear. definition : typing.Optional[WorkflowDefinitionInput] + Replacement graph and execution config. Omit to leave unchanged; must be valid if supplied. request_options : typing.Optional[RequestOptions] Request-specific configuration. @@ -1344,13 +1606,19 @@ async def list_workflow_uploads( """ List confirmed uploads attached to a workflow. + Returns only uploads that have been confirmed (fully transferred and + committed to the workflow). In-progress or abandoned uploads are excluded. + Each item includes a short-lived download URL for the uploaded file. + Parameters ---------- workflow_id : str offset : typing.Optional[int] + Zero-based pagination offset. limit : typing.Optional[int] + Maximum items to return (1–100). workspace_id : typing.Optional[str] @@ -1394,7 +1662,12 @@ async def estimate_workflow( request_options: typing.Optional[RequestOptions] = None, ) -> ApiResponseEstimateResponse: """ - Estimate workflow credits without creating a run. + Estimate the credit cost of running a workflow without creating a run. + + Computes a breakdown of expected credits per node type based on the + workflow's current definition. No run is created, no credits are charged, + and no side effects occur. Equivalent to `POST /runs/preview`; prefer that + path in new integrations as it is co-located with the run lifecycle. Parameters ---------- @@ -1442,7 +1715,12 @@ async def preview_run( request_options: typing.Optional[RequestOptions] = None, ) -> ApiResponseEstimateResponse: """ - Estimate workflow run credits without creating a run. + Dry-run credit estimate for a workflow — no run is created. + + Returns a per-node-type credit breakdown based on the workflow's current + definition. No run is enqueued, no credits are charged, and the workflow + state is not modified. Use this before calling `POST /runs` to confirm + the expected cost. Equivalent to `POST /estimate`. Parameters ---------- @@ -1492,22 +1770,32 @@ async def runs_summary( request_options: typing.Optional[RequestOptions] = None, ) -> ApiResponseRunsSummaryOut: """ - Aggregate run-status counts plus pass_rate and average_duration_seconds. + Aggregate run statistics for a workflow over an optional date window. + + Returns per-status counts (`completed`, `failed`, `cancelled`, `pending`, + `running`, `paused`) plus two derived metrics: + + - `pass_rate`: `completed / (completed + failed + cancelled)`. `null` when + there are no terminal runs in the window. + - `average_duration_seconds`: mean of `completed_at - started_at` over + successfully completed runs only. `null` when no runs have completed. + + **Date range:** `from` / `to` filter by `created_at`. Both must be ISO 8601 + with a UTC offset; a naive datetime returns 422. An inverted range + (`from > to`) also returns 422. Omit both to aggregate over all runs. - ``pass_rate = completed / (completed + failed + cancelled)``; - ``None`` when no terminal runs. - ``average_duration_seconds = mean(completed_at - started_at)`` over - completed runs only; ``None`` when there are zero completed runs. + Use `GET /runs` with `?status=` filters for individual run details; this + endpoint is best for dashboard-style health metrics. Parameters ---------- workflow_id : str from_ : typing.Optional[dt.datetime] - Filter runs by created_at >= this ISO datetime. + Filter runs by created_at >= this ISO datetime (ISO 8601 with UTC offset required). to : typing.Optional[dt.datetime] - Filter runs by created_at <= this ISO datetime. + Filter runs by created_at <= this ISO datetime (ISO 8601 with UTC offset required). workspace_id : typing.Optional[str] @@ -1552,7 +1840,23 @@ async def get_run_steps( request_options: typing.Optional[RequestOptions] = None, ) -> ApiResponseListWorkflowRunStepOut: """ - List steps for a workflow run. + List per-node execution steps for a workflow run. + + Returns one entry per node execution attempt, ordered by execution sequence. + Each step includes the node type, status, iteration number (for nodes that + are retried), start/completion timestamps, and the node's `result` output. + + For audio output nodes, `result` is hydrated with short-lived `playback_url` + 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. + + For a higher-level view with aggregated metrics (pass rates, audio duration + by language), use `GET /runs/{run_id}/overview`. For paginated, grouped + script+audio rows suitable for a data table, use `GET /runs/{run_id}/data`. Parameters ---------- @@ -1606,6 +1910,21 @@ async def get_run_overview( """ Fetch server-computed overview aggregates for a workflow run. + Returns structured metric sections (e.g. audio duration totals, validation + 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. + + This endpoint is best suited for a summary/results view after a run + completes. It differs from the other run sub-resources as follows: + + - `GET /runs/{run_id}` — full run record including the raw definition snapshot. + - `GET /runs/{run_id}/status` — volatile status fields only; for polling. + - `GET /runs/{run_id}/steps` — flat per-node step log with audio playback URLs. + - `GET /runs/{run_id}/data` — paginated script+audio rows for a data table. + - `GET /runs/{run_id}/overview` (this endpoint) — pre-aggregated metrics and + node state map for a dashboard/overview panel. + Parameters ---------- workflow_id : str @@ -1660,10 +1979,26 @@ async def get_run_data( request_options: typing.Optional[RequestOptions] = None, ) -> WorkflowRunDataResponse: """ - Fetch normalized grouped rows/cards for the run detail Data tab. + Paginated script-and-audio data rows for a completed workflow run. + + Returns grouped rows where each row represents one source script line. + Within each row, `cards` contain the per-language audio outputs, per-card + validation scores (word accuracy, naturalness), and short-lived audio + `playback_url` values (valid for 15 minutes). + + **Filtering:** + - `search` narrows which rows are returned based on their source script text. + - `language` narrows the `cards` list within each returned row to a single + locale. Rows with no matching cards are still returned (with empty `cards`), + and `pagination.total` always reflects the search-filtered row count + regardless of `language`. + + **Pagination:** `pagination.total` is scoped to the `search` filter only. - `pagination.total` is search-scoped and language-independent; language - filters only card lists, so returned rows may contain empty `cards`. + Response includes a `partial` field indicating whether any data is still + being computed (e.g. audio not yet generated, validation not yet scored). + This endpoint sets `Cache-Control: no-store` because playback URLs are + short-lived and data may change while a run is still in progress. Parameters ---------- @@ -1672,14 +2007,16 @@ async def get_run_data( run_id : str search : typing.Optional[str] - Case-insensitive search over visible grouped source/script text. + Case-insensitive search over the source/script text of each row. language : typing.Optional[str] - Exact full-locale card filter. `_` is normalized to `-`; filtering cards preserves row visibility and pagination.total remains language-independent, so rows may return empty cards. + Exact full-locale code to filter cards within each row (e.g. `en-US`). `_` is normalized to `-`. Filtering is card-level only — rows remain visible even when all their cards are filtered out, and `pagination.total` is unaffected. offset : typing.Optional[int] + Zero-based pagination offset. limit : typing.Optional[int] + Maximum rows to return (1–100). workspace_id : typing.Optional[str] @@ -1732,7 +2069,20 @@ async def download_run( request_options: typing.Optional[RequestOptions] = None, ) -> ApiResponseDownloadUrlOut: """ - Create a temporary download URL for a workflow run export. + Create a temporary download URL for a complete workflow run export. + + Returns a pre-signed URL pointing to a ZIP archive containing all audio + output files produced by the run. The URL is valid for 15 minutes + (`expires_at`). The archive is generated on first request and cached for + subsequent calls; re-calling this endpoint before expiry returns a new + URL for the same cached archive. + + Only available for runs in `completed` status — returns 409 for runs that + are still active or ended in `failed`/`cancelled`. Returns 404 if the run + produced no audio files. + + To download output from a single output node rather than the whole run, + use `GET /runs/{run_id}/nodes/{node_id}/download`. Parameters ---------- @@ -1785,7 +2135,20 @@ async def download_run_node( request_options: typing.Optional[RequestOptions] = None, ) -> ApiResponseDownloadUrlOut: """ - Create a temporary download URL for a node-level workflow export. + Create a temporary download URL for a single output node's audio export. + + Returns a pre-signed URL for a ZIP archive containing the audio files + produced by one specific output node within the run. Useful when a + workflow has multiple output nodes and the caller wants only one node's + 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 + 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 + archive, use `GET /runs/{run_id}/download` instead. Parameters ---------- @@ -1840,10 +2203,17 @@ async def pause_run( request_options: typing.Optional[RequestOptions] = None, ) -> ApiResponseWorkflowRunOut: """ - Pause a workflow run. + Pause an active workflow run at the next safe checkpoint. + + For a running run, the current wave of parallel nodes is allowed to finish + before the run parks (in-flight work is preserved, not abandoned). For a + pending run that has not yet started, it parks immediately. The run + transitions to `paused` status once drained; during the drain period, + `status` remains `running` with `pause_requested_at` set. - A running run finishes its current wave (preserving in-flight work) and parks at the - next wave boundary; a pending run parks immediately. Fire-and-forget and idempotent. + The operation is idempotent: pausing an already-paused run returns it + unchanged. A paused run can be resumed via `POST /runs/{run_id}/resume` + or permanently stopped via `POST /runs/{run_id}/cancel`. Parameters ---------- @@ -1897,8 +2267,15 @@ async def resume_run( """ Resume a paused workflow run from its last completed wave. - Best-effort: if the workflow already has another active run, or the caller is at their - concurrent-run limit, the run stays paused and a 409 explains why (retry later). + Transitions the run from `paused` back to `running` and schedules + execution to continue from where it left off — no nodes that already + completed are re-executed. + + Returns 409 if the workspace already has another active run for this + workflow, or if the caller is at the concurrent-run limit. In that case + the run stays `paused` and the caller can retry later. Only runs in + `paused` status can be resumed; attempting to resume a `running`, + `completed`, `failed`, or `cancelled` run returns 409. Parameters ---------- @@ -1949,7 +2326,12 @@ async def duplicate_workflow( request_options: typing.Optional[RequestOptions] = None, ) -> ApiResponseWorkflowOut: """ - Duplicate a workflow. + Create a copy of an existing workflow in the same workspace. + + The new workflow inherits the source's `name` (suffixed with " (Copy)"), + `description`, and `definition`. Runs from the original workflow are not + copied — the duplicate starts with zero runs. Requires at least `editor` + role. Returns 201 with the new workflow on success. Parameters ---------- diff --git a/src/onepin/workflows/raw_client.py b/src/onepin/workflows/raw_client.py index 8b9761b..28712e1 100644 --- a/src/onepin/workflows/raw_client.py +++ b/src/onepin/workflows/raw_client.py @@ -50,6 +50,7 @@ def list( order: typing.Optional[typing.Sequence[ListWorkflowsRequestOrderItem]] = None, last_run_after: typing.Optional[dt.datetime] = None, last_run_before: typing.Optional[dt.datetime] = None, + has_failed_run: typing.Optional[bool] = None, offset: typing.Optional[int] = None, limit: typing.Optional[int] = None, workspace_id: typing.Optional[str] = None, @@ -58,23 +59,42 @@ def list( """ List workflows in the current workspace. - Multi-sort: `sort` and `order` are parallel lists. + 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. + + **Status filter:** `status` narrows by the UI-derived state of the workflow's + most recent run. `completed` matches only workflows whose latest run succeeded + (completed-only), and `failed` matches only workflows whose latest run failed — + the two buckets are disjoint. A workflow whose latest run was `cancelled` matches + neither bucket and surfaces only in the unfiltered list. `running` matches active + (running or paused) workflows. `draft` matches workflows with no runs yet. + `paused` is accepted but currently returns no results. + + **Multi-sort:** `sort` and `order` are parallel query lists. `?sort=runs_count&sort=name&order=desc&order=asc` orders primarily by - runs_count DESC, secondarily by name ASC. When `order` is shorter than - `sort`, missing entries default per-field: - `name=asc, updated_at=desc, runs_count=desc`. When `sort` is omitted, - list defaults to `updated_at DESC`. Every sort path appends - `Workflow.id ASC` as a deterministic tiebreaker for pagination stability. + `runs_count DESC`, then by `name ASC`. When `order` has fewer entries than + `sort`, missing positions use per-field defaults (`name=asc`, + `updated_at=desc`, `runs_count=desc`). Omitting `sort` defaults to + `updated_at DESC`. A stable `id ASC` tiebreaker is always appended so + offset/limit pagination is consistent when sort keys tie. + + **Date range:** `last_run_after` / `last_run_before` filter by the time of + the most recent run. Both must be ISO 8601 with a UTC offset; a naive + datetime returns 422. An inverted range (`after > before`) also returns 422. - `paused` is accepted but currently returns an empty result because - backend pause state is not implemented. `last_run_status` is the raw - RunStatus value, including values like `pending` or `cancelled`, and - pagination `total` is the filtered total for the current query. + **Failure history:** `has_failed_run` is orthogonal to `status`. `status` + keys off the latest run only; `has_failed_run=true` matches workflows with a + `failed` run *anywhere* in their history, so a workflow whose latest run + completed still matches if an earlier run failed. It composes with the other + filters (AND). `cancelled` runs do not count as failures. + + `pagination.total` reflects the filtered count for the current query. Parameters ---------- status : typing.Optional[WorkflowListStatus] - UI workflow status filter. `completed` means FINISHED — it matches workflows whose latest run is completed, failed, or cancelled, so it overlaps `failed` on failed runs. `paused` is accepted for forward compatibility and currently returns no rows. + UI workflow status filter. `completed` matches workflows whose latest run succeeded (completed-only); `failed` matches failed-only — the two are disjoint. A workflow whose latest run was cancelled matches neither and appears only in the unfiltered list. `paused` is accepted for forward compatibility and currently returns no rows. search : typing.Optional[str] Case-insensitive search over name and description. @@ -86,14 +106,19 @@ def list( Parallel to sort[]; shorter is padded with per-field defaults. last_run_after : typing.Optional[dt.datetime] - Filter workflows whose last_run_at is at or after this ISO datetime. + Filter workflows whose last_run_at is at or after this ISO datetime (ISO 8601 with UTC offset required). last_run_before : typing.Optional[dt.datetime] - Filter workflows whose last_run_at is at or before this ISO datetime. + Filter workflows whose last_run_at is at or before this ISO datetime (ISO 8601 with UTC offset required). + + has_failed_run : typing.Optional[bool] + Filter by failure history — ORTHOGONAL to `status` (which is latest-run based). `true` returns only workflows with at least one run that ended in `failed` state anywhere in their history; a workflow whose latest run succeeded still matches if an earlier run failed. `false` returns only workflows that have never had a failed run. Composes (ANDs) with `status`/`search`/date filters. `cancelled` runs are not treated as failures. offset : typing.Optional[int] + Zero-based pagination offset. limit : typing.Optional[int] + Maximum items to return (1–100). workspace_id : typing.Optional[str] @@ -115,6 +140,7 @@ def list( "order": order, "last_run_after": serialize_datetime(last_run_after) if last_run_after is not None else None, "last_run_before": serialize_datetime(last_run_before) if last_run_before is not None else None, + "has_failed_run": has_failed_run, "offset": offset, "limit": limit, }, @@ -163,17 +189,29 @@ def create_workflow( request_options: typing.Optional[RequestOptions] = None, ) -> HttpResponse[ApiResponseWorkflowOut]: """ - Create a workflow. + Create a new workflow in the current workspace. + + Validates the workflow `definition` (graph structure, node types, edge + connectivity) before persisting. Returns 422 with structured details if + the definition fails validation. Requires at least `editor` role in the + workspace; viewers cannot create workflows. + + The `definition` contains a `graph` (nodes and edges) and an `execution` + block (ordered step list and execution params). Omitting `definition` + creates a workflow with an empty graph that can be edited later. Parameters ---------- name : str + Human-readable workflow name (1–200 characters, non-blank). workspace_id : typing.Optional[str] description : typing.Optional[str] + Optional description shown in the workflow list (max 5000 characters). definition : typing.Optional[WorkflowDefinitionInput] + Graph and execution config. Omit to create an empty workflow. request_options : typing.Optional[RequestOptions] Request-specific configuration. @@ -238,7 +276,16 @@ def get( request_options: typing.Optional[RequestOptions] = None, ) -> HttpResponse[ApiResponseWorkflowOut]: """ - Fetch a workflow by id. + Fetch a single workflow by ID. + + Returns the full workflow including its `definition` (graph nodes/edges and + execution config), aggregate run stats, and the latest run status. The + `definition` is returned with any backwards-compatible config migrations + applied, so node configs always reflect the current schema even if the + workflow was saved with an older version. + + Use `GET /workflows` to list multiple workflows without fetching their + full definitions. Parameters ---------- @@ -303,19 +350,28 @@ def update_workflow( request_options: typing.Optional[RequestOptions] = None, ) -> HttpResponse[ApiResponseWorkflowOut]: """ - Update a workflow. + Replace a workflow's name, description, and definition (full update). + + All fields in the request body are required. The `definition` is + validated before persisting; an invalid graph returns 422. Existing runs + are not affected — each run captures a `definition_snapshot` at start + time. Requires at least `editor` role. Use `PATCH` to update only + specific fields without supplying the full definition. Parameters ---------- workflow_id : str name : str + Human-readable workflow name (1–200 characters, non-blank). definition : WorkflowDefinitionInput + Full replacement graph and execution config. Must be valid. workspace_id : typing.Optional[str] description : typing.Optional[str] + Optional description (max 5000 characters). Pass null to clear. request_options : typing.Optional[RequestOptions] Request-specific configuration. @@ -380,7 +436,12 @@ def delete_workflow( request_options: typing.Optional[RequestOptions] = None, ) -> HttpResponse[ApiResponseDict]: """ - Soft-delete a workflow. + Delete a workflow and hide it from all list and get endpoints. + + The workflow is soft-deleted: its data (including runs and their outputs) + is retained for audit and GDPR-purge purposes but is no longer accessible + via the API. Subsequent `GET`, `PUT`, `PATCH`, or run requests on the + same ID return 404. Requires at least `editor` role. Parameters ---------- @@ -445,7 +506,14 @@ def patch_workflow( request_options: typing.Optional[RequestOptions] = None, ) -> HttpResponse[ApiResponseWorkflowOut]: """ - Partially update a workflow. Only fields present in the body are applied. + Partially update a workflow — only supplied fields are changed. + + Any combination of `name`, `description`, and `definition` may be + included; omitted fields are left unchanged. At least one field must be + present (empty body returns 422). If `definition` is provided it is fully + validated; an invalid graph returns 422. Requires at least `editor` role. + + Use `PUT` when replacing the full workflow definition in one operation. Parameters ---------- @@ -454,10 +522,13 @@ def patch_workflow( workspace_id : typing.Optional[str] name : typing.Optional[str] + New workflow name (1–200 characters). Omit to leave unchanged. description : typing.Optional[str] + New description (max 5000 characters). Omit to leave unchanged; pass null to clear. definition : typing.Optional[WorkflowDefinitionInput] + Replacement graph and execution config. Omit to leave unchanged; must be valid if supplied. request_options : typing.Optional[RequestOptions] Request-specific configuration. @@ -526,13 +597,19 @@ def list_workflow_uploads( """ List confirmed uploads attached to a workflow. + Returns only uploads that have been confirmed (fully transferred and + committed to the workflow). In-progress or abandoned uploads are excluded. + Each item includes a short-lived download URL for the uploaded file. + Parameters ---------- workflow_id : str offset : typing.Optional[int] + Zero-based pagination offset. limit : typing.Optional[int] + Maximum items to return (1–100). workspace_id : typing.Optional[str] @@ -594,7 +671,12 @@ def estimate_workflow( request_options: typing.Optional[RequestOptions] = None, ) -> HttpResponse[ApiResponseEstimateResponse]: """ - Estimate workflow credits without creating a run. + Estimate the credit cost of running a workflow without creating a run. + + Computes a breakdown of expected credits per node type based on the + workflow's current definition. No run is created, no credits are charged, + and no side effects occur. Equivalent to `POST /runs/preview`; prefer that + path in new integrations as it is co-located with the run lifecycle. Parameters ---------- @@ -656,7 +738,12 @@ def preview_run( request_options: typing.Optional[RequestOptions] = None, ) -> HttpResponse[ApiResponseEstimateResponse]: """ - Estimate workflow run credits without creating a run. + Dry-run credit estimate for a workflow — no run is created. + + Returns a per-node-type credit breakdown based on the workflow's current + definition. No run is enqueued, no credits are charged, and the workflow + state is not modified. Use this before calling `POST /runs` to confirm + the expected cost. Equivalent to `POST /estimate`. Parameters ---------- @@ -720,22 +807,32 @@ def runs_summary( request_options: typing.Optional[RequestOptions] = None, ) -> HttpResponse[ApiResponseRunsSummaryOut]: """ - Aggregate run-status counts plus pass_rate and average_duration_seconds. + Aggregate run statistics for a workflow over an optional date window. + + Returns per-status counts (`completed`, `failed`, `cancelled`, `pending`, + `running`, `paused`) plus two derived metrics: + + - `pass_rate`: `completed / (completed + failed + cancelled)`. `null` when + there are no terminal runs in the window. + - `average_duration_seconds`: mean of `completed_at - started_at` over + successfully completed runs only. `null` when no runs have completed. + + **Date range:** `from` / `to` filter by `created_at`. Both must be ISO 8601 + with a UTC offset; a naive datetime returns 422. An inverted range + (`from > to`) also returns 422. Omit both to aggregate over all runs. - ``pass_rate = completed / (completed + failed + cancelled)``; - ``None`` when no terminal runs. - ``average_duration_seconds = mean(completed_at - started_at)`` over - completed runs only; ``None`` when there are zero completed runs. + Use `GET /runs` with `?status=` filters for individual run details; this + endpoint is best for dashboard-style health metrics. Parameters ---------- workflow_id : str from_ : typing.Optional[dt.datetime] - Filter runs by created_at >= this ISO datetime. + Filter runs by created_at >= this ISO datetime (ISO 8601 with UTC offset required). to : typing.Optional[dt.datetime] - Filter runs by created_at <= this ISO datetime. + Filter runs by created_at <= this ISO datetime (ISO 8601 with UTC offset required). workspace_id : typing.Optional[str] @@ -798,7 +895,23 @@ def get_run_steps( request_options: typing.Optional[RequestOptions] = None, ) -> HttpResponse[ApiResponseListWorkflowRunStepOut]: """ - List steps for a workflow run. + List per-node execution steps for a workflow run. + + Returns one entry per node execution attempt, ordered by execution sequence. + Each step includes the node type, status, iteration number (for nodes that + are retried), start/completion timestamps, and the node's `result` output. + + For audio output nodes, `result` is hydrated with short-lived `playback_url` + 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. + + For a higher-level view with aggregated metrics (pass rates, audio duration + by language), use `GET /runs/{run_id}/overview`. For paginated, grouped + script+audio rows suitable for a data table, use `GET /runs/{run_id}/data`. Parameters ---------- @@ -865,6 +978,21 @@ def get_run_overview( """ Fetch server-computed overview aggregates for a workflow run. + Returns structured metric sections (e.g. audio duration totals, validation + 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. + + This endpoint is best suited for a summary/results view after a run + completes. It differs from the other run sub-resources as follows: + + - `GET /runs/{run_id}` — full run record including the raw definition snapshot. + - `GET /runs/{run_id}/status` — volatile status fields only; for polling. + - `GET /runs/{run_id}/steps` — flat per-node step log with audio playback URLs. + - `GET /runs/{run_id}/data` — paginated script+audio rows for a data table. + - `GET /runs/{run_id}/overview` (this endpoint) — pre-aggregated metrics and + node state map for a dashboard/overview panel. + Parameters ---------- workflow_id : str @@ -932,10 +1060,26 @@ def get_run_data( request_options: typing.Optional[RequestOptions] = None, ) -> HttpResponse[WorkflowRunDataResponse]: """ - Fetch normalized grouped rows/cards for the run detail Data tab. + Paginated script-and-audio data rows for a completed workflow run. + + Returns grouped rows where each row represents one source script line. + Within each row, `cards` contain the per-language audio outputs, per-card + validation scores (word accuracy, naturalness), and short-lived audio + `playback_url` values (valid for 15 minutes). + + **Filtering:** + - `search` narrows which rows are returned based on their source script text. + - `language` narrows the `cards` list within each returned row to a single + locale. Rows with no matching cards are still returned (with empty `cards`), + and `pagination.total` always reflects the search-filtered row count + regardless of `language`. + + **Pagination:** `pagination.total` is scoped to the `search` filter only. - `pagination.total` is search-scoped and language-independent; language - filters only card lists, so returned rows may contain empty `cards`. + Response includes a `partial` field indicating whether any data is still + being computed (e.g. audio not yet generated, validation not yet scored). + This endpoint sets `Cache-Control: no-store` because playback URLs are + short-lived and data may change while a run is still in progress. Parameters ---------- @@ -944,14 +1088,16 @@ def get_run_data( run_id : str search : typing.Optional[str] - Case-insensitive search over visible grouped source/script text. + Case-insensitive search over the source/script text of each row. language : typing.Optional[str] - Exact full-locale card filter. `_` is normalized to `-`; filtering cards preserves row visibility and pagination.total remains language-independent, so rows may return empty cards. + Exact full-locale code to filter cards within each row (e.g. `en-US`). `_` is normalized to `-`. Filtering is card-level only — rows remain visible even when all their cards are filtered out, and `pagination.total` is unaffected. offset : typing.Optional[int] + Zero-based pagination offset. limit : typing.Optional[int] + Maximum rows to return (1–100). workspace_id : typing.Optional[str] @@ -1016,7 +1162,20 @@ def download_run( request_options: typing.Optional[RequestOptions] = None, ) -> HttpResponse[ApiResponseDownloadUrlOut]: """ - Create a temporary download URL for a workflow run export. + Create a temporary download URL for a complete workflow run export. + + Returns a pre-signed URL pointing to a ZIP archive containing all audio + output files produced by the run. The URL is valid for 15 minutes + (`expires_at`). The archive is generated on first request and cached for + subsequent calls; re-calling this endpoint before expiry returns a new + URL for the same cached archive. + + Only available for runs in `completed` status — returns 409 for runs that + are still active or ended in `failed`/`cancelled`. Returns 404 if the run + produced no audio files. + + To download output from a single output node rather than the whole run, + use `GET /runs/{run_id}/nodes/{node_id}/download`. Parameters ---------- @@ -1104,7 +1263,20 @@ def download_run_node( request_options: typing.Optional[RequestOptions] = None, ) -> HttpResponse[ApiResponseDownloadUrlOut]: """ - Create a temporary download URL for a node-level workflow export. + Create a temporary download URL for a single output node's audio export. + + Returns a pre-signed URL for a ZIP archive containing the audio files + produced by one specific output node within the run. Useful when a + workflow has multiple output nodes and the caller wants only one node's + 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 + 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 + archive, use `GET /runs/{run_id}/download` instead. Parameters ---------- @@ -1193,10 +1365,17 @@ def pause_run( request_options: typing.Optional[RequestOptions] = None, ) -> HttpResponse[ApiResponseWorkflowRunOut]: """ - Pause a workflow run. + Pause an active workflow run at the next safe checkpoint. + + For a running run, the current wave of parallel nodes is allowed to finish + before the run parks (in-flight work is preserved, not abandoned). For a + pending run that has not yet started, it parks immediately. The run + transitions to `paused` status once drained; during the drain period, + `status` remains `running` with `pause_requested_at` set. - A running run finishes its current wave (preserving in-flight work) and parks at the - next wave boundary; a pending run parks immediately. Fire-and-forget and idempotent. + The operation is idempotent: pausing an already-paused run returns it + unchanged. A paused run can be resumed via `POST /runs/{run_id}/resume` + or permanently stopped via `POST /runs/{run_id}/cancel`. Parameters ---------- @@ -1263,8 +1442,15 @@ def resume_run( """ Resume a paused workflow run from its last completed wave. - Best-effort: if the workflow already has another active run, or the caller is at their - concurrent-run limit, the run stays paused and a 409 explains why (retry later). + Transitions the run from `paused` back to `running` and schedules + execution to continue from where it left off — no nodes that already + completed are re-executed. + + Returns 409 if the workspace already has another active run for this + workflow, or if the caller is at the concurrent-run limit. In that case + the run stays `paused` and the caller can retry later. Only runs in + `paused` status can be resumed; attempting to resume a `running`, + `completed`, `failed`, or `cancelled` run returns 409. Parameters ---------- @@ -1328,7 +1514,12 @@ def duplicate_workflow( request_options: typing.Optional[RequestOptions] = None, ) -> HttpResponse[ApiResponseWorkflowOut]: """ - Duplicate a workflow. + Create a copy of an existing workflow in the same workspace. + + The new workflow inherits the source's `name` (suffixed with " (Copy)"), + `description`, and `definition`. Runs from the original workflow are not + copied — the duplicate starts with zero runs. Requires at least `editor` + role. Returns 201 with the new workflow on success. Parameters ---------- @@ -1396,6 +1587,7 @@ async def list( order: typing.Optional[typing.Sequence[ListWorkflowsRequestOrderItem]] = None, last_run_after: typing.Optional[dt.datetime] = None, last_run_before: typing.Optional[dt.datetime] = None, + has_failed_run: typing.Optional[bool] = None, offset: typing.Optional[int] = None, limit: typing.Optional[int] = None, workspace_id: typing.Optional[str] = None, @@ -1404,23 +1596,42 @@ async def list( """ List workflows in the current workspace. - Multi-sort: `sort` and `order` are parallel lists. + 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. + + **Status filter:** `status` narrows by the UI-derived state of the workflow's + most recent run. `completed` matches only workflows whose latest run succeeded + (completed-only), and `failed` matches only workflows whose latest run failed — + the two buckets are disjoint. A workflow whose latest run was `cancelled` matches + neither bucket and surfaces only in the unfiltered list. `running` matches active + (running or paused) workflows. `draft` matches workflows with no runs yet. + `paused` is accepted but currently returns no results. + + **Multi-sort:** `sort` and `order` are parallel query lists. `?sort=runs_count&sort=name&order=desc&order=asc` orders primarily by - runs_count DESC, secondarily by name ASC. When `order` is shorter than - `sort`, missing entries default per-field: - `name=asc, updated_at=desc, runs_count=desc`. When `sort` is omitted, - list defaults to `updated_at DESC`. Every sort path appends - `Workflow.id ASC` as a deterministic tiebreaker for pagination stability. + `runs_count DESC`, then by `name ASC`. When `order` has fewer entries than + `sort`, missing positions use per-field defaults (`name=asc`, + `updated_at=desc`, `runs_count=desc`). Omitting `sort` defaults to + `updated_at DESC`. A stable `id ASC` tiebreaker is always appended so + offset/limit pagination is consistent when sort keys tie. + + **Date range:** `last_run_after` / `last_run_before` filter by the time of + the most recent run. Both must be ISO 8601 with a UTC offset; a naive + datetime returns 422. An inverted range (`after > before`) also returns 422. - `paused` is accepted but currently returns an empty result because - backend pause state is not implemented. `last_run_status` is the raw - RunStatus value, including values like `pending` or `cancelled`, and - pagination `total` is the filtered total for the current query. + **Failure history:** `has_failed_run` is orthogonal to `status`. `status` + keys off the latest run only; `has_failed_run=true` matches workflows with a + `failed` run *anywhere* in their history, so a workflow whose latest run + completed still matches if an earlier run failed. It composes with the other + filters (AND). `cancelled` runs do not count as failures. + + `pagination.total` reflects the filtered count for the current query. Parameters ---------- status : typing.Optional[WorkflowListStatus] - UI workflow status filter. `completed` means FINISHED — it matches workflows whose latest run is completed, failed, or cancelled, so it overlaps `failed` on failed runs. `paused` is accepted for forward compatibility and currently returns no rows. + UI workflow status filter. `completed` matches workflows whose latest run succeeded (completed-only); `failed` matches failed-only — the two are disjoint. A workflow whose latest run was cancelled matches neither and appears only in the unfiltered list. `paused` is accepted for forward compatibility and currently returns no rows. search : typing.Optional[str] Case-insensitive search over name and description. @@ -1432,14 +1643,19 @@ async def list( Parallel to sort[]; shorter is padded with per-field defaults. last_run_after : typing.Optional[dt.datetime] - Filter workflows whose last_run_at is at or after this ISO datetime. + Filter workflows whose last_run_at is at or after this ISO datetime (ISO 8601 with UTC offset required). last_run_before : typing.Optional[dt.datetime] - Filter workflows whose last_run_at is at or before this ISO datetime. + Filter workflows whose last_run_at is at or before this ISO datetime (ISO 8601 with UTC offset required). + + has_failed_run : typing.Optional[bool] + Filter by failure history — ORTHOGONAL to `status` (which is latest-run based). `true` returns only workflows with at least one run that ended in `failed` state anywhere in their history; a workflow whose latest run succeeded still matches if an earlier run failed. `false` returns only workflows that have never had a failed run. Composes (ANDs) with `status`/`search`/date filters. `cancelled` runs are not treated as failures. offset : typing.Optional[int] + Zero-based pagination offset. limit : typing.Optional[int] + Maximum items to return (1–100). workspace_id : typing.Optional[str] @@ -1461,6 +1677,7 @@ async def list( "order": order, "last_run_after": serialize_datetime(last_run_after) if last_run_after is not None else None, "last_run_before": serialize_datetime(last_run_before) if last_run_before is not None else None, + "has_failed_run": has_failed_run, "offset": offset, "limit": limit, }, @@ -1509,17 +1726,29 @@ async def create_workflow( request_options: typing.Optional[RequestOptions] = None, ) -> AsyncHttpResponse[ApiResponseWorkflowOut]: """ - Create a workflow. + Create a new workflow in the current workspace. + + Validates the workflow `definition` (graph structure, node types, edge + connectivity) before persisting. Returns 422 with structured details if + the definition fails validation. Requires at least `editor` role in the + workspace; viewers cannot create workflows. + + The `definition` contains a `graph` (nodes and edges) and an `execution` + block (ordered step list and execution params). Omitting `definition` + creates a workflow with an empty graph that can be edited later. Parameters ---------- name : str + Human-readable workflow name (1–200 characters, non-blank). workspace_id : typing.Optional[str] description : typing.Optional[str] + Optional description shown in the workflow list (max 5000 characters). definition : typing.Optional[WorkflowDefinitionInput] + Graph and execution config. Omit to create an empty workflow. request_options : typing.Optional[RequestOptions] Request-specific configuration. @@ -1584,7 +1813,16 @@ async def get( request_options: typing.Optional[RequestOptions] = None, ) -> AsyncHttpResponse[ApiResponseWorkflowOut]: """ - Fetch a workflow by id. + Fetch a single workflow by ID. + + Returns the full workflow including its `definition` (graph nodes/edges and + execution config), aggregate run stats, and the latest run status. The + `definition` is returned with any backwards-compatible config migrations + applied, so node configs always reflect the current schema even if the + workflow was saved with an older version. + + Use `GET /workflows` to list multiple workflows without fetching their + full definitions. Parameters ---------- @@ -1649,19 +1887,28 @@ async def update_workflow( request_options: typing.Optional[RequestOptions] = None, ) -> AsyncHttpResponse[ApiResponseWorkflowOut]: """ - Update a workflow. + Replace a workflow's name, description, and definition (full update). + + All fields in the request body are required. The `definition` is + validated before persisting; an invalid graph returns 422. Existing runs + are not affected — each run captures a `definition_snapshot` at start + time. Requires at least `editor` role. Use `PATCH` to update only + specific fields without supplying the full definition. Parameters ---------- workflow_id : str name : str + Human-readable workflow name (1–200 characters, non-blank). definition : WorkflowDefinitionInput + Full replacement graph and execution config. Must be valid. workspace_id : typing.Optional[str] description : typing.Optional[str] + Optional description (max 5000 characters). Pass null to clear. request_options : typing.Optional[RequestOptions] Request-specific configuration. @@ -1726,7 +1973,12 @@ async def delete_workflow( request_options: typing.Optional[RequestOptions] = None, ) -> AsyncHttpResponse[ApiResponseDict]: """ - Soft-delete a workflow. + Delete a workflow and hide it from all list and get endpoints. + + The workflow is soft-deleted: its data (including runs and their outputs) + is retained for audit and GDPR-purge purposes but is no longer accessible + via the API. Subsequent `GET`, `PUT`, `PATCH`, or run requests on the + same ID return 404. Requires at least `editor` role. Parameters ---------- @@ -1791,7 +2043,14 @@ async def patch_workflow( request_options: typing.Optional[RequestOptions] = None, ) -> AsyncHttpResponse[ApiResponseWorkflowOut]: """ - Partially update a workflow. Only fields present in the body are applied. + Partially update a workflow — only supplied fields are changed. + + Any combination of `name`, `description`, and `definition` may be + included; omitted fields are left unchanged. At least one field must be + present (empty body returns 422). If `definition` is provided it is fully + validated; an invalid graph returns 422. Requires at least `editor` role. + + Use `PUT` when replacing the full workflow definition in one operation. Parameters ---------- @@ -1800,10 +2059,13 @@ async def patch_workflow( workspace_id : typing.Optional[str] name : typing.Optional[str] + New workflow name (1–200 characters). Omit to leave unchanged. description : typing.Optional[str] + New description (max 5000 characters). Omit to leave unchanged; pass null to clear. definition : typing.Optional[WorkflowDefinitionInput] + Replacement graph and execution config. Omit to leave unchanged; must be valid if supplied. request_options : typing.Optional[RequestOptions] Request-specific configuration. @@ -1872,13 +2134,19 @@ async def list_workflow_uploads( """ List confirmed uploads attached to a workflow. + Returns only uploads that have been confirmed (fully transferred and + committed to the workflow). In-progress or abandoned uploads are excluded. + Each item includes a short-lived download URL for the uploaded file. + Parameters ---------- workflow_id : str offset : typing.Optional[int] + Zero-based pagination offset. limit : typing.Optional[int] + Maximum items to return (1–100). workspace_id : typing.Optional[str] @@ -1940,7 +2208,12 @@ async def estimate_workflow( request_options: typing.Optional[RequestOptions] = None, ) -> AsyncHttpResponse[ApiResponseEstimateResponse]: """ - Estimate workflow credits without creating a run. + Estimate the credit cost of running a workflow without creating a run. + + Computes a breakdown of expected credits per node type based on the + workflow's current definition. No run is created, no credits are charged, + and no side effects occur. Equivalent to `POST /runs/preview`; prefer that + path in new integrations as it is co-located with the run lifecycle. Parameters ---------- @@ -2002,7 +2275,12 @@ async def preview_run( request_options: typing.Optional[RequestOptions] = None, ) -> AsyncHttpResponse[ApiResponseEstimateResponse]: """ - Estimate workflow run credits without creating a run. + Dry-run credit estimate for a workflow — no run is created. + + Returns a per-node-type credit breakdown based on the workflow's current + definition. No run is enqueued, no credits are charged, and the workflow + state is not modified. Use this before calling `POST /runs` to confirm + the expected cost. Equivalent to `POST /estimate`. Parameters ---------- @@ -2066,22 +2344,32 @@ async def runs_summary( request_options: typing.Optional[RequestOptions] = None, ) -> AsyncHttpResponse[ApiResponseRunsSummaryOut]: """ - Aggregate run-status counts plus pass_rate and average_duration_seconds. + Aggregate run statistics for a workflow over an optional date window. + + Returns per-status counts (`completed`, `failed`, `cancelled`, `pending`, + `running`, `paused`) plus two derived metrics: + + - `pass_rate`: `completed / (completed + failed + cancelled)`. `null` when + there are no terminal runs in the window. + - `average_duration_seconds`: mean of `completed_at - started_at` over + successfully completed runs only. `null` when no runs have completed. + + **Date range:** `from` / `to` filter by `created_at`. Both must be ISO 8601 + with a UTC offset; a naive datetime returns 422. An inverted range + (`from > to`) also returns 422. Omit both to aggregate over all runs. - ``pass_rate = completed / (completed + failed + cancelled)``; - ``None`` when no terminal runs. - ``average_duration_seconds = mean(completed_at - started_at)`` over - completed runs only; ``None`` when there are zero completed runs. + Use `GET /runs` with `?status=` filters for individual run details; this + endpoint is best for dashboard-style health metrics. Parameters ---------- workflow_id : str from_ : typing.Optional[dt.datetime] - Filter runs by created_at >= this ISO datetime. + Filter runs by created_at >= this ISO datetime (ISO 8601 with UTC offset required). to : typing.Optional[dt.datetime] - Filter runs by created_at <= this ISO datetime. + Filter runs by created_at <= this ISO datetime (ISO 8601 with UTC offset required). workspace_id : typing.Optional[str] @@ -2144,7 +2432,23 @@ async def get_run_steps( request_options: typing.Optional[RequestOptions] = None, ) -> AsyncHttpResponse[ApiResponseListWorkflowRunStepOut]: """ - List steps for a workflow run. + List per-node execution steps for a workflow run. + + Returns one entry per node execution attempt, ordered by execution sequence. + Each step includes the node type, status, iteration number (for nodes that + are retried), start/completion timestamps, and the node's `result` output. + + For audio output nodes, `result` is hydrated with short-lived `playback_url` + 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. + + For a higher-level view with aggregated metrics (pass rates, audio duration + by language), use `GET /runs/{run_id}/overview`. For paginated, grouped + script+audio rows suitable for a data table, use `GET /runs/{run_id}/data`. Parameters ---------- @@ -2211,6 +2515,21 @@ async def get_run_overview( """ Fetch server-computed overview aggregates for a workflow run. + Returns structured metric sections (e.g. audio duration totals, validation + 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. + + This endpoint is best suited for a summary/results view after a run + completes. It differs from the other run sub-resources as follows: + + - `GET /runs/{run_id}` — full run record including the raw definition snapshot. + - `GET /runs/{run_id}/status` — volatile status fields only; for polling. + - `GET /runs/{run_id}/steps` — flat per-node step log with audio playback URLs. + - `GET /runs/{run_id}/data` — paginated script+audio rows for a data table. + - `GET /runs/{run_id}/overview` (this endpoint) — pre-aggregated metrics and + node state map for a dashboard/overview panel. + Parameters ---------- workflow_id : str @@ -2278,10 +2597,26 @@ async def get_run_data( request_options: typing.Optional[RequestOptions] = None, ) -> AsyncHttpResponse[WorkflowRunDataResponse]: """ - Fetch normalized grouped rows/cards for the run detail Data tab. + Paginated script-and-audio data rows for a completed workflow run. + + Returns grouped rows where each row represents one source script line. + Within each row, `cards` contain the per-language audio outputs, per-card + validation scores (word accuracy, naturalness), and short-lived audio + `playback_url` values (valid for 15 minutes). + + **Filtering:** + - `search` narrows which rows are returned based on their source script text. + - `language` narrows the `cards` list within each returned row to a single + locale. Rows with no matching cards are still returned (with empty `cards`), + and `pagination.total` always reflects the search-filtered row count + regardless of `language`. + + **Pagination:** `pagination.total` is scoped to the `search` filter only. - `pagination.total` is search-scoped and language-independent; language - filters only card lists, so returned rows may contain empty `cards`. + Response includes a `partial` field indicating whether any data is still + being computed (e.g. audio not yet generated, validation not yet scored). + This endpoint sets `Cache-Control: no-store` because playback URLs are + short-lived and data may change while a run is still in progress. Parameters ---------- @@ -2290,14 +2625,16 @@ async def get_run_data( run_id : str search : typing.Optional[str] - Case-insensitive search over visible grouped source/script text. + Case-insensitive search over the source/script text of each row. language : typing.Optional[str] - Exact full-locale card filter. `_` is normalized to `-`; filtering cards preserves row visibility and pagination.total remains language-independent, so rows may return empty cards. + Exact full-locale code to filter cards within each row (e.g. `en-US`). `_` is normalized to `-`. Filtering is card-level only — rows remain visible even when all their cards are filtered out, and `pagination.total` is unaffected. offset : typing.Optional[int] + Zero-based pagination offset. limit : typing.Optional[int] + Maximum rows to return (1–100). workspace_id : typing.Optional[str] @@ -2362,7 +2699,20 @@ async def download_run( request_options: typing.Optional[RequestOptions] = None, ) -> AsyncHttpResponse[ApiResponseDownloadUrlOut]: """ - Create a temporary download URL for a workflow run export. + Create a temporary download URL for a complete workflow run export. + + Returns a pre-signed URL pointing to a ZIP archive containing all audio + output files produced by the run. The URL is valid for 15 minutes + (`expires_at`). The archive is generated on first request and cached for + subsequent calls; re-calling this endpoint before expiry returns a new + URL for the same cached archive. + + Only available for runs in `completed` status — returns 409 for runs that + are still active or ended in `failed`/`cancelled`. Returns 404 if the run + produced no audio files. + + To download output from a single output node rather than the whole run, + use `GET /runs/{run_id}/nodes/{node_id}/download`. Parameters ---------- @@ -2450,7 +2800,20 @@ async def download_run_node( request_options: typing.Optional[RequestOptions] = None, ) -> AsyncHttpResponse[ApiResponseDownloadUrlOut]: """ - Create a temporary download URL for a node-level workflow export. + Create a temporary download URL for a single output node's audio export. + + Returns a pre-signed URL for a ZIP archive containing the audio files + produced by one specific output node within the run. Useful when a + workflow has multiple output nodes and the caller wants only one node's + 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 + 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 + archive, use `GET /runs/{run_id}/download` instead. Parameters ---------- @@ -2539,10 +2902,17 @@ async def pause_run( request_options: typing.Optional[RequestOptions] = None, ) -> AsyncHttpResponse[ApiResponseWorkflowRunOut]: """ - Pause a workflow run. + Pause an active workflow run at the next safe checkpoint. + + For a running run, the current wave of parallel nodes is allowed to finish + before the run parks (in-flight work is preserved, not abandoned). For a + pending run that has not yet started, it parks immediately. The run + transitions to `paused` status once drained; during the drain period, + `status` remains `running` with `pause_requested_at` set. - A running run finishes its current wave (preserving in-flight work) and parks at the - next wave boundary; a pending run parks immediately. Fire-and-forget and idempotent. + The operation is idempotent: pausing an already-paused run returns it + unchanged. A paused run can be resumed via `POST /runs/{run_id}/resume` + or permanently stopped via `POST /runs/{run_id}/cancel`. Parameters ---------- @@ -2609,8 +2979,15 @@ async def resume_run( """ Resume a paused workflow run from its last completed wave. - Best-effort: if the workflow already has another active run, or the caller is at their - concurrent-run limit, the run stays paused and a 409 explains why (retry later). + Transitions the run from `paused` back to `running` and schedules + execution to continue from where it left off — no nodes that already + completed are re-executed. + + Returns 409 if the workspace already has another active run for this + workflow, or if the caller is at the concurrent-run limit. In that case + the run stays `paused` and the caller can retry later. Only runs in + `paused` status can be resumed; attempting to resume a `running`, + `completed`, `failed`, or `cancelled` run returns 409. Parameters ---------- @@ -2674,7 +3051,12 @@ async def duplicate_workflow( request_options: typing.Optional[RequestOptions] = None, ) -> AsyncHttpResponse[ApiResponseWorkflowOut]: """ - Duplicate a workflow. + Create a copy of an existing workflow in the same workspace. + + The new workflow inherits the source's `name` (suffixed with " (Copy)"), + `description`, and `definition`. Runs from the original workflow are not + copied — the duplicate starts with zero runs. Requires at least `editor` + role. Returns 201 with the new workflow on success. Parameters ---------- diff --git a/src/onepin/workflows/runs/client.py b/src/onepin/workflows/runs/client.py index 304d179..e5e2327 100644 --- a/src/onepin/workflows/runs/client.py +++ b/src/onepin/workflows/runs/client.py @@ -42,32 +42,43 @@ def list( request_options: typing.Optional[RequestOptions] = None, ) -> ApiCountedListResponseWorkflowRunListItem: """ - List runs for a workflow. + List runs for a workflow, newest first by default. - Tiebreaker is always ``id ASC`` so offset/limit pagination is stable when - primary sort keys tie. ``status`` accepts comma-separated raw RunStatus - values; unknown values return 422. ``search`` matches the triggering - user's display name (full name, falling back to email). + Returns a counted, paginated list of runs for the specified workflow. + Each item includes the run's status, step progress (`total_steps`, + `finished_steps`), credit usage, and the actor who triggered it. + + **Status filter:** Pass `?status=completed,failed` to OR-match multiple + statuses. Values must be the raw lowercase RunStatus strings. Unknown values + or empty tokens (e.g. `a,,b`) return 422. + + **Pagination:** `pagination.total` reflects the filtered count. Sort + tiebreaks always append `id ASC` for stable offset/limit pagination. + + For aggregate statistics (pass rate, average duration) across all runs, + use `GET /runs/summary` instead. Parameters ---------- workflow_id : str offset : typing.Optional[int] + Zero-based pagination offset. limit : typing.Optional[int] + Maximum items to return (1–100). status : typing.Optional[str] - Comma-separated raw RunStatus values (e.g. `completed,failed`). Values are case-sensitive lowercase. Multiple values OR-match. Empty tokens (e.g. `a,,b`) and unknown values return 422. + Comma-separated raw RunStatus values (e.g. `completed,failed`). Values are case-sensitive lowercase: `pending`, `running`, `completed`, `failed`, `cancelled`, `paused`. Multiple values OR-match. Empty tokens (e.g. `a,,b`) and unknown values return 422. search : typing.Optional[str] - Case-insensitive search over triggering user's display name and email. + Case-insensitive search over the triggering user's display name (falls back to email). sort : typing.Optional[ListRunsRequestSort] - Sort field: created_at | started_at | completed_at | status. + Sort field: `created_at` | `started_at` | `completed_at` | `status`. Defaults to `created_at`. order : typing.Optional[ListRunsRequestOrder] - asc or desc. + `asc` or `desc`. Defaults to `desc`. workspace_id : typing.Optional[str] @@ -111,7 +122,20 @@ def start( request_options: typing.Optional[RequestOptions] = None, ) -> ApiResponseWorkflowRunOut: """ - Start a workflow run. + Start a new execution of a workflow (202 Accepted). + + Enqueues the workflow for asynchronous execution and returns the newly + created run in `pending` or `running` status. The run progresses through + 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. + + 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. + + Returns 409 if the workspace is at its concurrent-run limit or another + run for this workflow is already active. Parameters ---------- @@ -150,11 +174,18 @@ def get( request_options: typing.Optional[RequestOptions] = None, ) -> ApiResponseWorkflowRunDetailOut: """ - Fetch a workflow run by id. + Fetch full detail for a single workflow run. - Includes `definition_snapshot` — the graph/execution config captured - when the run started, returned raw (no config migrations applied) so - it reflects the workflow exactly as it existed for this run. + Returns all run fields plus `definition_snapshot` — the graph and + execution config captured at the moment the run started. The snapshot is + returned raw (no config migrations applied), so it faithfully represents + the workflow as it existed for this specific execution even if the + workflow definition has since been edited. + + This is the heaviest run endpoint. For progress polling, use the lighter + `GET /runs/{run_id}/status` which omits the snapshot. For aggregated + visual metrics, use `GET /runs/{run_id}/overview`. For the per-node step + log with audio playback URLs, use `GET /runs/{run_id}/steps`. Parameters ---------- @@ -198,12 +229,23 @@ def status( request_options: typing.Optional[RequestOptions] = None, ) -> ApiResponseWorkflowRunStatusOut: """ - Lightweight run status for polling. + Lightweight run status for progress polling. + + Returns only the volatile, frequently-changing fields: `status`, step + counts (`total_steps`, `finished_steps`), timestamps (`started_at`, + `completed_at`, `paused_at`, `pause_requested_at`), `usage_summary`, + `error`, and `has_export`. The definition snapshot is intentionally + omitted to keep response size small. + + Recommended polling pattern: call `GET /runs/{run_id}` once after + starting a run to load the immutable definition snapshot and initial + metadata, then poll this endpoint until `status` reaches a terminal value + (`completed`, `failed`, or `cancelled`). `has_export` becoming `true` + signals that a download is ready via `GET /runs/{run_id}/download`. - Returns only the volatile run state (status, step counts, timestamps, - usage_summary, error, has_export) — no graph snapshot. Use this for - progress polling; call `GET /runs/{run_id}` once to load the immutable - definition snapshot. + The transient `pausing` state is observable here: `status == "running"` + with `pause_requested_at` set means a pause is in progress but the + current wave has not yet finished draining. Parameters ---------- @@ -247,7 +289,19 @@ def cancel( request_options: typing.Optional[RequestOptions] = None, ) -> ApiResponseWorkflowRunOut: """ - Cancel a running workflow run. + Cancel an active workflow run. + + Immediately marks the run as `cancelled` and stops any further processing. + In-flight work at the current node may be abandoned mid-execution. The + operation is idempotent: cancelling an already-cancelled run returns the + run unchanged without error. + + Only runs in `pending`, `running`, or `paused` status can be cancelled. + Runs that have already reached a terminal state (`completed`, `failed`, + `cancelled`) return 409. + + Unlike `pause`, cancel is permanent — a cancelled run cannot be resumed. + Use `pause` if you intend to continue the run later. Parameters ---------- @@ -312,32 +366,43 @@ async def list( request_options: typing.Optional[RequestOptions] = None, ) -> ApiCountedListResponseWorkflowRunListItem: """ - List runs for a workflow. + List runs for a workflow, newest first by default. - Tiebreaker is always ``id ASC`` so offset/limit pagination is stable when - primary sort keys tie. ``status`` accepts comma-separated raw RunStatus - values; unknown values return 422. ``search`` matches the triggering - user's display name (full name, falling back to email). + Returns a counted, paginated list of runs for the specified workflow. + Each item includes the run's status, step progress (`total_steps`, + `finished_steps`), credit usage, and the actor who triggered it. + + **Status filter:** Pass `?status=completed,failed` to OR-match multiple + statuses. Values must be the raw lowercase RunStatus strings. Unknown values + or empty tokens (e.g. `a,,b`) return 422. + + **Pagination:** `pagination.total` reflects the filtered count. Sort + tiebreaks always append `id ASC` for stable offset/limit pagination. + + For aggregate statistics (pass rate, average duration) across all runs, + use `GET /runs/summary` instead. Parameters ---------- workflow_id : str offset : typing.Optional[int] + Zero-based pagination offset. limit : typing.Optional[int] + Maximum items to return (1–100). status : typing.Optional[str] - Comma-separated raw RunStatus values (e.g. `completed,failed`). Values are case-sensitive lowercase. Multiple values OR-match. Empty tokens (e.g. `a,,b`) and unknown values return 422. + Comma-separated raw RunStatus values (e.g. `completed,failed`). Values are case-sensitive lowercase: `pending`, `running`, `completed`, `failed`, `cancelled`, `paused`. Multiple values OR-match. Empty tokens (e.g. `a,,b`) and unknown values return 422. search : typing.Optional[str] - Case-insensitive search over triggering user's display name and email. + Case-insensitive search over the triggering user's display name (falls back to email). sort : typing.Optional[ListRunsRequestSort] - Sort field: created_at | started_at | completed_at | status. + Sort field: `created_at` | `started_at` | `completed_at` | `status`. Defaults to `created_at`. order : typing.Optional[ListRunsRequestOrder] - asc or desc. + `asc` or `desc`. Defaults to `desc`. workspace_id : typing.Optional[str] @@ -389,7 +454,20 @@ async def start( request_options: typing.Optional[RequestOptions] = None, ) -> ApiResponseWorkflowRunOut: """ - Start a workflow run. + Start a new execution of a workflow (202 Accepted). + + Enqueues the workflow for asynchronous execution and returns the newly + created run in `pending` or `running` status. The run progresses through + 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. + + 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. + + Returns 409 if the workspace is at its concurrent-run limit or another + run for this workflow is already active. Parameters ---------- @@ -438,11 +516,18 @@ async def get( request_options: typing.Optional[RequestOptions] = None, ) -> ApiResponseWorkflowRunDetailOut: """ - Fetch a workflow run by id. + Fetch full detail for a single workflow run. - Includes `definition_snapshot` — the graph/execution config captured - when the run started, returned raw (no config migrations applied) so - it reflects the workflow exactly as it existed for this run. + Returns all run fields plus `definition_snapshot` — the graph and + execution config captured at the moment the run started. The snapshot is + returned raw (no config migrations applied), so it faithfully represents + the workflow as it existed for this specific execution even if the + workflow definition has since been edited. + + This is the heaviest run endpoint. For progress polling, use the lighter + `GET /runs/{run_id}/status` which omits the snapshot. For aggregated + visual metrics, use `GET /runs/{run_id}/overview`. For the per-node step + log with audio playback URLs, use `GET /runs/{run_id}/steps`. Parameters ---------- @@ -494,12 +579,23 @@ async def status( request_options: typing.Optional[RequestOptions] = None, ) -> ApiResponseWorkflowRunStatusOut: """ - Lightweight run status for polling. + Lightweight run status for progress polling. + + Returns only the volatile, frequently-changing fields: `status`, step + counts (`total_steps`, `finished_steps`), timestamps (`started_at`, + `completed_at`, `paused_at`, `pause_requested_at`), `usage_summary`, + `error`, and `has_export`. The definition snapshot is intentionally + omitted to keep response size small. + + Recommended polling pattern: call `GET /runs/{run_id}` once after + starting a run to load the immutable definition snapshot and initial + metadata, then poll this endpoint until `status` reaches a terminal value + (`completed`, `failed`, or `cancelled`). `has_export` becoming `true` + signals that a download is ready via `GET /runs/{run_id}/download`. - Returns only the volatile run state (status, step counts, timestamps, - usage_summary, error, has_export) — no graph snapshot. Use this for - progress polling; call `GET /runs/{run_id}` once to load the immutable - definition snapshot. + The transient `pausing` state is observable here: `status == "running"` + with `pause_requested_at` set means a pause is in progress but the + current wave has not yet finished draining. Parameters ---------- @@ -551,7 +647,19 @@ async def cancel( request_options: typing.Optional[RequestOptions] = None, ) -> ApiResponseWorkflowRunOut: """ - Cancel a running workflow run. + Cancel an active workflow run. + + Immediately marks the run as `cancelled` and stops any further processing. + In-flight work at the current node may be abandoned mid-execution. The + operation is idempotent: cancelling an already-cancelled run returns the + run unchanged without error. + + Only runs in `pending`, `running`, or `paused` status can be cancelled. + Runs that have already reached a terminal state (`completed`, `failed`, + `cancelled`) return 409. + + Unlike `pause`, cancel is permanent — a cancelled run cannot be resumed. + Use `pause` if you intend to continue the run later. Parameters ---------- diff --git a/src/onepin/workflows/runs/raw_client.py b/src/onepin/workflows/runs/raw_client.py index 73da382..fc71b49 100644 --- a/src/onepin/workflows/runs/raw_client.py +++ b/src/onepin/workflows/runs/raw_client.py @@ -38,32 +38,43 @@ def list( request_options: typing.Optional[RequestOptions] = None, ) -> HttpResponse[ApiCountedListResponseWorkflowRunListItem]: """ - List runs for a workflow. + List runs for a workflow, newest first by default. - Tiebreaker is always ``id ASC`` so offset/limit pagination is stable when - primary sort keys tie. ``status`` accepts comma-separated raw RunStatus - values; unknown values return 422. ``search`` matches the triggering - user's display name (full name, falling back to email). + Returns a counted, paginated list of runs for the specified workflow. + Each item includes the run's status, step progress (`total_steps`, + `finished_steps`), credit usage, and the actor who triggered it. + + **Status filter:** Pass `?status=completed,failed` to OR-match multiple + statuses. Values must be the raw lowercase RunStatus strings. Unknown values + or empty tokens (e.g. `a,,b`) return 422. + + **Pagination:** `pagination.total` reflects the filtered count. Sort + tiebreaks always append `id ASC` for stable offset/limit pagination. + + For aggregate statistics (pass rate, average duration) across all runs, + use `GET /runs/summary` instead. Parameters ---------- workflow_id : str offset : typing.Optional[int] + Zero-based pagination offset. limit : typing.Optional[int] + Maximum items to return (1–100). status : typing.Optional[str] - Comma-separated raw RunStatus values (e.g. `completed,failed`). Values are case-sensitive lowercase. Multiple values OR-match. Empty tokens (e.g. `a,,b`) and unknown values return 422. + Comma-separated raw RunStatus values (e.g. `completed,failed`). Values are case-sensitive lowercase: `pending`, `running`, `completed`, `failed`, `cancelled`, `paused`. Multiple values OR-match. Empty tokens (e.g. `a,,b`) and unknown values return 422. search : typing.Optional[str] - Case-insensitive search over triggering user's display name and email. + Case-insensitive search over the triggering user's display name (falls back to email). sort : typing.Optional[ListRunsRequestSort] - Sort field: created_at | started_at | completed_at | status. + Sort field: `created_at` | `started_at` | `completed_at` | `status`. Defaults to `created_at`. order : typing.Optional[ListRunsRequestOrder] - asc or desc. + `asc` or `desc`. Defaults to `desc`. workspace_id : typing.Optional[str] @@ -129,7 +140,20 @@ def start( request_options: typing.Optional[RequestOptions] = None, ) -> HttpResponse[ApiResponseWorkflowRunOut]: """ - Start a workflow run. + Start a new execution of a workflow (202 Accepted). + + Enqueues the workflow for asynchronous execution and returns the newly + created run in `pending` or `running` status. The run progresses through + 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. + + 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. + + Returns 409 if the workspace is at its concurrent-run limit or another + run for this workflow is already active. Parameters ---------- @@ -192,11 +216,18 @@ def get( request_options: typing.Optional[RequestOptions] = None, ) -> HttpResponse[ApiResponseWorkflowRunDetailOut]: """ - Fetch a workflow run by id. + Fetch full detail for a single workflow run. - Includes `definition_snapshot` — the graph/execution config captured - when the run started, returned raw (no config migrations applied) so - it reflects the workflow exactly as it existed for this run. + Returns all run fields plus `definition_snapshot` — the graph and + execution config captured at the moment the run started. The snapshot is + returned raw (no config migrations applied), so it faithfully represents + the workflow as it existed for this specific execution even if the + workflow definition has since been edited. + + This is the heaviest run endpoint. For progress polling, use the lighter + `GET /runs/{run_id}/status` which omits the snapshot. For aggregated + visual metrics, use `GET /runs/{run_id}/overview`. For the per-node step + log with audio playback URLs, use `GET /runs/{run_id}/steps`. Parameters ---------- @@ -261,12 +292,23 @@ def status( request_options: typing.Optional[RequestOptions] = None, ) -> HttpResponse[ApiResponseWorkflowRunStatusOut]: """ - Lightweight run status for polling. + Lightweight run status for progress polling. + + Returns only the volatile, frequently-changing fields: `status`, step + counts (`total_steps`, `finished_steps`), timestamps (`started_at`, + `completed_at`, `paused_at`, `pause_requested_at`), `usage_summary`, + `error`, and `has_export`. The definition snapshot is intentionally + omitted to keep response size small. + + Recommended polling pattern: call `GET /runs/{run_id}` once after + starting a run to load the immutable definition snapshot and initial + metadata, then poll this endpoint until `status` reaches a terminal value + (`completed`, `failed`, or `cancelled`). `has_export` becoming `true` + signals that a download is ready via `GET /runs/{run_id}/download`. - Returns only the volatile run state (status, step counts, timestamps, - usage_summary, error, has_export) — no graph snapshot. Use this for - progress polling; call `GET /runs/{run_id}` once to load the immutable - definition snapshot. + The transient `pausing` state is observable here: `status == "running"` + with `pause_requested_at` set means a pause is in progress but the + current wave has not yet finished draining. Parameters ---------- @@ -331,7 +373,19 @@ def cancel( request_options: typing.Optional[RequestOptions] = None, ) -> HttpResponse[ApiResponseWorkflowRunOut]: """ - Cancel a running workflow run. + Cancel an active workflow run. + + Immediately marks the run as `cancelled` and stops any further processing. + In-flight work at the current node may be abandoned mid-execution. The + operation is idempotent: cancelling an already-cancelled run returns the + run unchanged without error. + + Only runs in `pending`, `running`, or `paused` status can be cancelled. + Runs that have already reached a terminal state (`completed`, `failed`, + `cancelled`) return 409. + + Unlike `pause`, cancel is permanent — a cancelled run cannot be resumed. + Use `pause` if you intend to continue the run later. Parameters ---------- @@ -406,32 +460,43 @@ async def list( request_options: typing.Optional[RequestOptions] = None, ) -> AsyncHttpResponse[ApiCountedListResponseWorkflowRunListItem]: """ - List runs for a workflow. + List runs for a workflow, newest first by default. - Tiebreaker is always ``id ASC`` so offset/limit pagination is stable when - primary sort keys tie. ``status`` accepts comma-separated raw RunStatus - values; unknown values return 422. ``search`` matches the triggering - user's display name (full name, falling back to email). + Returns a counted, paginated list of runs for the specified workflow. + Each item includes the run's status, step progress (`total_steps`, + `finished_steps`), credit usage, and the actor who triggered it. + + **Status filter:** Pass `?status=completed,failed` to OR-match multiple + statuses. Values must be the raw lowercase RunStatus strings. Unknown values + or empty tokens (e.g. `a,,b`) return 422. + + **Pagination:** `pagination.total` reflects the filtered count. Sort + tiebreaks always append `id ASC` for stable offset/limit pagination. + + For aggregate statistics (pass rate, average duration) across all runs, + use `GET /runs/summary` instead. Parameters ---------- workflow_id : str offset : typing.Optional[int] + Zero-based pagination offset. limit : typing.Optional[int] + Maximum items to return (1–100). status : typing.Optional[str] - Comma-separated raw RunStatus values (e.g. `completed,failed`). Values are case-sensitive lowercase. Multiple values OR-match. Empty tokens (e.g. `a,,b`) and unknown values return 422. + Comma-separated raw RunStatus values (e.g. `completed,failed`). Values are case-sensitive lowercase: `pending`, `running`, `completed`, `failed`, `cancelled`, `paused`. Multiple values OR-match. Empty tokens (e.g. `a,,b`) and unknown values return 422. search : typing.Optional[str] - Case-insensitive search over triggering user's display name and email. + Case-insensitive search over the triggering user's display name (falls back to email). sort : typing.Optional[ListRunsRequestSort] - Sort field: created_at | started_at | completed_at | status. + Sort field: `created_at` | `started_at` | `completed_at` | `status`. Defaults to `created_at`. order : typing.Optional[ListRunsRequestOrder] - asc or desc. + `asc` or `desc`. Defaults to `desc`. workspace_id : typing.Optional[str] @@ -497,7 +562,20 @@ async def start( request_options: typing.Optional[RequestOptions] = None, ) -> AsyncHttpResponse[ApiResponseWorkflowRunOut]: """ - Start a workflow run. + Start a new execution of a workflow (202 Accepted). + + Enqueues the workflow for asynchronous execution and returns the newly + created run in `pending` or `running` status. The run progresses through + 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. + + 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. + + Returns 409 if the workspace is at its concurrent-run limit or another + run for this workflow is already active. Parameters ---------- @@ -560,11 +638,18 @@ async def get( request_options: typing.Optional[RequestOptions] = None, ) -> AsyncHttpResponse[ApiResponseWorkflowRunDetailOut]: """ - Fetch a workflow run by id. + Fetch full detail for a single workflow run. - Includes `definition_snapshot` — the graph/execution config captured - when the run started, returned raw (no config migrations applied) so - it reflects the workflow exactly as it existed for this run. + Returns all run fields plus `definition_snapshot` — the graph and + execution config captured at the moment the run started. The snapshot is + returned raw (no config migrations applied), so it faithfully represents + the workflow as it existed for this specific execution even if the + workflow definition has since been edited. + + This is the heaviest run endpoint. For progress polling, use the lighter + `GET /runs/{run_id}/status` which omits the snapshot. For aggregated + visual metrics, use `GET /runs/{run_id}/overview`. For the per-node step + log with audio playback URLs, use `GET /runs/{run_id}/steps`. Parameters ---------- @@ -629,12 +714,23 @@ async def status( request_options: typing.Optional[RequestOptions] = None, ) -> AsyncHttpResponse[ApiResponseWorkflowRunStatusOut]: """ - Lightweight run status for polling. + Lightweight run status for progress polling. + + Returns only the volatile, frequently-changing fields: `status`, step + counts (`total_steps`, `finished_steps`), timestamps (`started_at`, + `completed_at`, `paused_at`, `pause_requested_at`), `usage_summary`, + `error`, and `has_export`. The definition snapshot is intentionally + omitted to keep response size small. + + Recommended polling pattern: call `GET /runs/{run_id}` once after + starting a run to load the immutable definition snapshot and initial + metadata, then poll this endpoint until `status` reaches a terminal value + (`completed`, `failed`, or `cancelled`). `has_export` becoming `true` + signals that a download is ready via `GET /runs/{run_id}/download`. - Returns only the volatile run state (status, step counts, timestamps, - usage_summary, error, has_export) — no graph snapshot. Use this for - progress polling; call `GET /runs/{run_id}` once to load the immutable - definition snapshot. + The transient `pausing` state is observable here: `status == "running"` + with `pause_requested_at` set means a pause is in progress but the + current wave has not yet finished draining. Parameters ---------- @@ -699,7 +795,19 @@ async def cancel( request_options: typing.Optional[RequestOptions] = None, ) -> AsyncHttpResponse[ApiResponseWorkflowRunOut]: """ - Cancel a running workflow run. + Cancel an active workflow run. + + Immediately marks the run as `cancelled` and stops any further processing. + In-flight work at the current node may be abandoned mid-execution. The + operation is idempotent: cancelling an already-cancelled run returns the + run unchanged without error. + + Only runs in `pending`, `running`, or `paused` status can be cancelled. + Runs that have already reached a terminal state (`completed`, `failed`, + `cancelled`) return 409. + + Unlike `pause`, cancel is permanent — a cancelled run cannot be resumed. + Use `pause` if you intend to continue the run later. Parameters ---------- diff --git a/src/onepin/workspace/client.py b/src/onepin/workspace/client.py index ca4fd80..7feb888 100644 --- a/src/onepin/workspace/client.py +++ b/src/onepin/workspace/client.py @@ -27,15 +27,16 @@ def get_workspace_settings( self, ws_id: str, *, request_options: typing.Optional[RequestOptions] = None ) -> ApiResponseWorkspaceSettingsOut: """ - Return workspace-level settings (default_language, theme). + Return workspace-level settings for the specified workspace. - Settings are workspace-scoped: every member of the workspace sees the - same defaults. Per-user UI preferences (e.g. personal dark-mode) belong - in a future user_settings endpoint. + Currently exposes `default_language` (the locale used as the default for + new workflow nodes) and `theme` (the workspace's display color theme). + Settings are workspace-scoped: all members of the workspace share the + same values. Per-user preferences (e.g. personal dark mode) are outside + the scope of this endpoint. - Path-based authorization (via get_workspace_from_path_for_auth_context) prevents the - header-vs-path bypass class — the {ws_id} URL segment is the source of - truth for which workspace is being read. + If settings have not yet been explicitly configured for the workspace, + defaults are returned (and persisted) on first access. Parameters ---------- @@ -83,15 +84,16 @@ async def get_workspace_settings( self, ws_id: str, *, request_options: typing.Optional[RequestOptions] = None ) -> ApiResponseWorkspaceSettingsOut: """ - Return workspace-level settings (default_language, theme). + Return workspace-level settings for the specified workspace. - Settings are workspace-scoped: every member of the workspace sees the - same defaults. Per-user UI preferences (e.g. personal dark-mode) belong - in a future user_settings endpoint. + Currently exposes `default_language` (the locale used as the default for + new workflow nodes) and `theme` (the workspace's display color theme). + Settings are workspace-scoped: all members of the workspace share the + same values. Per-user preferences (e.g. personal dark mode) are outside + the scope of this endpoint. - Path-based authorization (via get_workspace_from_path_for_auth_context) prevents the - header-vs-path bypass class — the {ws_id} URL segment is the source of - truth for which workspace is being read. + If settings have not yet been explicitly configured for the workspace, + defaults are returned (and persisted) on first access. Parameters ---------- diff --git a/src/onepin/workspace/raw_client.py b/src/onepin/workspace/raw_client.py index cf7df87..205cb67 100644 --- a/src/onepin/workspace/raw_client.py +++ b/src/onepin/workspace/raw_client.py @@ -23,15 +23,16 @@ def get_workspace_settings( self, ws_id: str, *, request_options: typing.Optional[RequestOptions] = None ) -> HttpResponse[ApiResponseWorkspaceSettingsOut]: """ - Return workspace-level settings (default_language, theme). + Return workspace-level settings for the specified workspace. - Settings are workspace-scoped: every member of the workspace sees the - same defaults. Per-user UI preferences (e.g. personal dark-mode) belong - in a future user_settings endpoint. + Currently exposes `default_language` (the locale used as the default for + new workflow nodes) and `theme` (the workspace's display color theme). + Settings are workspace-scoped: all members of the workspace share the + same values. Per-user preferences (e.g. personal dark mode) are outside + the scope of this endpoint. - Path-based authorization (via get_workspace_from_path_for_auth_context) prevents the - header-vs-path bypass class — the {ws_id} URL segment is the source of - truth for which workspace is being read. + If settings have not yet been explicitly configured for the workspace, + defaults are returned (and persisted) on first access. Parameters ---------- @@ -89,15 +90,16 @@ async def get_workspace_settings( self, ws_id: str, *, request_options: typing.Optional[RequestOptions] = None ) -> AsyncHttpResponse[ApiResponseWorkspaceSettingsOut]: """ - Return workspace-level settings (default_language, theme). + Return workspace-level settings for the specified workspace. - Settings are workspace-scoped: every member of the workspace sees the - same defaults. Per-user UI preferences (e.g. personal dark-mode) belong - in a future user_settings endpoint. + Currently exposes `default_language` (the locale used as the default for + new workflow nodes) and `theme` (the workspace's display color theme). + Settings are workspace-scoped: all members of the workspace share the + same values. Per-user preferences (e.g. personal dark mode) are outside + the scope of this endpoint. - Path-based authorization (via get_workspace_from_path_for_auth_context) prevents the - header-vs-path bypass class — the {ws_id} URL segment is the source of - truth for which workspace is being read. + If settings have not yet been explicitly configured for the workspace, + defaults are returned (and persisted) on first access. Parameters ---------- diff --git a/src/onepin/workspace_aggregates/__init__.py b/src/onepin/workspace_aggregates/__init__.py deleted file mode 100644 index 5cde020..0000000 --- a/src/onepin/workspace_aggregates/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -# isort: skip_file - diff --git a/src/onepin/workspace_aggregates/client.py b/src/onepin/workspace_aggregates/client.py deleted file mode 100644 index 7c6ca8f..0000000 --- a/src/onepin/workspace_aggregates/client.py +++ /dev/null @@ -1,230 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper -from ..core.request_options import RequestOptions -from ..types.api_response_workspace_runs_stats_out import ApiResponseWorkspaceRunsStatsOut -from ..types.api_response_workspace_workflows_stats_out import ApiResponseWorkspaceWorkflowsStatsOut -from .raw_client import AsyncRawWorkspaceAggregatesClient, RawWorkspaceAggregatesClient - - -class WorkspaceAggregatesClient: - def __init__(self, *, client_wrapper: SyncClientWrapper): - self._raw_client = RawWorkspaceAggregatesClient(client_wrapper=client_wrapper) - - @property - def with_raw_response(self) -> RawWorkspaceAggregatesClient: - """ - Retrieves a raw implementation of this client that returns raw responses. - - Returns - ------- - RawWorkspaceAggregatesClient - """ - return self._raw_client - - def workspace_runs_stats( - self, - *, - from_: typing.Optional[dt.datetime] = None, - to: typing.Optional[dt.datetime] = None, - workspace_id: typing.Optional[str] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> ApiResponseWorkspaceRunsStatsOut: - """ - Aggregate workflow-run counts grouped by raw RunStatus across the workspace. - - Parameters - ---------- - from_ : typing.Optional[dt.datetime] - Filter runs by created_at >= this ISO datetime. - - to : typing.Optional[dt.datetime] - Filter runs by created_at <= this ISO datetime. - - workspace_id : typing.Optional[str] - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ApiResponseWorkspaceRunsStatsOut - Successful Response - - Examples - -------- - from onepin import OnePinClient - - client = OnePinClient( - token="YOUR_TOKEN", - ) - client.workspace_aggregates.workspace_runs_stats() - """ - _response = self._raw_client.workspace_runs_stats( - from_=from_, to=to, workspace_id=workspace_id, request_options=request_options - ) - return _response.data - - def workspace_workflows_stats( - self, - *, - from_: typing.Optional[dt.datetime] = None, - to: typing.Optional[dt.datetime] = None, - workspace_id: typing.Optional[str] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> ApiResponseWorkspaceWorkflowsStatsOut: - """ - Aggregate workflow counts grouped by derived WorkflowListStatus across the workspace. - - Parameters - ---------- - from_ : typing.Optional[dt.datetime] - Filter workflows by created_at >= this ISO datetime. - - to : typing.Optional[dt.datetime] - Filter workflows by created_at <= this ISO datetime. - - workspace_id : typing.Optional[str] - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ApiResponseWorkspaceWorkflowsStatsOut - Successful Response - - Examples - -------- - from onepin import OnePinClient - - client = OnePinClient( - token="YOUR_TOKEN", - ) - client.workspace_aggregates.workspace_workflows_stats() - """ - _response = self._raw_client.workspace_workflows_stats( - from_=from_, to=to, workspace_id=workspace_id, request_options=request_options - ) - return _response.data - - -class AsyncWorkspaceAggregatesClient: - def __init__(self, *, client_wrapper: AsyncClientWrapper): - self._raw_client = AsyncRawWorkspaceAggregatesClient(client_wrapper=client_wrapper) - - @property - def with_raw_response(self) -> AsyncRawWorkspaceAggregatesClient: - """ - Retrieves a raw implementation of this client that returns raw responses. - - Returns - ------- - AsyncRawWorkspaceAggregatesClient - """ - return self._raw_client - - async def workspace_runs_stats( - self, - *, - from_: typing.Optional[dt.datetime] = None, - to: typing.Optional[dt.datetime] = None, - workspace_id: typing.Optional[str] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> ApiResponseWorkspaceRunsStatsOut: - """ - Aggregate workflow-run counts grouped by raw RunStatus across the workspace. - - Parameters - ---------- - from_ : typing.Optional[dt.datetime] - Filter runs by created_at >= this ISO datetime. - - to : typing.Optional[dt.datetime] - Filter runs by created_at <= this ISO datetime. - - workspace_id : typing.Optional[str] - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ApiResponseWorkspaceRunsStatsOut - Successful Response - - Examples - -------- - import asyncio - - from onepin import AsyncOnePinClient - - client = AsyncOnePinClient( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.workspace_aggregates.workspace_runs_stats() - - - asyncio.run(main()) - """ - _response = await self._raw_client.workspace_runs_stats( - from_=from_, to=to, workspace_id=workspace_id, request_options=request_options - ) - return _response.data - - async def workspace_workflows_stats( - self, - *, - from_: typing.Optional[dt.datetime] = None, - to: typing.Optional[dt.datetime] = None, - workspace_id: typing.Optional[str] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> ApiResponseWorkspaceWorkflowsStatsOut: - """ - Aggregate workflow counts grouped by derived WorkflowListStatus across the workspace. - - Parameters - ---------- - from_ : typing.Optional[dt.datetime] - Filter workflows by created_at >= this ISO datetime. - - to : typing.Optional[dt.datetime] - Filter workflows by created_at <= this ISO datetime. - - workspace_id : typing.Optional[str] - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ApiResponseWorkspaceWorkflowsStatsOut - Successful Response - - Examples - -------- - import asyncio - - from onepin import AsyncOnePinClient - - client = AsyncOnePinClient( - token="YOUR_TOKEN", - ) - - - async def main() -> None: - await client.workspace_aggregates.workspace_workflows_stats() - - - asyncio.run(main()) - """ - _response = await self._raw_client.workspace_workflows_stats( - from_=from_, to=to, workspace_id=workspace_id, request_options=request_options - ) - return _response.data diff --git a/src/onepin/workspace_aggregates/raw_client.py b/src/onepin/workspace_aggregates/raw_client.py deleted file mode 100644 index 7b34660..0000000 --- a/src/onepin/workspace_aggregates/raw_client.py +++ /dev/null @@ -1,311 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing -from json.decoder import JSONDecodeError - -from ..core.api_error import ApiError -from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper -from ..core.datetime_utils import serialize_datetime -from ..core.http_response import AsyncHttpResponse, HttpResponse -from ..core.parse_error import ParsingError -from ..core.pydantic_utilities import parse_obj_as -from ..core.request_options import RequestOptions -from ..errors.unprocessable_entity_error import UnprocessableEntityError -from ..types.api_response_workspace_runs_stats_out import ApiResponseWorkspaceRunsStatsOut -from ..types.api_response_workspace_workflows_stats_out import ApiResponseWorkspaceWorkflowsStatsOut -from pydantic import ValidationError - - -class RawWorkspaceAggregatesClient: - def __init__(self, *, client_wrapper: SyncClientWrapper): - self._client_wrapper = client_wrapper - - def workspace_runs_stats( - self, - *, - from_: typing.Optional[dt.datetime] = None, - to: typing.Optional[dt.datetime] = None, - workspace_id: typing.Optional[str] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> HttpResponse[ApiResponseWorkspaceRunsStatsOut]: - """ - Aggregate workflow-run counts grouped by raw RunStatus across the workspace. - - Parameters - ---------- - from_ : typing.Optional[dt.datetime] - Filter runs by created_at >= this ISO datetime. - - to : typing.Optional[dt.datetime] - Filter runs by created_at <= this ISO datetime. - - workspace_id : typing.Optional[str] - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - HttpResponse[ApiResponseWorkspaceRunsStatsOut] - Successful Response - """ - _response = self._client_wrapper.httpx_client.request( - "api/v1/workspace/runs/stats", - method="GET", - params={ - "from": serialize_datetime(from_) if from_ is not None else None, - "to": serialize_datetime(to) if to is not None else None, - }, - 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( - ApiResponseWorkspaceRunsStatsOut, - parse_obj_as( - type_=ApiResponseWorkspaceRunsStatsOut, # 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 workspace_workflows_stats( - self, - *, - from_: typing.Optional[dt.datetime] = None, - to: typing.Optional[dt.datetime] = None, - workspace_id: typing.Optional[str] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> HttpResponse[ApiResponseWorkspaceWorkflowsStatsOut]: - """ - Aggregate workflow counts grouped by derived WorkflowListStatus across the workspace. - - Parameters - ---------- - from_ : typing.Optional[dt.datetime] - Filter workflows by created_at >= this ISO datetime. - - to : typing.Optional[dt.datetime] - Filter workflows by created_at <= this ISO datetime. - - workspace_id : typing.Optional[str] - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - HttpResponse[ApiResponseWorkspaceWorkflowsStatsOut] - Successful Response - """ - _response = self._client_wrapper.httpx_client.request( - "api/v1/workspace/workflows/stats", - method="GET", - params={ - "from": serialize_datetime(from_) if from_ is not None else None, - "to": serialize_datetime(to) if to is not None else None, - }, - 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( - ApiResponseWorkspaceWorkflowsStatsOut, - parse_obj_as( - type_=ApiResponseWorkspaceWorkflowsStatsOut, # 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) - - -class AsyncRawWorkspaceAggregatesClient: - def __init__(self, *, client_wrapper: AsyncClientWrapper): - self._client_wrapper = client_wrapper - - async def workspace_runs_stats( - self, - *, - from_: typing.Optional[dt.datetime] = None, - to: typing.Optional[dt.datetime] = None, - workspace_id: typing.Optional[str] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> AsyncHttpResponse[ApiResponseWorkspaceRunsStatsOut]: - """ - Aggregate workflow-run counts grouped by raw RunStatus across the workspace. - - Parameters - ---------- - from_ : typing.Optional[dt.datetime] - Filter runs by created_at >= this ISO datetime. - - to : typing.Optional[dt.datetime] - Filter runs by created_at <= this ISO datetime. - - workspace_id : typing.Optional[str] - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AsyncHttpResponse[ApiResponseWorkspaceRunsStatsOut] - Successful Response - """ - _response = await self._client_wrapper.httpx_client.request( - "api/v1/workspace/runs/stats", - method="GET", - params={ - "from": serialize_datetime(from_) if from_ is not None else None, - "to": serialize_datetime(to) if to is not None else None, - }, - 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( - ApiResponseWorkspaceRunsStatsOut, - parse_obj_as( - type_=ApiResponseWorkspaceRunsStatsOut, # 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 workspace_workflows_stats( - self, - *, - from_: typing.Optional[dt.datetime] = None, - to: typing.Optional[dt.datetime] = None, - workspace_id: typing.Optional[str] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> AsyncHttpResponse[ApiResponseWorkspaceWorkflowsStatsOut]: - """ - Aggregate workflow counts grouped by derived WorkflowListStatus across the workspace. - - Parameters - ---------- - from_ : typing.Optional[dt.datetime] - Filter workflows by created_at >= this ISO datetime. - - to : typing.Optional[dt.datetime] - Filter workflows by created_at <= this ISO datetime. - - workspace_id : typing.Optional[str] - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AsyncHttpResponse[ApiResponseWorkspaceWorkflowsStatsOut] - Successful Response - """ - _response = await self._client_wrapper.httpx_client.request( - "api/v1/workspace/workflows/stats", - method="GET", - params={ - "from": serialize_datetime(from_) if from_ is not None else None, - "to": serialize_datetime(to) if to is not None else None, - }, - 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( - ApiResponseWorkspaceWorkflowsStatsOut, - parse_obj_as( - type_=ApiResponseWorkspaceWorkflowsStatsOut, # 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) diff --git a/src/onepin/workspace_members/client.py b/src/onepin/workspace_members/client.py index be5cf25..77c4e22 100644 --- a/src/onepin/workspace_members/client.py +++ b/src/onepin/workspace_members/client.py @@ -35,6 +35,20 @@ def list_members( """ List active members and pending invites for a workspace. + Returns a unified list combining confirmed members (status `active`) and + outstanding invites that have not yet been accepted or revoked (status + `invited`). Pending invites appear with `user_id: null` and only the + `email` and `role` fields populated. + + The list is sorted: the requesting user appears first, then admins by + join date, then other members by join date. No pagination — the full + roster is returned in a single response. + + Roles: + - `admin`: can manage members, invites, workspace settings, and all content. + - `editor`: can create, edit, and run workflows; cannot manage members. + - `viewer`: read-only access to workspace content and run history. + Parameters ---------- ws_id : str @@ -65,18 +79,34 @@ def create_invite( self, ws_id: str, *, email: str, role: WorkspaceRole, request_options: typing.Optional[RequestOptions] = None ) -> ApiResponseWorkspaceInviteOut: """ - Invite a user to the workspace via email. + Invite a user to the workspace by email. Admin only. + + Creates a pending invite and sends an invitation email to the specified + address. The invitee does not need to have an existing account — they can + sign up after receiving the invite. The invite includes a role + (`admin`, `editor`, or `viewer`) that the invitee will receive upon + accepting. + + Invites expire after 14 days. Only one pending invite per email address + 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. - POD-301: gated by `seats` plan limit on the workspace owner's plan. - Active members + pending invites both count against the cap. + 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}`. Parameters ---------- ws_id : str email : str + Email address to invite. Normalized to lowercase. Returns 409 if this address is already an active member or has a pending invite. role : WorkspaceRole + Role to grant when the invite is accepted: `admin`, `editor`, or `viewer`. request_options : typing.Optional[RequestOptions] Request-specific configuration. @@ -106,7 +136,18 @@ def remove_member( self, ws_id: str, member_id: str, *, request_options: typing.Optional[RequestOptions] = None ) -> ApiResponseDict: """ - Remove an active member. Admin only. + Remove an active member from the workspace. Admin only. + + The removed member immediately loses access to all workspace resources. + They receive an email notification informing them they have been removed. + + Two protections prevent accidental lockouts: + - The workspace owner cannot be removed. + - The last remaining admin cannot be removed (returns 409). + + Removing a member does not affect their account or other workspaces. To + block an invited but not-yet-accepted user instead, revoke the invite via + `DELETE /workspaces/{ws_id}/invites/{invite_id}`. Parameters ---------- @@ -146,7 +187,18 @@ def update_member_role( request_options: typing.Optional[RequestOptions] = None, ) -> ApiResponseDict: """ - Change a member's role. Admin only. + Change a workspace member's role. Admin only. + + Updates the role of an active member to `admin`, `editor`, or `viewer`. + The operation is idempotent — setting a member to their current role + succeeds silently (no error, no duplicate email notification). + + Two protections prevent accidental lockouts: + - The workspace owner's role cannot be changed. + - The last remaining admin cannot be demoted (returns 409). + + When the role actually changes, the affected member receives an email + notification describing their new permissions. Parameters ---------- @@ -155,6 +207,7 @@ def update_member_role( member_id : str role : WorkspaceRole + New role to assign: `admin`, `editor`, or `viewer`. request_options : typing.Optional[RequestOptions] Request-specific configuration. @@ -184,7 +237,15 @@ def revoke_invite( self, ws_id: str, invite_id: str, *, request_options: typing.Optional[RequestOptions] = None ) -> ApiResponseDict: """ - Revoke a pending invite. Admin only. + Cancel a pending invite so the invitee can no longer accept it. Admin only. + + The invite token is invalidated immediately. If the invitee attempts to + accept after revocation, they receive a 410 Gone. The invite is removed + from the pending list returned by `GET /workspaces/{ws_id}/members`. + + Revoking an invite that is already accepted, revoked, or expired returns + 404. To remove an already-accepted member, use + `DELETE /workspaces/{ws_id}/members/{member_id}`. Parameters ---------- @@ -224,7 +285,12 @@ def update_invite_role( request_options: typing.Optional[RequestOptions] = None, ) -> ApiResponseWorkspaceInviteOut: """ - Update role on a pending invite. Admin only. + Change the role on a pending (not yet accepted) invite. Admin only. + + Updates the role the invitee will receive when they accept. Only pending + invites can be updated — attempting to update an accepted, revoked, or + expired invite returns 409. The invitee is not notified of the role + change; the updated role takes effect when they accept. Parameters ---------- @@ -233,6 +299,7 @@ def update_invite_role( invite_id : str role : WorkspaceRole + New role to assign: `admin`, `editor`, or `viewer`. request_options : typing.Optional[RequestOptions] Request-specific configuration. @@ -260,10 +327,26 @@ def update_invite_role( def accept_invite(self, token: str, *, request_options: typing.Optional[RequestOptions] = None) -> ApiResponseDict: """ - Accept a workspace invite. Authenticated; validates email match. + Accept a workspace invite using the token from the invitation email. + + The `token` path parameter comes from the invitation link sent to the + invitee's email. The caller must be authenticated and their verified email + address must match the address the invite was sent to (403 if it does not). + + On success the caller is added to the workspace with the role specified in + the invite, and `workspace_id` is returned so the caller can immediately + begin using that workspace. If the caller is already a member of the + workspace (e.g. accepted via a different device), the accept is idempotent + and returns the same `workspace_id`. + + Error cases (all return 410 Gone): + - Invite already accepted. + - Invite was revoked by an admin. + - Invite has expired (14-day TTL from creation). - POD-301: re-checks `seats` against owner's plan at accept time — owner may - have downgraded since invite sent. + 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 ---------- @@ -313,6 +396,20 @@ async def list_members( """ List active members and pending invites for a workspace. + Returns a unified list combining confirmed members (status `active`) and + outstanding invites that have not yet been accepted or revoked (status + `invited`). Pending invites appear with `user_id: null` and only the + `email` and `role` fields populated. + + The list is sorted: the requesting user appears first, then admins by + join date, then other members by join date. No pagination — the full + roster is returned in a single response. + + Roles: + - `admin`: can manage members, invites, workspace settings, and all content. + - `editor`: can create, edit, and run workflows; cannot manage members. + - `viewer`: read-only access to workspace content and run history. + Parameters ---------- ws_id : str @@ -351,18 +448,34 @@ async def create_invite( self, ws_id: str, *, email: str, role: WorkspaceRole, request_options: typing.Optional[RequestOptions] = None ) -> ApiResponseWorkspaceInviteOut: """ - Invite a user to the workspace via email. + Invite a user to the workspace by email. Admin only. + + Creates a pending invite and sends an invitation email to the specified + address. The invitee does not need to have an existing account — they can + sign up after receiving the invite. The invite includes a role + (`admin`, `editor`, or `viewer`) that the invitee will receive upon + accepting. + + Invites expire after 14 days. Only one pending invite per email address + 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. - POD-301: gated by `seats` plan limit on the workspace owner's plan. - Active members + pending invites both count against the cap. + 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}`. Parameters ---------- ws_id : str email : str + Email address to invite. Normalized to lowercase. Returns 409 if this address is already an active member or has a pending invite. role : WorkspaceRole + Role to grant when the invite is accepted: `admin`, `editor`, or `viewer`. request_options : typing.Optional[RequestOptions] Request-specific configuration. @@ -400,7 +513,18 @@ async def remove_member( self, ws_id: str, member_id: str, *, request_options: typing.Optional[RequestOptions] = None ) -> ApiResponseDict: """ - Remove an active member. Admin only. + Remove an active member from the workspace. Admin only. + + The removed member immediately loses access to all workspace resources. + They receive an email notification informing them they have been removed. + + Two protections prevent accidental lockouts: + - The workspace owner cannot be removed. + - The last remaining admin cannot be removed (returns 409). + + Removing a member does not affect their account or other workspaces. To + block an invited but not-yet-accepted user instead, revoke the invite via + `DELETE /workspaces/{ws_id}/invites/{invite_id}`. Parameters ---------- @@ -448,7 +572,18 @@ async def update_member_role( request_options: typing.Optional[RequestOptions] = None, ) -> ApiResponseDict: """ - Change a member's role. Admin only. + Change a workspace member's role. Admin only. + + Updates the role of an active member to `admin`, `editor`, or `viewer`. + The operation is idempotent — setting a member to their current role + succeeds silently (no error, no duplicate email notification). + + Two protections prevent accidental lockouts: + - The workspace owner's role cannot be changed. + - The last remaining admin cannot be demoted (returns 409). + + When the role actually changes, the affected member receives an email + notification describing their new permissions. Parameters ---------- @@ -457,6 +592,7 @@ async def update_member_role( member_id : str role : WorkspaceRole + New role to assign: `admin`, `editor`, or `viewer`. request_options : typing.Optional[RequestOptions] Request-specific configuration. @@ -496,7 +632,15 @@ async def revoke_invite( self, ws_id: str, invite_id: str, *, request_options: typing.Optional[RequestOptions] = None ) -> ApiResponseDict: """ - Revoke a pending invite. Admin only. + Cancel a pending invite so the invitee can no longer accept it. Admin only. + + The invite token is invalidated immediately. If the invitee attempts to + accept after revocation, they receive a 410 Gone. The invite is removed + from the pending list returned by `GET /workspaces/{ws_id}/members`. + + Revoking an invite that is already accepted, revoked, or expired returns + 404. To remove an already-accepted member, use + `DELETE /workspaces/{ws_id}/members/{member_id}`. Parameters ---------- @@ -544,7 +688,12 @@ async def update_invite_role( request_options: typing.Optional[RequestOptions] = None, ) -> ApiResponseWorkspaceInviteOut: """ - Update role on a pending invite. Admin only. + Change the role on a pending (not yet accepted) invite. Admin only. + + Updates the role the invitee will receive when they accept. Only pending + invites can be updated — attempting to update an accepted, revoked, or + expired invite returns 409. The invitee is not notified of the role + change; the updated role takes effect when they accept. Parameters ---------- @@ -553,6 +702,7 @@ async def update_invite_role( invite_id : str role : WorkspaceRole + New role to assign: `admin`, `editor`, or `viewer`. request_options : typing.Optional[RequestOptions] Request-specific configuration. @@ -592,10 +742,26 @@ async def accept_invite( self, token: str, *, request_options: typing.Optional[RequestOptions] = None ) -> ApiResponseDict: """ - Accept a workspace invite. Authenticated; validates email match. + Accept a workspace invite using the token from the invitation email. + + The `token` path parameter comes from the invitation link sent to the + invitee's email. The caller must be authenticated and their verified email + address must match the address the invite was sent to (403 if it does not). + + On success the caller is added to the workspace with the role specified in + the invite, and `workspace_id` is returned so the caller can immediately + begin using that workspace. If the caller is already a member of the + workspace (e.g. accepted via a different device), the accept is idempotent + and returns the same `workspace_id`. + + Error cases (all return 410 Gone): + - Invite already accepted. + - Invite was revoked by an admin. + - Invite has expired (14-day TTL from creation). - POD-301: re-checks `seats` against owner's plan at accept time — owner may - have downgraded since invite sent. + 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 ---------- diff --git a/src/onepin/workspace_members/raw_client.py b/src/onepin/workspace_members/raw_client.py index 1a8198a..4149352 100644 --- a/src/onepin/workspace_members/raw_client.py +++ b/src/onepin/workspace_members/raw_client.py @@ -31,6 +31,20 @@ def list_members( """ List active members and pending invites for a workspace. + Returns a unified list combining confirmed members (status `active`) and + outstanding invites that have not yet been accepted or revoked (status + `invited`). Pending invites appear with `user_id: null` and only the + `email` and `role` fields populated. + + The list is sorted: the requesting user appears first, then admins by + join date, then other members by join date. No pagination — the full + roster is returned in a single response. + + Roles: + - `admin`: can manage members, invites, workspace settings, and all content. + - `editor`: can create, edit, and run workflows; cannot manage members. + - `viewer`: read-only access to workspace content and run history. + Parameters ---------- ws_id : str @@ -82,18 +96,34 @@ def create_invite( self, ws_id: str, *, email: str, role: WorkspaceRole, request_options: typing.Optional[RequestOptions] = None ) -> HttpResponse[ApiResponseWorkspaceInviteOut]: """ - Invite a user to the workspace via email. + Invite a user to the workspace by email. Admin only. + + Creates a pending invite and sends an invitation email to the specified + address. The invitee does not need to have an existing account — they can + sign up after receiving the invite. The invite includes a role + (`admin`, `editor`, or `viewer`) that the invitee will receive upon + accepting. - POD-301: gated by `seats` plan limit on the workspace owner's plan. - Active members + pending invites both count against the cap. + Invites expire after 14 days. Only one pending invite per email address + 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}`. Parameters ---------- ws_id : str email : str + Email address to invite. Normalized to lowercase. Returns 409 if this address is already an active member or has a pending invite. role : WorkspaceRole + Role to grant when the invite is accepted: `admin`, `editor`, or `viewer`. request_options : typing.Optional[RequestOptions] Request-specific configuration. @@ -150,7 +180,18 @@ def remove_member( self, ws_id: str, member_id: str, *, request_options: typing.Optional[RequestOptions] = None ) -> HttpResponse[ApiResponseDict]: """ - Remove an active member. Admin only. + Remove an active member from the workspace. Admin only. + + The removed member immediately loses access to all workspace resources. + They receive an email notification informing them they have been removed. + + Two protections prevent accidental lockouts: + - The workspace owner cannot be removed. + - The last remaining admin cannot be removed (returns 409). + + Removing a member does not affect their account or other workspaces. To + block an invited but not-yet-accepted user instead, revoke the invite via + `DELETE /workspaces/{ws_id}/invites/{invite_id}`. Parameters ---------- @@ -210,7 +251,18 @@ def update_member_role( request_options: typing.Optional[RequestOptions] = None, ) -> HttpResponse[ApiResponseDict]: """ - Change a member's role. Admin only. + Change a workspace member's role. Admin only. + + Updates the role of an active member to `admin`, `editor`, or `viewer`. + The operation is idempotent — setting a member to their current role + succeeds silently (no error, no duplicate email notification). + + Two protections prevent accidental lockouts: + - The workspace owner's role cannot be changed. + - The last remaining admin cannot be demoted (returns 409). + + When the role actually changes, the affected member receives an email + notification describing their new permissions. Parameters ---------- @@ -219,6 +271,7 @@ def update_member_role( member_id : str role : WorkspaceRole + New role to assign: `admin`, `editor`, or `viewer`. request_options : typing.Optional[RequestOptions] Request-specific configuration. @@ -274,7 +327,15 @@ def revoke_invite( self, ws_id: str, invite_id: str, *, request_options: typing.Optional[RequestOptions] = None ) -> HttpResponse[ApiResponseDict]: """ - Revoke a pending invite. Admin only. + Cancel a pending invite so the invitee can no longer accept it. Admin only. + + The invite token is invalidated immediately. If the invitee attempts to + accept after revocation, they receive a 410 Gone. The invite is removed + from the pending list returned by `GET /workspaces/{ws_id}/members`. + + Revoking an invite that is already accepted, revoked, or expired returns + 404. To remove an already-accepted member, use + `DELETE /workspaces/{ws_id}/members/{member_id}`. Parameters ---------- @@ -334,7 +395,12 @@ def update_invite_role( request_options: typing.Optional[RequestOptions] = None, ) -> HttpResponse[ApiResponseWorkspaceInviteOut]: """ - Update role on a pending invite. Admin only. + Change the role on a pending (not yet accepted) invite. Admin only. + + Updates the role the invitee will receive when they accept. Only pending + invites can be updated — attempting to update an accepted, revoked, or + expired invite returns 409. The invitee is not notified of the role + change; the updated role takes effect when they accept. Parameters ---------- @@ -343,6 +409,7 @@ def update_invite_role( invite_id : str role : WorkspaceRole + New role to assign: `admin`, `editor`, or `viewer`. request_options : typing.Optional[RequestOptions] Request-specific configuration. @@ -398,10 +465,26 @@ def accept_invite( self, token: str, *, request_options: typing.Optional[RequestOptions] = None ) -> HttpResponse[ApiResponseDict]: """ - Accept a workspace invite. Authenticated; validates email match. + Accept a workspace invite using the token from the invitation email. + + The `token` path parameter comes from the invitation link sent to the + invitee's email. The caller must be authenticated and their verified email + address must match the address the invite was sent to (403 if it does not). + + On success the caller is added to the workspace with the role specified in + the invite, and `workspace_id` is returned so the caller can immediately + begin using that workspace. If the caller is already a member of the + workspace (e.g. accepted via a different device), the accept is idempotent + and returns the same `workspace_id`. - POD-301: re-checks `seats` against owner's plan at accept time — owner may - have downgraded since invite sent. + 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. Parameters ---------- @@ -461,6 +544,20 @@ async def list_members( """ List active members and pending invites for a workspace. + Returns a unified list combining confirmed members (status `active`) and + outstanding invites that have not yet been accepted or revoked (status + `invited`). Pending invites appear with `user_id: null` and only the + `email` and `role` fields populated. + + The list is sorted: the requesting user appears first, then admins by + join date, then other members by join date. No pagination — the full + roster is returned in a single response. + + Roles: + - `admin`: can manage members, invites, workspace settings, and all content. + - `editor`: can create, edit, and run workflows; cannot manage members. + - `viewer`: read-only access to workspace content and run history. + Parameters ---------- ws_id : str @@ -512,18 +609,34 @@ async def create_invite( self, ws_id: str, *, email: str, role: WorkspaceRole, request_options: typing.Optional[RequestOptions] = None ) -> AsyncHttpResponse[ApiResponseWorkspaceInviteOut]: """ - Invite a user to the workspace via email. + Invite a user to the workspace by email. Admin only. - POD-301: gated by `seats` plan limit on the workspace owner's plan. - Active members + pending invites both count against the cap. + Creates a pending invite and sends an invitation email to the specified + address. The invitee does not need to have an existing account — they can + sign up after receiving the invite. The invite includes a role + (`admin`, `editor`, or `viewer`) that the invitee will receive upon + accepting. + + Invites expire after 14 days. Only one pending invite per email address + 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}`. Parameters ---------- ws_id : str email : str + Email address to invite. Normalized to lowercase. Returns 409 if this address is already an active member or has a pending invite. role : WorkspaceRole + Role to grant when the invite is accepted: `admin`, `editor`, or `viewer`. request_options : typing.Optional[RequestOptions] Request-specific configuration. @@ -580,7 +693,18 @@ async def remove_member( self, ws_id: str, member_id: str, *, request_options: typing.Optional[RequestOptions] = None ) -> AsyncHttpResponse[ApiResponseDict]: """ - Remove an active member. Admin only. + Remove an active member from the workspace. Admin only. + + The removed member immediately loses access to all workspace resources. + They receive an email notification informing them they have been removed. + + Two protections prevent accidental lockouts: + - The workspace owner cannot be removed. + - The last remaining admin cannot be removed (returns 409). + + Removing a member does not affect their account or other workspaces. To + block an invited but not-yet-accepted user instead, revoke the invite via + `DELETE /workspaces/{ws_id}/invites/{invite_id}`. Parameters ---------- @@ -640,7 +764,18 @@ async def update_member_role( request_options: typing.Optional[RequestOptions] = None, ) -> AsyncHttpResponse[ApiResponseDict]: """ - Change a member's role. Admin only. + Change a workspace member's role. Admin only. + + Updates the role of an active member to `admin`, `editor`, or `viewer`. + The operation is idempotent — setting a member to their current role + succeeds silently (no error, no duplicate email notification). + + Two protections prevent accidental lockouts: + - The workspace owner's role cannot be changed. + - The last remaining admin cannot be demoted (returns 409). + + When the role actually changes, the affected member receives an email + notification describing their new permissions. Parameters ---------- @@ -649,6 +784,7 @@ async def update_member_role( member_id : str role : WorkspaceRole + New role to assign: `admin`, `editor`, or `viewer`. request_options : typing.Optional[RequestOptions] Request-specific configuration. @@ -704,7 +840,15 @@ async def revoke_invite( self, ws_id: str, invite_id: str, *, request_options: typing.Optional[RequestOptions] = None ) -> AsyncHttpResponse[ApiResponseDict]: """ - Revoke a pending invite. Admin only. + Cancel a pending invite so the invitee can no longer accept it. Admin only. + + The invite token is invalidated immediately. If the invitee attempts to + accept after revocation, they receive a 410 Gone. The invite is removed + from the pending list returned by `GET /workspaces/{ws_id}/members`. + + Revoking an invite that is already accepted, revoked, or expired returns + 404. To remove an already-accepted member, use + `DELETE /workspaces/{ws_id}/members/{member_id}`. Parameters ---------- @@ -764,7 +908,12 @@ async def update_invite_role( request_options: typing.Optional[RequestOptions] = None, ) -> AsyncHttpResponse[ApiResponseWorkspaceInviteOut]: """ - Update role on a pending invite. Admin only. + Change the role on a pending (not yet accepted) invite. Admin only. + + Updates the role the invitee will receive when they accept. Only pending + invites can be updated — attempting to update an accepted, revoked, or + expired invite returns 409. The invitee is not notified of the role + change; the updated role takes effect when they accept. Parameters ---------- @@ -773,6 +922,7 @@ async def update_invite_role( invite_id : str role : WorkspaceRole + New role to assign: `admin`, `editor`, or `viewer`. request_options : typing.Optional[RequestOptions] Request-specific configuration. @@ -828,10 +978,26 @@ async def accept_invite( self, token: str, *, request_options: typing.Optional[RequestOptions] = None ) -> AsyncHttpResponse[ApiResponseDict]: """ - Accept a workspace invite. Authenticated; validates email match. - - POD-301: re-checks `seats` against owner's plan at accept time — owner may - have downgraded since invite sent. + Accept a workspace invite using the token from the invitation email. + + The `token` path parameter comes from the invitation link sent to the + invitee's email. The caller must be authenticated and their verified email + address must match the address the invite was sent to (403 if it does not). + + On success the caller is added to the workspace with the role specified in + the invite, and `workspace_id` is returned so the caller can immediately + begin using that workspace. If the caller is already a member of the + workspace (e.g. accepted via a different device), the accept is idempotent + and returns the same `workspace_id`. + + 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. Parameters ---------- diff --git a/src/onepin/workspaces/client.py b/src/onepin/workspaces/client.py index 2051bce..378c1b4 100644 --- a/src/onepin/workspaces/client.py +++ b/src/onepin/workspaces/client.py @@ -37,7 +37,12 @@ def list_workspaces( request_options: typing.Optional[RequestOptions] = None, ) -> ApiListResponseWorkspaceOut: """ - List workspaces the current user is a member of. + List all workspaces the current user is a member of. + + Returns workspaces where the caller has any role (admin, editor, or + viewer), including workspaces they own and workspaces they joined via + invite. Results are paginated; omits soft-deleted workspaces and the + internal system workspace. Parameters ---------- @@ -76,17 +81,30 @@ def create_workspace( """ Create a new workspace owned by the current user. - POD-301: gated by `workspaces_per_owner` plan limit. Free=1, Creator=1, - Studio=2, Enterprise=bespoke. Owner soft-deletes don't free up quota until - purge — keeps the gate honest against rapid create/delete cycles. + Workspaces are the top-level container for all resources (workflows, + voices, dictionary entries, members). Every resource is scoped to exactly + one workspace via the `X-Workspace-Id` header on subsequent requests. + + The authenticated user becomes the workspace owner and is automatically + added as an `admin` member. An optional `slug` (1–50 characters, + lowercase kebab-case) can be supplied for a human-readable workspace + identifier; if omitted, one is auto-generated from `name`. Returns 409 + if the slug is already taken, 422 if the slug format is invalid or uses + a reserved word. + + The number of workspaces a user may own is plan-gated. Attempting to + exceed the limit returns 402. Parameters ---------- name : str + Human-readable workspace name (1–200 characters, non-blank). slug : typing.Optional[str] + Optional URL-safe identifier (lowercase kebab-case, 1–50 characters). Auto-generated from `name` if omitted. Returns 409 if taken, 422 if invalid or reserved. color_idx : typing.Optional[int] + Index into the workspace color palette (0–6). request_options : typing.Optional[RequestOptions] Request-specific configuration. @@ -120,17 +138,23 @@ def slug_available( request_options: typing.Optional[RequestOptions] = None, ) -> ApiResponseSlugAvailabilityOut: """ - POD-557: admin-only live availability check for a workspace slug. + Check whether a slug is available for the current workspace. Admin only. - Declared before `/{workspace_id}` so the literal path wins over the UUID - route. Auth is hard 4xx (missing/invalid X-Workspace-Id -> 400, not a member - -> 404, not admin -> 403, missing ?slug -> 422). Slug content is soft 200 - `{available, reason?: invalid|reserved|taken}`, self-excluded against the - X-Workspace-Id workspace's own current slug. Global across tenants. + Returns `{ available: true }` if the slug is valid, not reserved, and + not already claimed by another workspace. When unavailable, `reason` + indicates why: `invalid` (format/length), `reserved` (blocked word), or + `taken` (already in use globally). The workspace's own current slug is + self-excluded, so an admin can safely check their existing slug without + receiving `taken`. - Advisory only — a point-in-time snapshot. A concurrent request can claim the - slug between this check and the caller's POST/PATCH, so callers must still - handle 409 WORKSPACE_SLUG_TAKEN on the write path. + This is an advisory point-in-time check — a concurrent `POST /workspaces` + or `PATCH /workspaces/{id}` from another session can claim the slug + between this response and the caller's write. Always handle 409 + `WORKSPACE_SLUG_TAKEN` on `create_workspace` and `update_workspace`. + + Requires the `X-Workspace-Id` header (the workspace being renamed) and + admin role in that workspace. Missing/invalid header returns 400; not a + member returns 404; not admin returns 403. Parameters ---------- @@ -167,7 +191,12 @@ def get_workspace( self, workspace_id: str, *, request_options: typing.Optional[RequestOptions] = None ) -> ApiResponseWorkspaceOut: """ - Get a workspace the current user is a member of. + Fetch a single workspace by ID. + + Returns the workspace if the current user is an active member (any role). + Returns 404 if the workspace does not exist, has been deleted, or the + caller is not a member — the two cases are intentionally indistinguishable + to prevent workspace enumeration. Parameters ---------- @@ -199,7 +228,16 @@ def delete_workspace( self, workspace_id: str, *, request_options: typing.Optional[RequestOptions] = None ) -> ApiResponseDict: """ - Soft-delete a workspace and cascade soft-delete to its resources. + Delete a workspace and all of its resources. Owner only. + + Soft-deletes the workspace and cascades to all owned resources (workflows, + voices, dictionary entries, members, etc.). The workspace and its contents + become inaccessible via the API immediately. Data is retained for the GDPR + retention period before permanent purge. + + Only the workspace owner (the user who created it) can delete it; admin + members who are not the owner receive 404. Returns 404 if the workspace + does not exist or the caller is not the owner. Parameters ---------- @@ -234,20 +272,41 @@ def update_workspace( name: typing.Optional[str] = OMIT, slug: typing.Optional[str] = OMIT, color_idx: typing.Optional[int] = OMIT, + routing_price_sensitivity: typing.Optional[float] = OMIT, + routing_llm_fit: typing.Optional[bool] = OMIT, request_options: typing.Optional[RequestOptions] = None, ) -> ApiResponseWorkspaceOut: """ - Update workspace name, color, and/or slug. Admin only. + Update a workspace's name, color palette index, and/or slug. Admin only. + + All fields are optional — supply only the fields you want to change. + `slug` follows the same validation rules as on create (lowercase + kebab-case, 1–50 characters, no reserved words). Returns 409 if the new + slug is already claimed by another workspace, 422 if the slug format is + invalid or reserved. Re-setting the workspace's current slug to itself + never returns 409. + + Only workspace admins may call this endpoint; other members receive 404 + (same as not-found, to avoid leaking membership details to non-members). Parameters ---------- workspace_id : str name : typing.Optional[str] + New workspace name. Omit to leave unchanged. slug : typing.Optional[str] + New slug (lowercase kebab-case, 1–50 characters). Omit to leave unchanged. Returns 409 if taken, 422 if invalid or reserved. color_idx : typing.Optional[int] + New color palette index (0–6). Omit to leave unchanged. + + routing_price_sensitivity : typing.Optional[float] + New voice-selection price/quality balance (0.0 = pure quality, 1.0 = pure price, 0.5 = balanced). Omit to leave unchanged. + + routing_llm_fit : typing.Optional[bool] + New setting for whether automatic voice selection also weighs content fit. Omit to leave unchanged. request_options : typing.Optional[RequestOptions] Request-specific configuration. @@ -269,7 +328,13 @@ def update_workspace( ) """ _response = self._raw_client.update_workspace( - workspace_id, name=name, slug=slug, color_idx=color_idx, request_options=request_options + workspace_id, + name=name, + slug=slug, + color_idx=color_idx, + routing_price_sensitivity=routing_price_sensitivity, + routing_llm_fit=routing_llm_fit, + request_options=request_options, ) return _response.data @@ -297,7 +362,12 @@ async def list_workspaces( request_options: typing.Optional[RequestOptions] = None, ) -> ApiListResponseWorkspaceOut: """ - List workspaces the current user is a member of. + List all workspaces the current user is a member of. + + Returns workspaces where the caller has any role (admin, editor, or + viewer), including workspaces they own and workspaces they joined via + invite. Results are paginated; omits soft-deleted workspaces and the + internal system workspace. Parameters ---------- @@ -344,17 +414,30 @@ async def create_workspace( """ Create a new workspace owned by the current user. - POD-301: gated by `workspaces_per_owner` plan limit. Free=1, Creator=1, - Studio=2, Enterprise=bespoke. Owner soft-deletes don't free up quota until - purge — keeps the gate honest against rapid create/delete cycles. + Workspaces are the top-level container for all resources (workflows, + voices, dictionary entries, members). Every resource is scoped to exactly + one workspace via the `X-Workspace-Id` header on subsequent requests. + + The authenticated user becomes the workspace owner and is automatically + added as an `admin` member. An optional `slug` (1–50 characters, + lowercase kebab-case) can be supplied for a human-readable workspace + identifier; if omitted, one is auto-generated from `name`. Returns 409 + if the slug is already taken, 422 if the slug format is invalid or uses + a reserved word. + + The number of workspaces a user may own is plan-gated. Attempting to + exceed the limit returns 402. Parameters ---------- name : str + Human-readable workspace name (1–200 characters, non-blank). slug : typing.Optional[str] + Optional URL-safe identifier (lowercase kebab-case, 1–50 characters). Auto-generated from `name` if omitted. Returns 409 if taken, 422 if invalid or reserved. color_idx : typing.Optional[int] + Index into the workspace color palette (0–6). request_options : typing.Optional[RequestOptions] Request-specific configuration. @@ -396,17 +479,23 @@ async def slug_available( request_options: typing.Optional[RequestOptions] = None, ) -> ApiResponseSlugAvailabilityOut: """ - POD-557: admin-only live availability check for a workspace slug. + Check whether a slug is available for the current workspace. Admin only. - Declared before `/{workspace_id}` so the literal path wins over the UUID - route. Auth is hard 4xx (missing/invalid X-Workspace-Id -> 400, not a member - -> 404, not admin -> 403, missing ?slug -> 422). Slug content is soft 200 - `{available, reason?: invalid|reserved|taken}`, self-excluded against the - X-Workspace-Id workspace's own current slug. Global across tenants. + Returns `{ available: true }` if the slug is valid, not reserved, and + not already claimed by another workspace. When unavailable, `reason` + indicates why: `invalid` (format/length), `reserved` (blocked word), or + `taken` (already in use globally). The workspace's own current slug is + self-excluded, so an admin can safely check their existing slug without + receiving `taken`. - Advisory only — a point-in-time snapshot. A concurrent request can claim the - slug between this check and the caller's POST/PATCH, so callers must still - handle 409 WORKSPACE_SLUG_TAKEN on the write path. + This is an advisory point-in-time check — a concurrent `POST /workspaces` + or `PATCH /workspaces/{id}` from another session can claim the slug + between this response and the caller's write. Always handle 409 + `WORKSPACE_SLUG_TAKEN` on `create_workspace` and `update_workspace`. + + Requires the `X-Workspace-Id` header (the workspace being renamed) and + admin role in that workspace. Missing/invalid header returns 400; not a + member returns 404; not admin returns 403. Parameters ---------- @@ -451,7 +540,12 @@ async def get_workspace( self, workspace_id: str, *, request_options: typing.Optional[RequestOptions] = None ) -> ApiResponseWorkspaceOut: """ - Get a workspace the current user is a member of. + Fetch a single workspace by ID. + + Returns the workspace if the current user is an active member (any role). + Returns 404 if the workspace does not exist, has been deleted, or the + caller is not a member — the two cases are intentionally indistinguishable + to prevent workspace enumeration. Parameters ---------- @@ -491,7 +585,16 @@ async def delete_workspace( self, workspace_id: str, *, request_options: typing.Optional[RequestOptions] = None ) -> ApiResponseDict: """ - Soft-delete a workspace and cascade soft-delete to its resources. + Delete a workspace and all of its resources. Owner only. + + Soft-deletes the workspace and cascades to all owned resources (workflows, + voices, dictionary entries, members, etc.). The workspace and its contents + become inaccessible via the API immediately. Data is retained for the GDPR + retention period before permanent purge. + + Only the workspace owner (the user who created it) can delete it; admin + members who are not the owner receive 404. Returns 404 if the workspace + does not exist or the caller is not the owner. Parameters ---------- @@ -534,20 +637,41 @@ async def update_workspace( name: typing.Optional[str] = OMIT, slug: typing.Optional[str] = OMIT, color_idx: typing.Optional[int] = OMIT, + routing_price_sensitivity: typing.Optional[float] = OMIT, + routing_llm_fit: typing.Optional[bool] = OMIT, request_options: typing.Optional[RequestOptions] = None, ) -> ApiResponseWorkspaceOut: """ - Update workspace name, color, and/or slug. Admin only. + Update a workspace's name, color palette index, and/or slug. Admin only. + + All fields are optional — supply only the fields you want to change. + `slug` follows the same validation rules as on create (lowercase + kebab-case, 1–50 characters, no reserved words). Returns 409 if the new + slug is already claimed by another workspace, 422 if the slug format is + invalid or reserved. Re-setting the workspace's current slug to itself + never returns 409. + + Only workspace admins may call this endpoint; other members receive 404 + (same as not-found, to avoid leaking membership details to non-members). Parameters ---------- workspace_id : str name : typing.Optional[str] + New workspace name. Omit to leave unchanged. slug : typing.Optional[str] + New slug (lowercase kebab-case, 1–50 characters). Omit to leave unchanged. Returns 409 if taken, 422 if invalid or reserved. color_idx : typing.Optional[int] + New color palette index (0–6). Omit to leave unchanged. + + routing_price_sensitivity : typing.Optional[float] + New voice-selection price/quality balance (0.0 = pure quality, 1.0 = pure price, 0.5 = balanced). Omit to leave unchanged. + + routing_llm_fit : typing.Optional[bool] + New setting for whether automatic voice selection also weighs content fit. Omit to leave unchanged. request_options : typing.Optional[RequestOptions] Request-specific configuration. @@ -577,6 +701,12 @@ async def main() -> None: asyncio.run(main()) """ _response = await self._raw_client.update_workspace( - workspace_id, name=name, slug=slug, color_idx=color_idx, request_options=request_options + workspace_id, + name=name, + slug=slug, + color_idx=color_idx, + routing_price_sensitivity=routing_price_sensitivity, + routing_llm_fit=routing_llm_fit, + request_options=request_options, ) return _response.data diff --git a/src/onepin/workspaces/raw_client.py b/src/onepin/workspaces/raw_client.py index 21fe1a6..50f4297 100644 --- a/src/onepin/workspaces/raw_client.py +++ b/src/onepin/workspaces/raw_client.py @@ -38,7 +38,12 @@ def list_workspaces( request_options: typing.Optional[RequestOptions] = None, ) -> HttpResponse[ApiListResponseWorkspaceOut]: """ - List workspaces the current user is a member of. + List all workspaces the current user is a member of. + + Returns workspaces where the caller has any role (admin, editor, or + viewer), including workspaces they own and workspaces they joined via + invite. Results are paginated; omits soft-deleted workspaces and the + internal system workspace. Parameters ---------- @@ -104,17 +109,30 @@ def create_workspace( """ Create a new workspace owned by the current user. - POD-301: gated by `workspaces_per_owner` plan limit. Free=1, Creator=1, - Studio=2, Enterprise=bespoke. Owner soft-deletes don't free up quota until - purge — keeps the gate honest against rapid create/delete cycles. + Workspaces are the top-level container for all resources (workflows, + voices, dictionary entries, members). Every resource is scoped to exactly + one workspace via the `X-Workspace-Id` header on subsequent requests. + + The authenticated user becomes the workspace owner and is automatically + added as an `admin` member. An optional `slug` (1–50 characters, + lowercase kebab-case) can be supplied for a human-readable workspace + identifier; if omitted, one is auto-generated from `name`. Returns 409 + if the slug is already taken, 422 if the slug format is invalid or uses + a reserved word. + + The number of workspaces a user may own is plan-gated. Attempting to + exceed the limit returns 402. Parameters ---------- name : str + Human-readable workspace name (1–200 characters, non-blank). slug : typing.Optional[str] + Optional URL-safe identifier (lowercase kebab-case, 1–50 characters). Auto-generated from `name` if omitted. Returns 409 if taken, 422 if invalid or reserved. color_idx : typing.Optional[int] + Index into the workspace color palette (0–6). request_options : typing.Optional[RequestOptions] Request-specific configuration. @@ -187,17 +205,23 @@ def slug_available( request_options: typing.Optional[RequestOptions] = None, ) -> HttpResponse[ApiResponseSlugAvailabilityOut]: """ - POD-557: admin-only live availability check for a workspace slug. + Check whether a slug is available for the current workspace. Admin only. - Declared before `/{workspace_id}` so the literal path wins over the UUID - route. Auth is hard 4xx (missing/invalid X-Workspace-Id -> 400, not a member - -> 404, not admin -> 403, missing ?slug -> 422). Slug content is soft 200 - `{available, reason?: invalid|reserved|taken}`, self-excluded against the - X-Workspace-Id workspace's own current slug. Global across tenants. + Returns `{ available: true }` if the slug is valid, not reserved, and + not already claimed by another workspace. When unavailable, `reason` + indicates why: `invalid` (format/length), `reserved` (blocked word), or + `taken` (already in use globally). The workspace's own current slug is + self-excluded, so an admin can safely check their existing slug without + receiving `taken`. - Advisory only — a point-in-time snapshot. A concurrent request can claim the - slug between this check and the caller's POST/PATCH, so callers must still - handle 409 WORKSPACE_SLUG_TAKEN on the write path. + This is an advisory point-in-time check — a concurrent `POST /workspaces` + or `PATCH /workspaces/{id}` from another session can claim the slug + between this response and the caller's write. Always handle 409 + `WORKSPACE_SLUG_TAKEN` on `create_workspace` and `update_workspace`. + + Requires the `X-Workspace-Id` header (the workspace being renamed) and + admin role in that workspace. Missing/invalid header returns 400; not a + member returns 404; not admin returns 403. Parameters ---------- @@ -292,7 +316,12 @@ def get_workspace( self, workspace_id: str, *, request_options: typing.Optional[RequestOptions] = None ) -> HttpResponse[ApiResponseWorkspaceOut]: """ - Get a workspace the current user is a member of. + Fetch a single workspace by ID. + + Returns the workspace if the current user is an active member (any role). + Returns 404 if the workspace does not exist, has been deleted, or the + caller is not a member — the two cases are intentionally indistinguishable + to prevent workspace enumeration. Parameters ---------- @@ -345,7 +374,16 @@ def delete_workspace( self, workspace_id: str, *, request_options: typing.Optional[RequestOptions] = None ) -> HttpResponse[ApiResponseDict]: """ - Soft-delete a workspace and cascade soft-delete to its resources. + Delete a workspace and all of its resources. Owner only. + + Soft-deletes the workspace and cascades to all owned resources (workflows, + voices, dictionary entries, members, etc.). The workspace and its contents + become inaccessible via the API immediately. Data is retained for the GDPR + retention period before permanent purge. + + Only the workspace owner (the user who created it) can delete it; admin + members who are not the owner receive 404. Returns 404 if the workspace + does not exist or the caller is not the owner. Parameters ---------- @@ -401,20 +439,41 @@ def update_workspace( name: typing.Optional[str] = OMIT, slug: typing.Optional[str] = OMIT, color_idx: typing.Optional[int] = OMIT, + routing_price_sensitivity: typing.Optional[float] = OMIT, + routing_llm_fit: typing.Optional[bool] = OMIT, request_options: typing.Optional[RequestOptions] = None, ) -> HttpResponse[ApiResponseWorkspaceOut]: """ - Update workspace name, color, and/or slug. Admin only. + Update a workspace's name, color palette index, and/or slug. Admin only. + + All fields are optional — supply only the fields you want to change. + `slug` follows the same validation rules as on create (lowercase + kebab-case, 1–50 characters, no reserved words). Returns 409 if the new + slug is already claimed by another workspace, 422 if the slug format is + invalid or reserved. Re-setting the workspace's current slug to itself + never returns 409. + + Only workspace admins may call this endpoint; other members receive 404 + (same as not-found, to avoid leaking membership details to non-members). Parameters ---------- workspace_id : str name : typing.Optional[str] + New workspace name. Omit to leave unchanged. slug : typing.Optional[str] + New slug (lowercase kebab-case, 1–50 characters). Omit to leave unchanged. Returns 409 if taken, 422 if invalid or reserved. color_idx : typing.Optional[int] + New color palette index (0–6). Omit to leave unchanged. + + routing_price_sensitivity : typing.Optional[float] + New voice-selection price/quality balance (0.0 = pure quality, 1.0 = pure price, 0.5 = balanced). Omit to leave unchanged. + + routing_llm_fit : typing.Optional[bool] + New setting for whether automatic voice selection also weighs content fit. Omit to leave unchanged. request_options : typing.Optional[RequestOptions] Request-specific configuration. @@ -431,6 +490,8 @@ def update_workspace( "name": name, "slug": slug, "color_idx": color_idx, + "routing_price_sensitivity": routing_price_sensitivity, + "routing_llm_fit": routing_llm_fit, }, headers={ "content-type": "application/json", @@ -492,7 +553,12 @@ async def list_workspaces( request_options: typing.Optional[RequestOptions] = None, ) -> AsyncHttpResponse[ApiListResponseWorkspaceOut]: """ - List workspaces the current user is a member of. + List all workspaces the current user is a member of. + + Returns workspaces where the caller has any role (admin, editor, or + viewer), including workspaces they own and workspaces they joined via + invite. Results are paginated; omits soft-deleted workspaces and the + internal system workspace. Parameters ---------- @@ -558,17 +624,30 @@ async def create_workspace( """ Create a new workspace owned by the current user. - POD-301: gated by `workspaces_per_owner` plan limit. Free=1, Creator=1, - Studio=2, Enterprise=bespoke. Owner soft-deletes don't free up quota until - purge — keeps the gate honest against rapid create/delete cycles. + Workspaces are the top-level container for all resources (workflows, + voices, dictionary entries, members). Every resource is scoped to exactly + one workspace via the `X-Workspace-Id` header on subsequent requests. + + The authenticated user becomes the workspace owner and is automatically + added as an `admin` member. An optional `slug` (1–50 characters, + lowercase kebab-case) can be supplied for a human-readable workspace + identifier; if omitted, one is auto-generated from `name`. Returns 409 + if the slug is already taken, 422 if the slug format is invalid or uses + a reserved word. + + The number of workspaces a user may own is plan-gated. Attempting to + exceed the limit returns 402. Parameters ---------- name : str + Human-readable workspace name (1–200 characters, non-blank). slug : typing.Optional[str] + Optional URL-safe identifier (lowercase kebab-case, 1–50 characters). Auto-generated from `name` if omitted. Returns 409 if taken, 422 if invalid or reserved. color_idx : typing.Optional[int] + Index into the workspace color palette (0–6). request_options : typing.Optional[RequestOptions] Request-specific configuration. @@ -641,17 +720,23 @@ async def slug_available( request_options: typing.Optional[RequestOptions] = None, ) -> AsyncHttpResponse[ApiResponseSlugAvailabilityOut]: """ - POD-557: admin-only live availability check for a workspace slug. + Check whether a slug is available for the current workspace. Admin only. - Declared before `/{workspace_id}` so the literal path wins over the UUID - route. Auth is hard 4xx (missing/invalid X-Workspace-Id -> 400, not a member - -> 404, not admin -> 403, missing ?slug -> 422). Slug content is soft 200 - `{available, reason?: invalid|reserved|taken}`, self-excluded against the - X-Workspace-Id workspace's own current slug. Global across tenants. + Returns `{ available: true }` if the slug is valid, not reserved, and + not already claimed by another workspace. When unavailable, `reason` + indicates why: `invalid` (format/length), `reserved` (blocked word), or + `taken` (already in use globally). The workspace's own current slug is + self-excluded, so an admin can safely check their existing slug without + receiving `taken`. - Advisory only — a point-in-time snapshot. A concurrent request can claim the - slug between this check and the caller's POST/PATCH, so callers must still - handle 409 WORKSPACE_SLUG_TAKEN on the write path. + This is an advisory point-in-time check — a concurrent `POST /workspaces` + or `PATCH /workspaces/{id}` from another session can claim the slug + between this response and the caller's write. Always handle 409 + `WORKSPACE_SLUG_TAKEN` on `create_workspace` and `update_workspace`. + + Requires the `X-Workspace-Id` header (the workspace being renamed) and + admin role in that workspace. Missing/invalid header returns 400; not a + member returns 404; not admin returns 403. Parameters ---------- @@ -746,7 +831,12 @@ async def get_workspace( self, workspace_id: str, *, request_options: typing.Optional[RequestOptions] = None ) -> AsyncHttpResponse[ApiResponseWorkspaceOut]: """ - Get a workspace the current user is a member of. + Fetch a single workspace by ID. + + Returns the workspace if the current user is an active member (any role). + Returns 404 if the workspace does not exist, has been deleted, or the + caller is not a member — the two cases are intentionally indistinguishable + to prevent workspace enumeration. Parameters ---------- @@ -799,7 +889,16 @@ async def delete_workspace( self, workspace_id: str, *, request_options: typing.Optional[RequestOptions] = None ) -> AsyncHttpResponse[ApiResponseDict]: """ - Soft-delete a workspace and cascade soft-delete to its resources. + Delete a workspace and all of its resources. Owner only. + + Soft-deletes the workspace and cascades to all owned resources (workflows, + voices, dictionary entries, members, etc.). The workspace and its contents + become inaccessible via the API immediately. Data is retained for the GDPR + retention period before permanent purge. + + Only the workspace owner (the user who created it) can delete it; admin + members who are not the owner receive 404. Returns 404 if the workspace + does not exist or the caller is not the owner. Parameters ---------- @@ -855,20 +954,41 @@ async def update_workspace( name: typing.Optional[str] = OMIT, slug: typing.Optional[str] = OMIT, color_idx: typing.Optional[int] = OMIT, + routing_price_sensitivity: typing.Optional[float] = OMIT, + routing_llm_fit: typing.Optional[bool] = OMIT, request_options: typing.Optional[RequestOptions] = None, ) -> AsyncHttpResponse[ApiResponseWorkspaceOut]: """ - Update workspace name, color, and/or slug. Admin only. + Update a workspace's name, color palette index, and/or slug. Admin only. + + All fields are optional — supply only the fields you want to change. + `slug` follows the same validation rules as on create (lowercase + kebab-case, 1–50 characters, no reserved words). Returns 409 if the new + slug is already claimed by another workspace, 422 if the slug format is + invalid or reserved. Re-setting the workspace's current slug to itself + never returns 409. + + Only workspace admins may call this endpoint; other members receive 404 + (same as not-found, to avoid leaking membership details to non-members). Parameters ---------- workspace_id : str name : typing.Optional[str] + New workspace name. Omit to leave unchanged. slug : typing.Optional[str] + New slug (lowercase kebab-case, 1–50 characters). Omit to leave unchanged. Returns 409 if taken, 422 if invalid or reserved. color_idx : typing.Optional[int] + New color palette index (0–6). Omit to leave unchanged. + + routing_price_sensitivity : typing.Optional[float] + New voice-selection price/quality balance (0.0 = pure quality, 1.0 = pure price, 0.5 = balanced). Omit to leave unchanged. + + routing_llm_fit : typing.Optional[bool] + New setting for whether automatic voice selection also weighs content fit. Omit to leave unchanged. request_options : typing.Optional[RequestOptions] Request-specific configuration. @@ -885,6 +1005,8 @@ async def update_workspace( "name": name, "slug": slug, "color_idx": color_idx, + "routing_price_sensitivity": routing_price_sensitivity, + "routing_llm_fit": routing_llm_fit, }, headers={ "content-type": "application/json", From fb71e13377503c6a6b2269252256625fd68334c0 Mon Sep 17 00:00:00 2001 From: kj-podonos Date: Fri, 3 Jul 2026 16:27:34 +0900 Subject: [PATCH 2/2] fix(tests): add required workspace routing fields to CLI test fixtures SDK sync (b3a0cd8) added routing_price_sensitivity/routing_llm_fit as required fields on WorkspaceOut, breaking mocked workspace fixtures in test_cli_extra.py and test_cli_integration_respx.py. Co-Authored-By: Claude Opus 4.6 --- tests/cli/test_cli_extra.py | 4 ++++ tests/cli/test_cli_integration_respx.py | 2 ++ 2 files changed, 6 insertions(+) diff --git a/tests/cli/test_cli_extra.py b/tests/cli/test_cli_extra.py index be15ce7..6135bbb 100644 --- a/tests/cli/test_cli_extra.py +++ b/tests/cli/test_cli_extra.py @@ -72,6 +72,8 @@ def get_workspace(self, workspace_id, *, request_options=None): name="n", slug="s", color_idx=0, + routing_price_sensitivity=0.5, + routing_llm_fit=True, created_by="u", created_at=NOW, updated_at=NOW, @@ -99,6 +101,8 @@ def get_workspace(self, workspace_id, **kw): name="Main", slug="main", color_idx=2, + routing_price_sensitivity=0.5, + routing_llm_fit=True, created_by="u", created_at=NOW, updated_at=NOW, diff --git a/tests/cli/test_cli_integration_respx.py b/tests/cli/test_cli_integration_respx.py index 43bade6..0691b1c 100644 --- a/tests/cli/test_cli_integration_respx.py +++ b/tests/cli/test_cli_integration_respx.py @@ -82,6 +82,8 @@ def test_workspace_list(self, tmp_home) -> None: "name": "Main", "slug": "main", "color_idx": 0, + "routing_price_sensitivity": 0.5, + "routing_llm_fit": True, "created_by": "u", "created_at": "2025-01-01T00:00:00Z", "updated_at": "2025-01-01T00:00:00Z",