diff --git a/.changelog-pending/2026-06-30T19-54-28-3c3685bab553edbe5f45b99d4f4d4e7ef625be2c.md b/.changelog-pending/2026-06-30T19-54-28-3c3685bab553edbe5f45b99d4f4d4e7ef625be2c.md new file mode 100644 index 00000000..08bf8c7c --- /dev/null +++ b/.changelog-pending/2026-06-30T19-54-28-3c3685bab553edbe5f45b99d4f4d4e7ef625be2c.md @@ -0,0 +1,11 @@ +* [#679](https://github.com/workos/workos-python/pull/679) feat(generated): regenerate from spec (1 change) + + **Features** + * **[pipes](https://workos.com/docs/reference/pipes)**: + * Added model `DataIntegrationCredentialsResponse` + * Added model `DataIntegrationCredentialsResponseCredential` + * Added model `DataIntegrationsUpsertApiKeyRequest` + * Added model `DataIntegrationsVendCredentialsRequest` + * Added enum `DataIntegrationCredentialsResponseError` + * Added endpoint `PUT /data-integrations/{slug}/api-key` + * Added endpoint `POST /data-integrations/{slug}/credentials` diff --git a/.last-synced-sha b/.last-synced-sha index d5b98283..5241742c 100644 --- a/.last-synced-sha +++ b/.last-synced-sha @@ -1 +1 @@ -1a2f47b20f63f2c8f0eb56bbd2adb3b5947d693a +e122f280d5635b7a6d39c7ce6dc24cd52e267e1c diff --git a/.oagen-manifest.json b/.oagen-manifest.json index ce6bfc85..3d14e61d 100644 --- a/.oagen-manifest.json +++ b/.oagen-manifest.json @@ -1,7 +1,7 @@ { "version": 2, "language": "python", - "generatedAt": "2026-06-17T20:55:36.392Z", + "generatedAt": "2026-06-30T19:54:11.665Z", "files": [ "src/workos/_client.py", "src/workos/admin_portal/__init__.py", @@ -12,6 +12,8 @@ "src/workos/admin_portal/models/intent_options.py", "src/workos/admin_portal/models/portal_link_response.py", "src/workos/admin_portal/models/sso_intent_options.py", + "src/workos/agents/__init__.py", + "src/workos/agents/models/__init__.py", "src/workos/api_keys/__init__.py", "src/workos/api_keys/_resource.py", "src/workos/api_keys/models/__init__.py", @@ -100,6 +102,7 @@ "src/workos/common/models/audit_log_configuration_log_stream_type.py", "src/workos/common/models/audit_log_configuration_state.py", "src/workos/common/models/audit_log_export_state.py", + "src/workos/common/models/auth_method_mismatch_error.py", "src/workos/common/models/authenticate_response_authentication_method.py", "src/workos/common/models/authenticate_response_impersonator.py", "src/workos/common/models/authentication_challenge.py", @@ -164,6 +167,7 @@ "src/workos/common/models/connect_application_m2m.py", "src/workos/common/models/connect_application_oauth.py", "src/workos/common/models/connect_application_oauth_redirect_uris.py", + "src/workos/common/models/connected_account.py", "src/workos/common/models/connected_account_auth_method.py", "src/workos/common/models/connected_account_state.py", "src/workos/common/models/connection_activated.py", @@ -200,6 +204,7 @@ "src/workos/common/models/create_webhook_endpoint_events.py", "src/workos/common/models/data_integration_access_token_response_error.py", "src/workos/common/models/data_integration_credentials_credentials_type.py", + "src/workos/common/models/data_integration_credentials_response_error.py", "src/workos/common/models/data_integrations_list_response_data_auth_methods.py", "src/workos/common/models/data_integrations_list_response_data_connected_account_auth_method.py", "src/workos/common/models/data_integrations_list_response_data_connected_account_state.py", @@ -546,11 +551,15 @@ "src/workos/pipes/models/data_integration_access_token_response.py", "src/workos/pipes/models/data_integration_access_token_response_access_token.py", "src/workos/pipes/models/data_integration_authorize_url_response.py", + "src/workos/pipes/models/data_integration_credentials_response.py", + "src/workos/pipes/models/data_integration_credentials_response_credential.py", "src/workos/pipes/models/data_integrations_get_data_integration_authorize_url_request.py", "src/workos/pipes/models/data_integrations_get_user_token_request.py", "src/workos/pipes/models/data_integrations_list_response.py", "src/workos/pipes/models/data_integrations_list_response_data.py", "src/workos/pipes/models/data_integrations_list_response_data_connected_account.py", + "src/workos/pipes/models/data_integrations_upsert_api_key_request.py", + "src/workos/pipes/models/data_integrations_vend_credentials_request.py", "src/workos/pipes_provider/__init__.py", "src/workos/pipes_provider/_resource.py", "src/workos/pipes_provider/models/__init__.py", @@ -586,6 +595,7 @@ "src/workos/sso/models/token_query.py", "src/workos/types/__init__.py", "src/workos/types/admin_portal/__init__.py", + "src/workos/types/agents/__init__.py", "src/workos/types/api_keys/__init__.py", "src/workos/types/audit_logs/__init__.py", "src/workos/types/authorization/__init__.py", @@ -729,6 +739,7 @@ "tests/fixtures/audit_log_schema_target.json", "tests/fixtures/audit_log_schema_target_input.json", "tests/fixtures/audit_logs_retention.json", + "tests/fixtures/auth_method_mismatch_error.json", "tests/fixtures/authenticate_response.json", "tests/fixtures/authenticate_response_impersonator.json", "tests/fixtures/authenticate_response_oauth_token.json", @@ -854,11 +865,15 @@ "tests/fixtures/data_integration_configuration_list_response.json", "tests/fixtures/data_integration_configuration_response.json", "tests/fixtures/data_integration_credentials.json", + "tests/fixtures/data_integration_credentials_response.json", + "tests/fixtures/data_integration_credentials_response_credential.json", "tests/fixtures/data_integrations_get_data_integration_authorize_url_request.json", "tests/fixtures/data_integrations_get_user_token_request.json", "tests/fixtures/data_integrations_list_response.json", "tests/fixtures/data_integrations_list_response_data.json", "tests/fixtures/data_integrations_list_response_data_connected_account.json", + "tests/fixtures/data_integrations_upsert_api_key_request.json", + "tests/fixtures/data_integrations_vend_credentials_request.json", "tests/fixtures/decrypt_request.json", "tests/fixtures/decrypt_response.json", "tests/fixtures/delete_group_role_assignments_by_criteria.json", @@ -1980,6 +1995,30 @@ "GET /audit_logs/exports/{auditLogExportId}": { "sdkMethod": "get_export", "service": "audit_logs" + }, + "POST /agents/credentials/validate": { + "sdkMethod": "create_validate", + "service": "agents" + }, + "GET /agents/registrations/{id}": { + "sdkMethod": "get_registration", + "service": "agents" + }, + "PUT /data-integrations/{slug}/api-key": { + "sdkMethod": "update_data_integration_api_key", + "service": "pipes" + }, + "POST /data-integrations/{slug}/credentials": { + "sdkMethod": "create_data_integration_credential", + "service": "pipes" + }, + "GET /user_management/cors_origins": { + "sdkMethod": "list_cors_origins", + "service": "user_management" + }, + "GET /user_management/redirect_uris": { + "sdkMethod": "list_redirect_uris", + "service": "user_management" } } } diff --git a/src/workos/_client.py b/src/workos/_client.py index 8322fa00..41d319cc 100644 --- a/src/workos/_client.py +++ b/src/workos/_client.py @@ -8,6 +8,7 @@ WorkOSClient as _SyncBase, AsyncWorkOSClient as _AsyncBase, ) +from .agents._resource import Agents, AsyncAgents from .multi_factor_auth._resource import MultiFactorAuth, AsyncMultiFactorAuth from .connect._resource import Connect, AsyncConnect from .authorization._resource import Authorization, AsyncAuthorization @@ -44,6 +45,11 @@ class WorkOSClient(_SyncBase): """Synchronous WorkOS API client with service accessors.""" + @functools.cached_property + def agents(self) -> Agents: + """Agents API resources.""" + return Agents(self) + @functools.cached_property def multi_factor_auth(self) -> MultiFactorAuth: """Multi Factor Auth API resources.""" @@ -182,6 +188,11 @@ def pkce(self) -> PKCE: class AsyncWorkOSClient(_AsyncBase): """Asynchronous WorkOS API client with service accessors.""" + @functools.cached_property + def agents(self) -> AsyncAgents: + """Agents API resources.""" + return AsyncAgents(self) + @functools.cached_property def multi_factor_auth(self) -> AsyncMultiFactorAuth: """Multi Factor Auth API resources.""" diff --git a/src/workos/admin_portal/models/__init__.py b/src/workos/admin_portal/models/__init__.py index b1b690c9..72480edf 100644 --- a/src/workos/admin_portal/models/__init__.py +++ b/src/workos/admin_portal/models/__init__.py @@ -1,9 +1,7 @@ # This file is auto-generated by oagen. Do not edit. -from .domain_verification_intent_options import ( - DomainVerificationIntentOptions as DomainVerificationIntentOptions, -) +from .domain_verification_intent_options import * # noqa: F401,F403 from .generate_link import GenerateLink as GenerateLink -from .intent_options import IntentOptions as IntentOptions +from .intent_options import * # noqa: F401,F403 from .portal_link_response import PortalLinkResponse as PortalLinkResponse -from .sso_intent_options import SSOIntentOptions as SSOIntentOptions +from .sso_intent_options import * # noqa: F401,F403 diff --git a/src/workos/agents/__init__.py b/src/workos/agents/__init__.py new file mode 100644 index 00000000..3b1eb581 --- /dev/null +++ b/src/workos/agents/__init__.py @@ -0,0 +1,4 @@ +# This file is auto-generated by oagen. Do not edit. + +from ._resource import Agents as Agents, AsyncAgents as AsyncAgents +from .models import * diff --git a/src/workos/agents/models/__init__.py b/src/workos/agents/models/__init__.py new file mode 100644 index 00000000..33e9c7b7 --- /dev/null +++ b/src/workos/agents/models/__init__.py @@ -0,0 +1,2 @@ +# This file is auto-generated by oagen. Do not edit. + diff --git a/src/workos/common/__init__.py b/src/workos/common/__init__.py index 884a9b6c..9498bbbf 100644 --- a/src/workos/common/__init__.py +++ b/src/workos/common/__init__.py @@ -121,6 +121,7 @@ AuthenticationSSOTimedOutDataError as AuthenticationSSOTimedOutDataError, ) from .models import AuthenticationSSOTimedOutDataSSO as AuthenticationSSOTimedOutDataSSO +from .models import AuthMethodMismatchError as AuthMethodMismatchError from .models import AuthorizationPermission as AuthorizationPermission from .models import ConnectApplication as ConnectApplication from .models import ConnectApplicationM2M as ConnectApplicationM2M @@ -130,6 +131,7 @@ ) from .models import ConnectApplicationUnknown as ConnectApplicationUnknown from .models import ConnectApplicationVariant as ConnectApplicationVariant +from .models import ConnectedAccount as ConnectedAccount from .models import ConnectedAccountAuthMethod as ConnectedAccountAuthMethod from .models import ConnectedAccountState as ConnectedAccountState from .models import ConnectionActivated as ConnectionActivated @@ -174,6 +176,9 @@ from .models import ( DataIntegrationCredentialsCredentialsType as DataIntegrationCredentialsCredentialsType, ) +from .models import ( + DataIntegrationCredentialsResponseError as DataIntegrationCredentialsResponseError, +) from .models import ( DataIntegrationsListResponseDataAuthMethods as DataIntegrationsListResponseDataAuthMethods, ) diff --git a/src/workos/common/models/__init__.py b/src/workos/common/models/__init__.py index 649a299d..224c6143 100644 --- a/src/workos/common/models/__init__.py +++ b/src/workos/common/models/__init__.py @@ -207,6 +207,9 @@ from .authentication_sso_timed_out_data_sso import ( AuthenticationSSOTimedOutDataSSO as AuthenticationSSOTimedOutDataSSO, ) +from .auth_method_mismatch_error import ( + AuthMethodMismatchError as AuthMethodMismatchError, +) from .authorization_permission import AuthorizationPermission as AuthorizationPermission from .connect_application import ConnectApplication as ConnectApplication from .connect_application_m2m import ConnectApplicationM2M as ConnectApplicationM2M @@ -218,6 +221,7 @@ ) from .connect_application import ConnectApplicationUnknown as ConnectApplicationUnknown from .connect_application import ConnectApplicationVariant as ConnectApplicationVariant +from .connected_account import ConnectedAccount as ConnectedAccount from .connected_account_auth_method import ( ConnectedAccountAuthMethod as ConnectedAccountAuthMethod, ) @@ -290,6 +294,9 @@ from .data_integration_credentials_credentials_type import ( DataIntegrationCredentialsCredentialsType as DataIntegrationCredentialsCredentialsType, ) +from .data_integration_credentials_response_error import ( + DataIntegrationCredentialsResponseError as DataIntegrationCredentialsResponseError, +) from .data_integrations_list_response_data_auth_methods import ( DataIntegrationsListResponseDataAuthMethods as DataIntegrationsListResponseDataAuthMethods, ) diff --git a/src/workos/common/models/auth_method_mismatch_error.py b/src/workos/common/models/auth_method_mismatch_error.py new file mode 100644 index 00000000..86a11ec5 --- /dev/null +++ b/src/workos/common/models/auth_method_mismatch_error.py @@ -0,0 +1,35 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Dict, Literal +from workos._types import _raise_deserialize_error + + +@dataclass(slots=True) +class AuthMethodMismatchError: + """Auth Method Mismatch Error model.""" + + code: Literal["auth_method_mismatch"] + """Error code indicating the endpoint does not match the installation auth method.""" + message: str + """A human-readable explanation of the mismatch.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "AuthMethodMismatchError": + """Deserialize from a dictionary.""" + try: + return cls( + code=data.get("code", "auth_method_mismatch"), + message=data["message"], + ) + except (KeyError, ValueError) as e: + _raise_deserialize_error("AuthMethodMismatchError", e) + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["code"] = self.code + result["message"] = self.message + return result diff --git a/src/workos/common/models/connected_account.py b/src/workos/common/models/connected_account.py new file mode 100644 index 00000000..dae7e6ca --- /dev/null +++ b/src/workos/common/models/connected_account.py @@ -0,0 +1,91 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from enum import Enum +from typing import Any, Dict, List, Literal, Optional +from workos._types import _raise_deserialize_error +from .connected_account_auth_method import ConnectedAccountAuthMethod +from .connected_account_state import ConnectedAccountState + + +@dataclass(slots=True) +class ConnectedAccount: + """Connected Account model.""" + + object: Literal["connected_account"] + """Distinguishes the connected account object.""" + id: str + """The unique identifier of the connected account.""" + user_id: Optional[str] + """The [User](https://workos.com/docs/reference/authkit/user) identifier associated with this connection.""" + organization_id: Optional[str] + """The [Organization](https://workos.com/docs/reference/organization) identifier associated with this connection, or `null` if not scoped to an organization.""" + scopes: List[str] + """The OAuth scopes granted for this connection.""" + state: "ConnectedAccountState" + """The state of the connected account: +- `connected`: The connection is active and tokens are valid. +- `needs_reauthorization`: The user needs to reauthorize the connection, typically because required scopes have changed. +- `disconnected`: The connection has been disconnected.""" + created_at: str + """The timestamp when the connection was created.""" + updated_at: str + """The timestamp when the connection was last updated.""" + auth_method: Optional["ConnectedAccountAuthMethod"] = None + """The authentication method used for this connection (`oauth` or `api_key`). Defaults to `oauth` if absent.""" + api_key_last_4: Optional[str] = None + """The last four characters of the API key, or `null` for OAuth connections.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "ConnectedAccount": + """Deserialize from a dictionary.""" + try: + return cls( + object=data.get("object", "connected_account"), + id=data["id"], + user_id=data["user_id"], + organization_id=data["organization_id"], + scopes=data["scopes"], + state=ConnectedAccountState(data["state"]), + created_at=data["created_at"], + updated_at=data["updated_at"], + auth_method=ConnectedAccountAuthMethod(_v_auth_method) + if (_v_auth_method := data.get("auth_method")) is not None + else None, + api_key_last_4=data.get("api_key_last_4"), + ) + except (KeyError, ValueError) as e: + _raise_deserialize_error("ConnectedAccount", e) + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["object"] = self.object + result["id"] = self.id + if self.user_id is not None: + result["user_id"] = self.user_id + else: + result["user_id"] = None + if self.organization_id is not None: + result["organization_id"] = self.organization_id + else: + result["organization_id"] = None + result["scopes"] = self.scopes + result["state"] = ( + self.state.value if isinstance(self.state, Enum) else self.state + ) + result["created_at"] = self.created_at + result["updated_at"] = self.updated_at + if self.auth_method is not None: + result["auth_method"] = ( + self.auth_method.value + if isinstance(self.auth_method, Enum) + else self.auth_method + ) + if self.api_key_last_4 is not None: + result["api_key_last_4"] = self.api_key_last_4 + else: + result["api_key_last_4"] = None + return result diff --git a/src/workos/common/models/data_integration_credentials_response_error.py b/src/workos/common/models/data_integration_credentials_response_error.py new file mode 100644 index 00000000..ba4291f9 --- /dev/null +++ b/src/workos/common/models/data_integration_credentials_response_error.py @@ -0,0 +1,11 @@ +# This file is auto-generated by oagen. Do not edit. + +from typing import TypeAlias +from .data_integration_access_token_response_error import ( + DataIntegrationAccessTokenResponseError, +) + +DataIntegrationCredentialsResponseError: TypeAlias = ( + DataIntegrationAccessTokenResponseError +) +__all__ = ["DataIntegrationCredentialsResponseError"] diff --git a/src/workos/pipes/_resource.py b/src/workos/pipes/_resource.py index 7bb3de1d..0883c2d1 100644 --- a/src/workos/pipes/_resource.py +++ b/src/workos/pipes/_resource.py @@ -9,11 +9,12 @@ from .._types import RequestOptions from .models import ( - ConnectedAccount, DataIntegrationAccessTokenResponse, DataIntegrationAuthorizeUrlResponse, + DataIntegrationCredentialsResponse, DataIntegrationsListResponse, ) +from workos.common.models.connected_account import ConnectedAccount class Pipes: @@ -22,6 +23,55 @@ class Pipes: def __init__(self, client: "WorkOSClient") -> None: self._client = client + def update_data_integration_api_key( + self, + slug: str, + *, + user_id: str, + secret: str, + organization_id: Optional[str] = None, + request_options: Optional[RequestOptions] = None, + ) -> ConnectedAccount: + """Upsert an API key for a connected account + + Creates or updates an API-key-based installation for the specified integration and user. If an installation already exists, the stored API key is rotated to the new value. + + Args: + slug: The identifier of the integration. + user_id: A [User](https://workos.com/docs/reference/authkit/user) identifier. + organization_id: An [Organization](https://workos.com/docs/reference/organization) identifier. Optional parameter to scope the connection to a specific organization. + secret: The API key secret to store for this integration. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + ConnectedAccount + + Raises: + BadRequestError: If the request is malformed (400). + AuthenticationError: If the API key is invalid (401). + AuthorizationError: If the request is forbidden (403). + NotFoundError: If the resource is not found (404). + UnprocessableEntityError: If the request data is unprocessable (422). + RateLimitExceededError: If rate limited (429). + ServerError: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + k: v + for k, v in { + "user_id": user_id, + "organization_id": organization_id, + "secret": secret, + }.items() + if v is not None + } + return self._client.request( + method="put", + path=("data-integrations", str(slug), "api-key"), + body=body, + model=ConnectedAccount, + request_options=request_options, + ) + def authorize_data_integration( self, slug: str, @@ -70,6 +120,50 @@ def authorize_data_integration( request_options=request_options, ) + def create_data_integration_credential( + self, + slug: str, + *, + user_id: str, + organization_id: Optional[str] = None, + request_options: Optional[RequestOptions] = None, + ) -> DataIntegrationCredentialsResponse: + """Vend credentials for a connected account + + Returns credentials for a user's connected account. Branches on the installation's `auth_method`: OAuth installations return an access token (refreshed if needed); API-key installations return the stored secret. + + Args: + slug: The identifier of the integration. + user_id: A [User](https://workos.com/docs/reference/authkit/user) identifier. + organization_id: An [Organization](https://workos.com/docs/reference/organization) identifier. Optional parameter to scope the connection to a specific organization. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + DataIntegrationCredentialsResponse + + Raises: + BadRequestError: If the request is malformed (400). + AuthenticationError: If the API key is invalid (401). + NotFoundError: If the resource is not found (404). + RateLimitExceededError: If rate limited (429). + ServerError: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + k: v + for k, v in { + "user_id": user_id, + "organization_id": organization_id, + }.items() + if v is not None + } + return self._client.request( + method="post", + path=("data-integrations", str(slug), "credentials"), + body=body, + model=DataIntegrationCredentialsResponse, + request_options=request_options, + ) + def get_access_token( self, provider: str, @@ -95,6 +189,7 @@ def get_access_token( BadRequestError: If the request is malformed (400). AuthenticationError: If the API key is invalid (401). NotFoundError: If the resource is not found (404). + UnprocessableEntityError: If the request data is unprocessable (422). RateLimitExceededError: If rate limited (429). ServerError: If the server returns a 5xx error. """ @@ -253,6 +348,55 @@ class AsyncPipes: def __init__(self, client: "AsyncWorkOSClient") -> None: self._client = client + async def update_data_integration_api_key( + self, + slug: str, + *, + user_id: str, + secret: str, + organization_id: Optional[str] = None, + request_options: Optional[RequestOptions] = None, + ) -> ConnectedAccount: + """Upsert an API key for a connected account + + Creates or updates an API-key-based installation for the specified integration and user. If an installation already exists, the stored API key is rotated to the new value. + + Args: + slug: The identifier of the integration. + user_id: A [User](https://workos.com/docs/reference/authkit/user) identifier. + organization_id: An [Organization](https://workos.com/docs/reference/organization) identifier. Optional parameter to scope the connection to a specific organization. + secret: The API key secret to store for this integration. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + ConnectedAccount + + Raises: + BadRequestError: If the request is malformed (400). + AuthenticationError: If the API key is invalid (401). + AuthorizationError: If the request is forbidden (403). + NotFoundError: If the resource is not found (404). + UnprocessableEntityError: If the request data is unprocessable (422). + RateLimitExceededError: If rate limited (429). + ServerError: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + k: v + for k, v in { + "user_id": user_id, + "organization_id": organization_id, + "secret": secret, + }.items() + if v is not None + } + return await self._client.request( + method="put", + path=("data-integrations", str(slug), "api-key"), + body=body, + model=ConnectedAccount, + request_options=request_options, + ) + async def authorize_data_integration( self, slug: str, @@ -301,6 +445,50 @@ async def authorize_data_integration( request_options=request_options, ) + async def create_data_integration_credential( + self, + slug: str, + *, + user_id: str, + organization_id: Optional[str] = None, + request_options: Optional[RequestOptions] = None, + ) -> DataIntegrationCredentialsResponse: + """Vend credentials for a connected account + + Returns credentials for a user's connected account. Branches on the installation's `auth_method`: OAuth installations return an access token (refreshed if needed); API-key installations return the stored secret. + + Args: + slug: The identifier of the integration. + user_id: A [User](https://workos.com/docs/reference/authkit/user) identifier. + organization_id: An [Organization](https://workos.com/docs/reference/organization) identifier. Optional parameter to scope the connection to a specific organization. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + DataIntegrationCredentialsResponse + + Raises: + BadRequestError: If the request is malformed (400). + AuthenticationError: If the API key is invalid (401). + NotFoundError: If the resource is not found (404). + RateLimitExceededError: If rate limited (429). + ServerError: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + k: v + for k, v in { + "user_id": user_id, + "organization_id": organization_id, + }.items() + if v is not None + } + return await self._client.request( + method="post", + path=("data-integrations", str(slug), "credentials"), + body=body, + model=DataIntegrationCredentialsResponse, + request_options=request_options, + ) + async def get_access_token( self, provider: str, @@ -326,6 +514,7 @@ async def get_access_token( BadRequestError: If the request is malformed (400). AuthenticationError: If the API key is invalid (401). NotFoundError: If the resource is not found (404). + UnprocessableEntityError: If the request data is unprocessable (422). RateLimitExceededError: If rate limited (429). ServerError: If the server returns a 5xx error. """ diff --git a/src/workos/pipes/models/__init__.py b/src/workos/pipes/models/__init__.py index a0899bc4..0959db7a 100644 --- a/src/workos/pipes/models/__init__.py +++ b/src/workos/pipes/models/__init__.py @@ -1,6 +1,7 @@ # This file is auto-generated by oagen. Do not edit. -from .connected_account import ConnectedAccount as ConnectedAccount +from .connected_account import * # noqa: F401,F403 +from workos.common.models.connected_account import ConnectedAccount as ConnectedAccount from .data_integration_access_token_response import ( DataIntegrationAccessTokenResponse as DataIntegrationAccessTokenResponse, ) @@ -10,6 +11,12 @@ from .data_integration_authorize_url_response import ( DataIntegrationAuthorizeUrlResponse as DataIntegrationAuthorizeUrlResponse, ) +from .data_integration_credentials_response import ( + DataIntegrationCredentialsResponse as DataIntegrationCredentialsResponse, +) +from .data_integration_credentials_response_credential import ( + DataIntegrationCredentialsResponseCredential as DataIntegrationCredentialsResponseCredential, +) from .data_integrations_get_data_integration_authorize_url_request import ( DataIntegrationsGetDataIntegrationAuthorizeUrlRequest as DataIntegrationsGetDataIntegrationAuthorizeUrlRequest, ) @@ -25,3 +32,9 @@ from .data_integrations_list_response_data_connected_account import ( DataIntegrationsListResponseDataConnectedAccount as DataIntegrationsListResponseDataConnectedAccount, ) +from .data_integrations_upsert_api_key_request import ( + DataIntegrationsUpsertApiKeyRequest as DataIntegrationsUpsertApiKeyRequest, +) +from .data_integrations_vend_credentials_request import ( + DataIntegrationsVendCredentialsRequest as DataIntegrationsVendCredentialsRequest, +) diff --git a/src/workos/pipes/models/data_integration_credentials_response.py b/src/workos/pipes/models/data_integration_credentials_response.py new file mode 100644 index 00000000..59c29a4d --- /dev/null +++ b/src/workos/pipes/models/data_integration_credentials_response.py @@ -0,0 +1,61 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from enum import Enum +from typing import cast +from typing import Any, Dict, Literal, Optional +from workos._types import _raise_deserialize_error + +from .data_integration_credentials_response_credential import ( + DataIntegrationCredentialsResponseCredential, +) +from workos.common.models.data_integration_credentials_response_error import ( + DataIntegrationCredentialsResponseError, +) + + +@dataclass(slots=True) +class DataIntegrationCredentialsResponse: + """Data Integration Credentials Response model.""" + + active: Optional[Literal[True]] = None + """Indicates credentials are available.""" + credential: Optional["DataIntegrationCredentialsResponseCredential"] = None + """The credential object containing the vended secret.""" + error: Optional["DataIntegrationCredentialsResponseError"] = None + """The reason credentials are unavailable. Additional values may be added in the future; handle unknown values gracefully. +- `"not_installed"`: The user does not have the integration installed. +- `"needs_reauthorization"`: The user needs to reauthorize the integration.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "DataIntegrationCredentialsResponse": + """Deserialize from a dictionary.""" + try: + return cls( + active=data.get("active"), + credential=DataIntegrationCredentialsResponseCredential.from_dict( + cast(Dict[str, Any], _v_credential) + ) + if (_v_credential := data.get("credential")) is not None + else None, + error=DataIntegrationCredentialsResponseError(_v_error) + if (_v_error := data.get("error")) is not None + else None, + ) + except (KeyError, ValueError) as e: + _raise_deserialize_error("DataIntegrationCredentialsResponse", e) + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + if self.active is not None: + result["active"] = self.active + if self.credential is not None: + result["credential"] = self.credential.to_dict() + if self.error is not None: + result["error"] = ( + self.error.value if isinstance(self.error, Enum) else self.error + ) + return result diff --git a/src/workos/pipes/models/data_integration_credentials_response_credential.py b/src/workos/pipes/models/data_integration_credentials_response_credential.py new file mode 100644 index 00000000..0c5c6bff --- /dev/null +++ b/src/workos/pipes/models/data_integration_credentials_response_credential.py @@ -0,0 +1,56 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Dict, List, Literal, Optional +from workos._types import _raise_deserialize_error + + +@dataclass(slots=True) +class DataIntegrationCredentialsResponseCredential: + """The credential object containing the vended secret.""" + + object: Literal["credential"] + """Distinguishes the credential object.""" + auth_method: Literal["oauth"] + """The authentication method for this credential. Additional values may be added in the future; handle unknown values gracefully.""" + value: str + """The OAuth access token.""" + expires_at: Optional[str] + """The ISO-8601 formatted timestamp indicating when the credential expires.""" + scopes: List[str] + """The scopes granted to the access token.""" + missing_scopes: List[str] + """If the integration has requested scopes that aren't present on the access token, they're listed here.""" + + @classmethod + def from_dict( + cls, data: Dict[str, Any] + ) -> "DataIntegrationCredentialsResponseCredential": + """Deserialize from a dictionary.""" + try: + return cls( + object=data.get("object", "credential"), + auth_method=data.get("auth_method", "oauth"), + value=data["value"], + expires_at=data["expires_at"], + scopes=data["scopes"], + missing_scopes=data["missing_scopes"], + ) + except (KeyError, ValueError) as e: + _raise_deserialize_error("DataIntegrationCredentialsResponseCredential", e) + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["object"] = self.object + result["auth_method"] = self.auth_method + result["value"] = self.value + if self.expires_at is not None: + result["expires_at"] = self.expires_at + else: + result["expires_at"] = None + result["scopes"] = self.scopes + result["missing_scopes"] = self.missing_scopes + return result diff --git a/src/workos/pipes/models/data_integrations_upsert_api_key_request.py b/src/workos/pipes/models/data_integrations_upsert_api_key_request.py new file mode 100644 index 00000000..72bec1f6 --- /dev/null +++ b/src/workos/pipes/models/data_integrations_upsert_api_key_request.py @@ -0,0 +1,40 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Dict, Optional +from workos._types import _raise_deserialize_error + + +@dataclass(slots=True) +class DataIntegrationsUpsertApiKeyRequest: + """Data Integrations Upsert Api Key Request model.""" + + user_id: str + """A [User](https://workos.com/docs/reference/authkit/user) identifier.""" + secret: str + """The API key secret to store for this integration.""" + organization_id: Optional[str] = None + """An [Organization](https://workos.com/docs/reference/organization) identifier. Optional parameter to scope the connection to a specific organization.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "DataIntegrationsUpsertApiKeyRequest": + """Deserialize from a dictionary.""" + try: + return cls( + user_id=data["user_id"], + secret=data["secret"], + organization_id=data.get("organization_id"), + ) + except (KeyError, ValueError) as e: + _raise_deserialize_error("DataIntegrationsUpsertApiKeyRequest", e) + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["user_id"] = self.user_id + result["secret"] = self.secret + if self.organization_id is not None: + result["organization_id"] = self.organization_id + return result diff --git a/src/workos/pipes/models/data_integrations_vend_credentials_request.py b/src/workos/pipes/models/data_integrations_vend_credentials_request.py new file mode 100644 index 00000000..35ec5eb0 --- /dev/null +++ b/src/workos/pipes/models/data_integrations_vend_credentials_request.py @@ -0,0 +1,38 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Dict, Optional +from workos._types import _raise_deserialize_error + + +@dataclass(slots=True) +class DataIntegrationsVendCredentialsRequest: + """Data Integrations Vend Credentials Request model.""" + + user_id: str + """A [User](https://workos.com/docs/reference/authkit/user) identifier.""" + organization_id: Optional[str] = None + """An [Organization](https://workos.com/docs/reference/organization) identifier. Optional parameter to scope the connection to a specific organization.""" + + @classmethod + def from_dict( + cls, data: Dict[str, Any] + ) -> "DataIntegrationsVendCredentialsRequest": + """Deserialize from a dictionary.""" + try: + return cls( + user_id=data["user_id"], + organization_id=data.get("organization_id"), + ) + except (KeyError, ValueError) as e: + _raise_deserialize_error("DataIntegrationsVendCredentialsRequest", e) + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["user_id"] = self.user_id + if self.organization_id is not None: + result["organization_id"] = self.organization_id + return result diff --git a/src/workos/types/agents/__init__.py b/src/workos/types/agents/__init__.py new file mode 100644 index 00000000..7a33b078 --- /dev/null +++ b/src/workos/types/agents/__init__.py @@ -0,0 +1,3 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.agents.models import * # noqa: F401,F403 diff --git a/tests/fixtures/api_key_validation_response.json b/tests/fixtures/api_key_validation_response.json index 77549bf9..01e025ac 100644 --- a/tests/fixtures/api_key_validation_response.json +++ b/tests/fixtures/api_key_validation_response.json @@ -16,5 +16,6 @@ ], "created_at": "2026-01-15T12:00:00.000Z", "updated_at": "2026-01-15T12:00:00.000Z" - } + }, + "agent_registration_id": "agent_reg_01EHZNVPK3SFK441A1RGBFSHRT" } diff --git a/tests/fixtures/auth_method_mismatch_error.json b/tests/fixtures/auth_method_mismatch_error.json new file mode 100644 index 00000000..6b3ed0d2 --- /dev/null +++ b/tests/fixtures/auth_method_mismatch_error.json @@ -0,0 +1,4 @@ +{ + "code": "auth_method_mismatch", + "message": "This installation uses oauth authentication. Use the POST /:slug/token endpoint instead." +} diff --git a/tests/fixtures/data_integration_credentials_response.json b/tests/fixtures/data_integration_credentials_response.json new file mode 100644 index 00000000..9efedd6d --- /dev/null +++ b/tests/fixtures/data_integration_credentials_response.json @@ -0,0 +1,15 @@ +{ + "active": true, + "credential": { + "object": "credential", + "auth_method": "oauth", + "value": "gho_16C7e42F292c6912E7710c838347Ae178B4a", + "expires_at": "2025-12-31T23:59:59.000Z", + "scopes": [ + "repo", + "user:email" + ], + "missing_scopes": [] + }, + "error": "not_installed" +} diff --git a/tests/fixtures/data_integration_credentials_response_credential.json b/tests/fixtures/data_integration_credentials_response_credential.json new file mode 100644 index 00000000..2b2b0bce --- /dev/null +++ b/tests/fixtures/data_integration_credentials_response_credential.json @@ -0,0 +1,11 @@ +{ + "object": "credential", + "auth_method": "oauth", + "value": "gho_16C7e42F292c6912E7710c838347Ae178B4a", + "expires_at": "2025-12-31T23:59:59.000Z", + "scopes": [ + "repo", + "user:email" + ], + "missing_scopes": [] +} diff --git a/tests/fixtures/data_integrations_upsert_api_key_request.json b/tests/fixtures/data_integrations_upsert_api_key_request.json new file mode 100644 index 00000000..bf0bd81b --- /dev/null +++ b/tests/fixtures/data_integrations_upsert_api_key_request.json @@ -0,0 +1,5 @@ +{ + "user_id": "user_01EHZNVPK3SFK441A1RGBFSHRT", + "organization_id": "org_01EHZNVPK3SFK441A1RGBFSHRT", + "secret": "sk-1234567890abcdef" +} diff --git a/tests/fixtures/data_integrations_vend_credentials_request.json b/tests/fixtures/data_integrations_vend_credentials_request.json new file mode 100644 index 00000000..ef8c4438 --- /dev/null +++ b/tests/fixtures/data_integrations_vend_credentials_request.json @@ -0,0 +1,4 @@ +{ + "user_id": "user_01EHZNVPK3SFK441A1RGBFSHRT", + "organization_id": "org_01EHZNVPK3SFK441A1RGBFSHRT" +} diff --git a/tests/fixtures/generate_link.json b/tests/fixtures/generate_link.json index 47084d30..0630573b 100644 --- a/tests/fixtures/generate_link.json +++ b/tests/fixtures/generate_link.json @@ -3,15 +3,6 @@ "success_url": "https://example.com/admin-portal/success", "organization": "org_01EHZNVPK3SFK441A1RGBFSHRT", "intent": "sso", - "intent_options": { - "sso": { - "bookmark_slug": "chatgpt", - "provider_type": "GoogleSAML" - }, - "domain_verification": { - "domain_name": "example.com" - } - }, "it_contact_emails": [ "it-contact@example.com" ] diff --git a/tests/fixtures/list_user_role_assignment.json b/tests/fixtures/list_user_role_assignment.json index e909d9ac..ac0737ac 100644 --- a/tests/fixtures/list_user_role_assignment.json +++ b/tests/fixtures/list_user_role_assignment.json @@ -12,6 +12,10 @@ "external_id": "proj-456", "resource_type_slug": "project" }, + "source": { + "type": "direct", + "group_role_assignment_id": null + }, "created_at": "2026-01-15T12:00:00.000Z", "updated_at": "2026-01-15T12:00:00.000Z" } diff --git a/tests/fixtures/permission_created.json b/tests/fixtures/permission_created.json index f991b655..7d84a43f 100644 --- a/tests/fixtures/permission_created.json +++ b/tests/fixtures/permission_created.json @@ -9,6 +9,7 @@ "name": "Manage Billing", "description": "Allows managing billing settings.", "system": false, + "resource_type_slug": "organization", "created_at": "2026-01-15T12:00:00.000Z", "updated_at": "2026-01-15T12:00:00.000Z" }, diff --git a/tests/fixtures/permission_created_data.json b/tests/fixtures/permission_created_data.json index b17cbcbe..5f6836ca 100644 --- a/tests/fixtures/permission_created_data.json +++ b/tests/fixtures/permission_created_data.json @@ -5,6 +5,7 @@ "name": "Manage Billing", "description": "Allows managing billing settings.", "system": false, + "resource_type_slug": "organization", "created_at": "2026-01-15T12:00:00.000Z", "updated_at": "2026-01-15T12:00:00.000Z" } diff --git a/tests/fixtures/permission_deleted.json b/tests/fixtures/permission_deleted.json index ee636f8e..b0459ff7 100644 --- a/tests/fixtures/permission_deleted.json +++ b/tests/fixtures/permission_deleted.json @@ -9,6 +9,7 @@ "name": "Manage Billing", "description": "Allows managing billing settings.", "system": false, + "resource_type_slug": "organization", "created_at": "2026-01-15T12:00:00.000Z", "updated_at": "2026-01-15T12:00:00.000Z" }, diff --git a/tests/fixtures/permission_deleted_data.json b/tests/fixtures/permission_deleted_data.json index b17cbcbe..5f6836ca 100644 --- a/tests/fixtures/permission_deleted_data.json +++ b/tests/fixtures/permission_deleted_data.json @@ -5,6 +5,7 @@ "name": "Manage Billing", "description": "Allows managing billing settings.", "system": false, + "resource_type_slug": "organization", "created_at": "2026-01-15T12:00:00.000Z", "updated_at": "2026-01-15T12:00:00.000Z" } diff --git a/tests/fixtures/permission_updated.json b/tests/fixtures/permission_updated.json index 17ddb1d5..bce477c3 100644 --- a/tests/fixtures/permission_updated.json +++ b/tests/fixtures/permission_updated.json @@ -9,6 +9,7 @@ "name": "Manage Billing", "description": "Allows managing billing settings.", "system": false, + "resource_type_slug": "organization", "created_at": "2026-01-15T12:00:00.000Z", "updated_at": "2026-01-15T12:00:00.000Z" }, diff --git a/tests/fixtures/permission_updated_data.json b/tests/fixtures/permission_updated_data.json index b17cbcbe..5f6836ca 100644 --- a/tests/fixtures/permission_updated_data.json +++ b/tests/fixtures/permission_updated_data.json @@ -5,6 +5,7 @@ "name": "Manage Billing", "description": "Allows managing billing settings.", "system": false, + "resource_type_slug": "organization", "created_at": "2026-01-15T12:00:00.000Z", "updated_at": "2026-01-15T12:00:00.000Z" } diff --git a/tests/fixtures/radar_standalone_assess_request.json b/tests/fixtures/radar_standalone_assess_request.json index 710b5312..969ae0ca 100644 --- a/tests/fixtures/radar_standalone_assess_request.json +++ b/tests/fixtures/radar_standalone_assess_request.json @@ -3,5 +3,6 @@ "user_agent": "Mozilla/5.0", "email": "user@example.com", "auth_method": "Password", - "action": "sign-in" + "action": "sign-in", + "signals_id": "01JBS0GN92GC2RJQS4X9DBPQ2A" } diff --git a/tests/fixtures/user_role_assignment.json b/tests/fixtures/user_role_assignment.json index 48ca7237..ae717059 100644 --- a/tests/fixtures/user_role_assignment.json +++ b/tests/fixtures/user_role_assignment.json @@ -10,6 +10,10 @@ "external_id": "proj-456", "resource_type_slug": "project" }, + "source": { + "type": "direct", + "group_role_assignment_id": null + }, "created_at": "2026-01-15T12:00:00.000Z", "updated_at": "2026-01-15T12:00:00.000Z" } diff --git a/tests/test_models_round_trip.py b/tests/test_models_round_trip.py index 341c98c7..b9b58ec4 100644 --- a/tests/test_models_round_trip.py +++ b/tests/test_models_round_trip.py @@ -6,12 +6,7 @@ from tests.generated_helpers import load_fixture -from workos.admin_portal.models import ( - DomainVerificationIntentOptions, - IntentOptions, - PortalLinkResponse, - SSOIntentOptions, -) +from workos.admin_portal.models import PortalLinkResponse from workos.api_keys.models import ( ApiKey, ApiKeyOwner, @@ -67,6 +62,7 @@ ApiKeyUpdatedData, ApiKeyUpdatedDataOwner, ApiKeyUpdatedDataPreviousAttribute, + AuthMethodMismatchError, AuthenticationEmailVerificationFailed, AuthenticationEmailVerificationFailedData, AuthenticationEmailVerificationFailedDataError, @@ -351,6 +347,8 @@ DataIntegrationAccessTokenResponse, DataIntegrationAccessTokenResponseAccessToken, DataIntegrationAuthorizeUrlResponse, + DataIntegrationCredentialsResponse, + DataIntegrationCredentialsResponseCredential, DataIntegrationsListResponse, DataIntegrationsListResponseData, DataIntegrationsListResponseDataConnectedAccount, @@ -706,67 +704,6 @@ def test_organization_domain_data_round_trips_unknown_enum_values(self): instance = OrganizationDomainData.from_dict(data) assert instance.to_dict() == data - def test_sso_intent_options_round_trip(self): - data = load_fixture("sso_intent_options.json") - instance = SSOIntentOptions.from_dict(data) - serialized = instance.to_dict() - assert serialized == data - restored = SSOIntentOptions.from_dict(serialized) - assert restored.to_dict() == serialized - - def test_sso_intent_options_minimal_payload(self): - data = {} - instance = SSOIntentOptions.from_dict(data) - assert instance.to_dict() is not None - - def test_sso_intent_options_omits_absent_optional_non_nullable_fields(self): - data = {} - instance = SSOIntentOptions.from_dict(data) - serialized = instance.to_dict() - assert "bookmark_slug" not in serialized - assert "provider_type" not in serialized - - def test_domain_verification_intent_options_round_trip(self): - data = load_fixture("domain_verification_intent_options.json") - instance = DomainVerificationIntentOptions.from_dict(data) - serialized = instance.to_dict() - assert serialized == data - restored = DomainVerificationIntentOptions.from_dict(serialized) - assert restored.to_dict() == serialized - - def test_domain_verification_intent_options_minimal_payload(self): - data = {} - instance = DomainVerificationIntentOptions.from_dict(data) - assert instance.to_dict() is not None - - def test_domain_verification_intent_options_omits_absent_optional_non_nullable_fields( - self, - ): - data = {} - instance = DomainVerificationIntentOptions.from_dict(data) - serialized = instance.to_dict() - assert "domain_name" not in serialized - - def test_intent_options_round_trip(self): - data = load_fixture("intent_options.json") - instance = IntentOptions.from_dict(data) - serialized = instance.to_dict() - assert serialized == data - restored = IntentOptions.from_dict(serialized) - assert restored.to_dict() == serialized - - def test_intent_options_minimal_payload(self): - data = {} - instance = IntentOptions.from_dict(data) - assert instance.to_dict() is not None - - def test_intent_options_omits_absent_optional_non_nullable_fields(self): - data = {} - instance = IntentOptions.from_dict(data) - serialized = instance.to_dict() - assert "sso" not in serialized - assert "domain_verification" not in serialized - def test_actor_round_trip(self): data = load_fixture("actor.json") instance = Actor.from_dict(data) @@ -1141,8 +1078,35 @@ def test_api_key_validation_response_minimal_payload(self): serialized = instance.to_dict() assert serialized["api_key"] == data["api_key"] + def test_api_key_validation_response_omits_absent_optional_non_nullable_fields( + self, + ): + data = { + "api_key": { + "object": "api_key", + "id": "api_key_01EHZNVPK3SFK441A1RGBFSHRT", + "owner": { + "type": "organization", + "id": "org_01EHZNVPK3SFK441A1RGBFSHRT", + }, + "name": "Production API Key", + "obfuscated_value": "sk_...3456", + "last_used_at": None, + "expires_at": None, + "permissions": ["posts:read", "posts:write"], + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + } + } + instance = ApiKeyValidationResponse.from_dict(data) + serialized = instance.to_dict() + assert "agent_registration_id" not in serialized + def test_api_key_validation_response_preserves_nullable_fields(self): - data = {"api_key": None} + data = { + "api_key": None, + "agent_registration_id": "agent_reg_01EHZNVPK3SFK441A1RGBFSHRT", + } instance = ApiKeyValidationResponse.from_dict(data) serialized = instance.to_dict() assert serialized["api_key"] is None @@ -1909,6 +1873,7 @@ def test_user_role_assignment_minimal_payload(self): "external_id": "proj-456", "resource_type_slug": "project", }, + "source": {"type": "direct", "group_role_assignment_id": None}, "created_at": "2026-01-15T12:00:00.000Z", "updated_at": "2026-01-15T12:00:00.000Z", } @@ -1922,6 +1887,7 @@ def test_user_role_assignment_minimal_payload(self): ) assert serialized["role"] == data["role"] assert serialized["resource"] == data["resource"] + assert serialized["source"] == data["source"] assert serialized["created_at"] == data["created_at"] assert serialized["updated_at"] == data["updated_at"] @@ -12377,6 +12343,7 @@ def test_permission_created_minimal_payload(self): "name": "Manage Billing", "description": "Allows managing billing settings.", "system": False, + "resource_type_slug": "organization", "created_at": "2026-01-15T12:00:00.000Z", "updated_at": "2026-01-15T12:00:00.000Z", }, @@ -12402,6 +12369,7 @@ def test_permission_created_omits_absent_optional_non_nullable_fields(self): "name": "Manage Billing", "description": "Allows managing billing settings.", "system": False, + "resource_type_slug": "organization", "created_at": "2026-01-15T12:00:00.000Z", "updated_at": "2026-01-15T12:00:00.000Z", }, @@ -12427,6 +12395,7 @@ def test_permission_created_data_minimal_payload(self): "name": "Manage Billing", "description": None, "system": False, + "resource_type_slug": "organization", "created_at": "2026-01-15T12:00:00.000Z", "updated_at": "2026-01-15T12:00:00.000Z", } @@ -12438,6 +12407,7 @@ def test_permission_created_data_minimal_payload(self): assert serialized["name"] == data["name"] assert serialized["description"] == data["description"] assert serialized["system"] == data["system"] + assert serialized["resource_type_slug"] == data["resource_type_slug"] assert serialized["created_at"] == data["created_at"] assert serialized["updated_at"] == data["updated_at"] @@ -12449,6 +12419,7 @@ def test_permission_created_data_preserves_nullable_fields(self): "name": "Manage Billing", "description": None, "system": False, + "resource_type_slug": "organization", "created_at": "2026-01-15T12:00:00.000Z", "updated_at": "2026-01-15T12:00:00.000Z", } @@ -12476,6 +12447,7 @@ def test_permission_deleted_minimal_payload(self): "name": "Manage Billing", "description": "Allows managing billing settings.", "system": False, + "resource_type_slug": "organization", "created_at": "2026-01-15T12:00:00.000Z", "updated_at": "2026-01-15T12:00:00.000Z", }, @@ -12501,6 +12473,7 @@ def test_permission_deleted_omits_absent_optional_non_nullable_fields(self): "name": "Manage Billing", "description": "Allows managing billing settings.", "system": False, + "resource_type_slug": "organization", "created_at": "2026-01-15T12:00:00.000Z", "updated_at": "2026-01-15T12:00:00.000Z", }, @@ -12526,6 +12499,7 @@ def test_permission_deleted_data_minimal_payload(self): "name": "Manage Billing", "description": None, "system": False, + "resource_type_slug": "organization", "created_at": "2026-01-15T12:00:00.000Z", "updated_at": "2026-01-15T12:00:00.000Z", } @@ -12537,6 +12511,7 @@ def test_permission_deleted_data_minimal_payload(self): assert serialized["name"] == data["name"] assert serialized["description"] == data["description"] assert serialized["system"] == data["system"] + assert serialized["resource_type_slug"] == data["resource_type_slug"] assert serialized["created_at"] == data["created_at"] assert serialized["updated_at"] == data["updated_at"] @@ -12548,6 +12523,7 @@ def test_permission_deleted_data_preserves_nullable_fields(self): "name": "Manage Billing", "description": None, "system": False, + "resource_type_slug": "organization", "created_at": "2026-01-15T12:00:00.000Z", "updated_at": "2026-01-15T12:00:00.000Z", } @@ -12575,6 +12551,7 @@ def test_permission_updated_minimal_payload(self): "name": "Manage Billing", "description": "Allows managing billing settings.", "system": False, + "resource_type_slug": "organization", "created_at": "2026-01-15T12:00:00.000Z", "updated_at": "2026-01-15T12:00:00.000Z", }, @@ -12600,6 +12577,7 @@ def test_permission_updated_omits_absent_optional_non_nullable_fields(self): "name": "Manage Billing", "description": "Allows managing billing settings.", "system": False, + "resource_type_slug": "organization", "created_at": "2026-01-15T12:00:00.000Z", "updated_at": "2026-01-15T12:00:00.000Z", }, @@ -12625,6 +12603,7 @@ def test_permission_updated_data_minimal_payload(self): "name": "Manage Billing", "description": None, "system": False, + "resource_type_slug": "organization", "created_at": "2026-01-15T12:00:00.000Z", "updated_at": "2026-01-15T12:00:00.000Z", } @@ -12636,6 +12615,7 @@ def test_permission_updated_data_minimal_payload(self): assert serialized["name"] == data["name"] assert serialized["description"] == data["description"] assert serialized["system"] == data["system"] + assert serialized["resource_type_slug"] == data["resource_type_slug"] assert serialized["created_at"] == data["created_at"] assert serialized["updated_at"] == data["updated_at"] @@ -12647,6 +12627,7 @@ def test_permission_updated_data_preserves_nullable_fields(self): "name": "Manage Billing", "description": None, "system": False, + "resource_type_slug": "organization", "created_at": "2026-01-15T12:00:00.000Z", "updated_at": "2026-01-15T12:00:00.000Z", } @@ -15235,6 +15216,65 @@ def test_data_integration_access_token_response_round_trips_unknown_enum_values( instance = DataIntegrationAccessTokenResponse.from_dict(data) assert instance.to_dict() == data + def test_auth_method_mismatch_error_round_trip(self): + data = load_fixture("auth_method_mismatch_error.json") + instance = AuthMethodMismatchError.from_dict(data) + serialized = instance.to_dict() + assert serialized == data + restored = AuthMethodMismatchError.from_dict(serialized) + assert restored.to_dict() == serialized + + def test_auth_method_mismatch_error_minimal_payload(self): + data = { + "code": "auth_method_mismatch", + "message": "This installation uses oauth authentication. Use the POST /:slug/token endpoint instead.", + } + instance = AuthMethodMismatchError.from_dict(data) + serialized = instance.to_dict() + assert serialized["code"] == data["code"] + assert serialized["message"] == data["message"] + + def test_data_integration_credentials_response_round_trip(self): + data = load_fixture("data_integration_credentials_response.json") + instance = DataIntegrationCredentialsResponse.from_dict(data) + serialized = instance.to_dict() + assert serialized == data + restored = DataIntegrationCredentialsResponse.from_dict(serialized) + assert restored.to_dict() == serialized + + def test_data_integration_credentials_response_minimal_payload(self): + data = {} + instance = DataIntegrationCredentialsResponse.from_dict(data) + assert instance.to_dict() is not None + + def test_data_integration_credentials_response_omits_absent_optional_non_nullable_fields( + self, + ): + data = {} + instance = DataIntegrationCredentialsResponse.from_dict(data) + serialized = instance.to_dict() + assert "active" not in serialized + assert "credential" not in serialized + assert "error" not in serialized + + def test_data_integration_credentials_response_round_trips_unknown_enum_values( + self, + ): + data = { + "active": True, + "credential": { + "object": "credential", + "auth_method": "oauth", + "value": "gho_16C7e42F292c6912E7710c838347Ae178B4a", + "expires_at": "2025-12-31T23:59:59.000Z", + "scopes": ["repo", "user:email"], + "missing_scopes": [], + }, + "error": "unexpected_data_integration_credentials_response_error", + } + instance = DataIntegrationCredentialsResponse.from_dict(data) + assert instance.to_dict() == data + def test_connected_account_round_trip(self): data = load_fixture("connected_account.json") instance = ConnectedAccount.from_dict(data) @@ -16849,6 +16889,47 @@ def test_data_integrations_list_response_data_round_trips_unknown_enum_values(se instance = DataIntegrationsListResponseData.from_dict(data) assert instance.to_dict() == data + def test_data_integration_credentials_response_credential_round_trip(self): + data = load_fixture("data_integration_credentials_response_credential.json") + instance = DataIntegrationCredentialsResponseCredential.from_dict(data) + serialized = instance.to_dict() + assert serialized == data + restored = DataIntegrationCredentialsResponseCredential.from_dict(serialized) + assert restored.to_dict() == serialized + + def test_data_integration_credentials_response_credential_minimal_payload(self): + data = { + "object": "credential", + "auth_method": "oauth", + "value": "gho_16C7e42F292c6912E7710c838347Ae178B4a", + "expires_at": None, + "scopes": ["repo", "user:email"], + "missing_scopes": [], + } + instance = DataIntegrationCredentialsResponseCredential.from_dict(data) + serialized = instance.to_dict() + assert serialized["object"] == data["object"] + assert serialized["auth_method"] == data["auth_method"] + assert serialized["value"] == data["value"] + assert serialized["expires_at"] == data["expires_at"] + assert serialized["scopes"] == data["scopes"] + assert serialized["missing_scopes"] == data["missing_scopes"] + + def test_data_integration_credentials_response_credential_preserves_nullable_fields( + self, + ): + data = { + "object": "credential", + "auth_method": "oauth", + "value": "gho_16C7e42F292c6912E7710c838347Ae178B4a", + "expires_at": None, + "scopes": ["repo", "user:email"], + "missing_scopes": [], + } + instance = DataIntegrationCredentialsResponseCredential.from_dict(data) + serialized = instance.to_dict() + assert serialized["expires_at"] is None + def test_data_integration_access_token_response_access_token_round_trip(self): data = load_fixture("data_integration_access_token_response_access_token.json") instance = DataIntegrationAccessTokenResponseAccessToken.from_dict(data) diff --git a/tests/test_pipes.py b/tests/test_pipes.py index ed57d5b6..5264db7f 100644 --- a/tests/test_pipes.py +++ b/tests/test_pipes.py @@ -6,10 +6,11 @@ from workos import WorkOSClient, AsyncWorkOSClient from tests.generated_helpers import load_fixture +from workos.common.models import ConnectedAccount from workos.pipes.models import ( - ConnectedAccount, DataIntegrationAccessTokenResponse, DataIntegrationAuthorizeUrlResponse, + DataIntegrationCredentialsResponse, DataIntegrationsListResponse, ) from workos._errors import ( @@ -23,6 +24,23 @@ class TestPipes: + def test_update_data_integration_api_key(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("connected_account.json"), + ) + result = workos.pipes.update_data_integration_api_key( + "test_slug", user_id="test_user_id", secret="test_secret" + ) + assert isinstance(result, ConnectedAccount) + assert result.object == "connected_account" + assert result.id == "data_installation_01EHZNVPK3SFK441A1RGBFSHRT" + request = httpx_mock.get_request() + assert request.method == "PUT" + assert request.url.path.endswith("/data-integrations/test_slug/api-key") + body = json.loads(request.content) + assert body["user_id"] == "test_user_id" + assert body["secret"] == "test_secret" + def test_authorize_data_integration(self, workos, httpx_mock): httpx_mock.add_response( json=load_fixture("data_integration_authorize_url_response.json"), @@ -41,6 +59,22 @@ def test_authorize_data_integration(self, workos, httpx_mock): body = json.loads(request.content) assert body["user_id"] == "test_user_id" + def test_create_data_integration_credential(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("data_integration_credentials_response.json"), + ) + result = workos.pipes.create_data_integration_credential( + "test_slug", user_id="test_user_id" + ) + assert isinstance(result, DataIntegrationCredentialsResponse) + assert result.active is True + assert result.error == "not_installed" + request = httpx_mock.get_request() + assert request.method == "POST" + assert request.url.path.endswith("/data-integrations/test_slug/credentials") + body = json.loads(request.content) + assert body["user_id"] == "test_user_id" + def test_get_access_token(self, workos, httpx_mock): httpx_mock.add_response( json=load_fixture("data_integration_access_token_response.json"), @@ -120,40 +154,43 @@ def test_list_user_data_providers_encodes_query_params(self, workos, httpx_mock) request = httpx_mock.get_request() assert request.url.params["organization_id"] == "value organization_id/test" - def test_authorize_data_integration_with_request_options(self, workos, httpx_mock): - httpx_mock.add_response( - json=load_fixture("data_integration_authorize_url_response.json") - ) - workos.pipes.authorize_data_integration( + def test_update_data_integration_api_key_with_request_options( + self, workos, httpx_mock + ): + httpx_mock.add_response(json=load_fixture("connected_account.json")) + workos.pipes.update_data_integration_api_key( "test_slug", user_id="test_user_id", + secret="test_secret", request_options={"extra_headers": {"X-Custom": "value"}}, ) request = httpx_mock.get_request() assert request.headers["X-Custom"] == "value" - def test_authorize_data_integration_unauthorized(self, workos, httpx_mock): + def test_update_data_integration_api_key_unauthorized(self, workos, httpx_mock): httpx_mock.add_response( status_code=401, json={"message": "Unauthorized"}, ) with pytest.raises(AuthenticationError): - workos.pipes.authorize_data_integration("test_slug", user_id="test_user_id") + workos.pipes.update_data_integration_api_key( + "test_slug", user_id="test_user_id", secret="test_secret" + ) - def test_authorize_data_integration_not_found(self, httpx_mock): + def test_update_data_integration_api_key_not_found(self, httpx_mock): workos = WorkOSClient( api_key="sk_test_123", client_id="client_test", max_retries=0 ) try: httpx_mock.add_response(status_code=404, json={"message": "Not found"}) with pytest.raises(NotFoundError): - workos.pipes.authorize_data_integration( - "test_slug", user_id="test_user_id" + workos.pipes.update_data_integration_api_key( + "test_slug", user_id="test_user_id", secret="test_secret" ) finally: workos.close() - def test_authorize_data_integration_rate_limited(self, httpx_mock): + def test_update_data_integration_api_key_rate_limited(self, httpx_mock): workos = WorkOSClient( api_key="sk_test_123", client_id="client_test", max_retries=0 ) @@ -164,53 +201,66 @@ def test_authorize_data_integration_rate_limited(self, httpx_mock): json={"message": "Slow down"}, ) with pytest.raises(RateLimitExceededError): - workos.pipes.authorize_data_integration( - "test_slug", user_id="test_user_id" + workos.pipes.update_data_integration_api_key( + "test_slug", user_id="test_user_id", secret="test_secret" ) finally: workos.close() - def test_authorize_data_integration_server_error(self, httpx_mock): + def test_update_data_integration_api_key_server_error(self, httpx_mock): workos = WorkOSClient( api_key="sk_test_123", client_id="client_test", max_retries=0 ) try: httpx_mock.add_response(status_code=500, json={"message": "Server error"}) with pytest.raises(ServerError): - workos.pipes.authorize_data_integration( - "test_slug", user_id="test_user_id" + workos.pipes.update_data_integration_api_key( + "test_slug", user_id="test_user_id", secret="test_secret" ) finally: workos.close() - def test_authorize_data_integration_bad_request(self, httpx_mock): + def test_update_data_integration_api_key_bad_request(self, httpx_mock): workos = WorkOSClient( api_key="sk_test_123", client_id="client_test", max_retries=0 ) try: httpx_mock.add_response(status_code=400, json={"message": "Bad request"}) with pytest.raises(BadRequestError): - workos.pipes.authorize_data_integration( - "test_slug", user_id="test_user_id" + workos.pipes.update_data_integration_api_key( + "test_slug", user_id="test_user_id", secret="test_secret" ) finally: workos.close() - def test_authorize_data_integration_unprocessable(self, httpx_mock): + def test_update_data_integration_api_key_unprocessable(self, httpx_mock): workos = WorkOSClient( api_key="sk_test_123", client_id="client_test", max_retries=0 ) try: httpx_mock.add_response(status_code=422, json={"message": "Unprocessable"}) with pytest.raises(UnprocessableEntityError): - workos.pipes.authorize_data_integration( - "test_slug", user_id="test_user_id" + workos.pipes.update_data_integration_api_key( + "test_slug", user_id="test_user_id", secret="test_secret" ) finally: workos.close() class TestAsyncPipes: + @pytest.mark.asyncio + async def test_update_data_integration_api_key(self, async_workos, httpx_mock): + httpx_mock.add_response(json=load_fixture("connected_account.json")) + result = await async_workos.pipes.update_data_integration_api_key( + "test_slug", user_id="test_user_id", secret="test_secret" + ) + assert isinstance(result, ConnectedAccount) + assert result.object == "connected_account" + assert result.id == "data_installation_01EHZNVPK3SFK441A1RGBFSHRT" + request = httpx_mock.get_request() + assert request.method == "PUT" + assert request.url.path.endswith("/data-integrations/test_slug/api-key") + @pytest.mark.asyncio async def test_authorize_data_integration(self, async_workos, httpx_mock): httpx_mock.add_response( @@ -228,6 +278,21 @@ async def test_authorize_data_integration(self, async_workos, httpx_mock): assert request.method == "POST" assert request.url.path.endswith("/data-integrations/test_slug/authorize") + @pytest.mark.asyncio + async def test_create_data_integration_credential(self, async_workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("data_integration_credentials_response.json") + ) + result = await async_workos.pipes.create_data_integration_credential( + "test_slug", user_id="test_user_id" + ) + assert isinstance(result, DataIntegrationCredentialsResponse) + assert result.active is True + assert result.error == "not_installed" + request = httpx_mock.get_request() + assert request.method == "POST" + assert request.url.path.endswith("/data-integrations/test_slug/credentials") + @pytest.mark.asyncio async def test_get_access_token(self, async_workos, httpx_mock): httpx_mock.add_response( @@ -321,46 +386,45 @@ async def test_list_user_data_providers_encodes_query_params( assert request.url.params["organization_id"] == "value organization_id/test" @pytest.mark.asyncio - async def test_authorize_data_integration_with_request_options( + async def test_update_data_integration_api_key_with_request_options( self, async_workos, httpx_mock ): - httpx_mock.add_response( - json=load_fixture("data_integration_authorize_url_response.json") - ) - await async_workos.pipes.authorize_data_integration( + httpx_mock.add_response(json=load_fixture("connected_account.json")) + await async_workos.pipes.update_data_integration_api_key( "test_slug", user_id="test_user_id", + secret="test_secret", request_options={"extra_headers": {"X-Custom": "value"}}, ) request = httpx_mock.get_request() assert request.headers["X-Custom"] == "value" @pytest.mark.asyncio - async def test_authorize_data_integration_unauthorized( + async def test_update_data_integration_api_key_unauthorized( self, async_workos, httpx_mock ): httpx_mock.add_response(status_code=401, json={"message": "Unauthorized"}) with pytest.raises(AuthenticationError): - await async_workos.pipes.authorize_data_integration( - "test_slug", user_id="test_user_id" + await async_workos.pipes.update_data_integration_api_key( + "test_slug", user_id="test_user_id", secret="test_secret" ) @pytest.mark.asyncio - async def test_authorize_data_integration_not_found(self, httpx_mock): + async def test_update_data_integration_api_key_not_found(self, httpx_mock): workos = AsyncWorkOSClient( api_key="sk_test_123", client_id="client_test", max_retries=0 ) try: httpx_mock.add_response(status_code=404, json={"message": "Not found"}) with pytest.raises(NotFoundError): - await workos.pipes.authorize_data_integration( - "test_slug", user_id="test_user_id" + await workos.pipes.update_data_integration_api_key( + "test_slug", user_id="test_user_id", secret="test_secret" ) finally: await workos.close() @pytest.mark.asyncio - async def test_authorize_data_integration_rate_limited(self, httpx_mock): + async def test_update_data_integration_api_key_rate_limited(self, httpx_mock): workos = AsyncWorkOSClient( api_key="sk_test_123", client_id="client_test", max_retries=0 ) @@ -371,50 +435,50 @@ async def test_authorize_data_integration_rate_limited(self, httpx_mock): json={"message": "Slow down"}, ) with pytest.raises(RateLimitExceededError): - await workos.pipes.authorize_data_integration( - "test_slug", user_id="test_user_id" + await workos.pipes.update_data_integration_api_key( + "test_slug", user_id="test_user_id", secret="test_secret" ) finally: await workos.close() @pytest.mark.asyncio - async def test_authorize_data_integration_server_error(self, httpx_mock): + async def test_update_data_integration_api_key_server_error(self, httpx_mock): workos = AsyncWorkOSClient( api_key="sk_test_123", client_id="client_test", max_retries=0 ) try: httpx_mock.add_response(status_code=500, json={"message": "Server error"}) with pytest.raises(ServerError): - await workos.pipes.authorize_data_integration( - "test_slug", user_id="test_user_id" + await workos.pipes.update_data_integration_api_key( + "test_slug", user_id="test_user_id", secret="test_secret" ) finally: await workos.close() @pytest.mark.asyncio - async def test_authorize_data_integration_bad_request(self, httpx_mock): + async def test_update_data_integration_api_key_bad_request(self, httpx_mock): workos = AsyncWorkOSClient( api_key="sk_test_123", client_id="client_test", max_retries=0 ) try: httpx_mock.add_response(status_code=400, json={"message": "Bad request"}) with pytest.raises(BadRequestError): - await workos.pipes.authorize_data_integration( - "test_slug", user_id="test_user_id" + await workos.pipes.update_data_integration_api_key( + "test_slug", user_id="test_user_id", secret="test_secret" ) finally: await workos.close() @pytest.mark.asyncio - async def test_authorize_data_integration_unprocessable(self, httpx_mock): + async def test_update_data_integration_api_key_unprocessable(self, httpx_mock): workos = AsyncWorkOSClient( api_key="sk_test_123", client_id="client_test", max_retries=0 ) try: httpx_mock.add_response(status_code=422, json={"message": "Unprocessable"}) with pytest.raises(UnprocessableEntityError): - await workos.pipes.authorize_data_integration( - "test_slug", user_id="test_user_id" + await workos.pipes.update_data_integration_api_key( + "test_slug", user_id="test_user_id", secret="test_secret" ) finally: await workos.close()