diff --git a/.changelog-pending/2026-09-14T13-28-03-0b4d419df1198eced531b9867bfbfdee80433fe2.md b/.changelog-pending/2026-09-14T13-28-03-0b4d419df1198eced531b9867bfbfdee80433fe2.md new file mode 100644 index 00000000..9b44e449 --- /dev/null +++ b/.changelog-pending/2026-09-14T13-28-03-0b4d419df1198eced531b9867bfbfdee80433fe2.md @@ -0,0 +1,102 @@ +* [#727](https://github.com/workos/workos-python/pull/727) feat(generated): regenerate from spec (4 changes) + + **Features** + * **[admin_portal](https://workos.com/docs/reference/admin-portal)**: + * Changed the format of `PortalLinkResponse.link` + * **[agents](https://workos.com/docs/reference/agents)**: + * Added parameter `AgentsSessions.list.organization_id` + * Changed the format of `ClaimViewResponse.user_code` + * Changed the format of `AgentAdminValidateCredentialRequest.credential` + * **[api_keys](https://workos.com/docs/reference/authkit/api-keys)**: + * Changed the format of `ValidateApiKey.value` + * Changed the format of `OrganizationApiKeyWithValue.value` + * Changed the format of `UserApiKeyWithValue.value` + * **[multi_factor_auth](https://workos.com/docs/reference/authkit/mfa)**: + * Changed the format of `AuthenticationChallenge.code` + * Changed the format of `AuthenticationFactorEnrolledTotp.secret` + * Changed the format of `AuthenticationFactorEnrolledTotp.qr_code` + * Changed the format of `AuthenticationFactorEnrolledTotp.uri` + * Changed the format of `AuthenticationChallengesVerifyRequest.code` + * **[pipes](https://workos.com/docs/reference/pipes)**: + * Added `supports_multiple_connections` to `DataIntegrationsVendCredentialsRequest` + * Added `supports_multiple_connections` to `DataIntegrationsGetUserTokenRequest` + * Added parameter `UserManagementDataProviders.getUserDataInstallation.supports_multiple_connections` + * Added parameter `UserManagementDataProviders.updateUserDataInstallation.supports_multiple_connections` + * Added parameter `UserManagementDataProviders.deleteUserDataInstallation.supports_multiple_connections` + * Added parameter `UserManagementDataProviders.getUserDataIntegrations.supports_multiple_connections` + * Added enum `PipesOwnership` + * Added `user` to `CreateDataIntegrationOwnership` + * Added `user` to `DataIntegrationOwnership` + * Added endpoint `GET /data-integrations/{slug}/organization` + * Added endpoint `PUT /data-integrations/{slug}/organization` + * Added endpoint `DELETE /data-integrations/{slug}/organization` + * Added parameter `Pipes.listDataIntegrations.ownership` + * Changed errors for endpoint `GET /data-integrations` + * Changed errors for endpoint `PUT /user_management/users/{user_id}/connected_accounts/{slug}` + * Changed errors for endpoint `DELETE /user_management/users/{user_id}/connected_accounts/{slug}` + * Changed the format of `DataIntegrationCredentialsResponseCredential.value` + * Added `connection_owner` to `DataIntegrationsUpsertApiKeyRequest` + * Added `connection_owner` to `DataIntegrationsUpsertClientCredentialsRequest` + * Added enum `DataIntegrationsUpsertApiKeyRequestConnectionOwner` + * Added enum `DataIntegrationsUpsertClientCredentialsRequestConnectionOwner` + * Added model `AccountSelectionRequiredError` + * Added `connected_account_id` to `DataIntegrationsUpsertApiKeyRequest` + * Added `connected_account_id` to `DataIntegrationsUpsertClientCredentialsRequest` + * Added `ownership` to `CreateDataIntegration` + * Added `ownership` to `DataIntegration` + * Added `connection_owner` to `DataIntegrationsVendCredentialsRequest` + * Added `connection_owner` to `DataIntegrationsGetUserTokenRequest` + * Added enum `CreateDataIntegrationOwnership` + * Added enum `DataIntegrationOwnership` + * Added enum `DataIntegrationsVendCredentialsRequestConnectionOwner` + * Added enum `DataIntegrationsGetUserTokenRequestConnectionOwner` + * Changed errors for endpoint `POST /data-integrations/{slug}/credentials` + * Changed errors for endpoint `POST /data-integrations/{provider}/token` + * **[sso](https://workos.com/docs/reference/sso)**: + * Changed the format of `CreateConnectionKeyPair.key` + * Changed the format of `TokenQuery.code` + * Changed the format of `SSOLogoutAuthorizeResponse.logout_url` + * **[user_management](https://workos.com/docs/reference/authkit/user)**: + * Added model `CreateAuthkitOAuthResource` + * Added model `AuthkitOAuthResource` + * Added service `UserManagementAuthkitOAuthResources` + * Changed the format of `DeviceAuthorizationResponse.verification_uri_complete` + * Changed the format of `VerifyEmailAddress.code` + * Changed the format of `ConfirmEmailChange.code` + * Changed the format of `MagicAuth.code` + * Changed the format of `UserInvite.accept_invitation_url` + * Changed the format of `EmailVerification.code` + * Changed the format of `PasswordReset.password_reset_url` + * Changed the format of `AuthenticateResponse.authkit_authorization_code` + * Changed the format of `DeviceAuthorizationResponse.device_code` + * Changed the format of `DeviceAuthorizationResponse.user_code` + * Changed the format of `AuthorizationCodeSessionAuthenticateRequest.code` + * Changed the format of `AuthorizationCodeSessionAuthenticateRequest.code_verifier` + * Changed the format of `MagicAuthCodeSessionAuthenticateRequest.code` + * Changed the format of `EmailVerificationCodeSessionAuthenticateRequest.code` + * Changed the format of `MfaTotpSessionAuthenticateRequest.code` + * Changed the format of `RadarEmailChallengeCodeSessionAuthenticateRequest.code` + * Changed the format of `RadarSmsChallengeCodeSessionAuthenticateRequest.code` + * Changed the format of `DeviceCodeSessionAuthenticateRequest.device_code` + * Changed the format of `Invitation.accept_invitation_url` + * Changed the format of `MagicAuthSendMagicAuthCodeAndReturnResponse.code` + * **[radar](https://workos.com/docs/reference/radar)**: + * Changed the format of `RadarChallenge.code` + * **[vault](https://workos.com/docs/reference/vault)**: + * Changed the format of `CreateObjectRequest.value` + * Changed the format of `VaultObject.value` + * Changed the format of `UpdateObjectRequest.value` + + **Fixes** + * **[pipes](https://workos.com/docs/reference/pipes)**: + * Removed `userland_user` from `CreateDataIntegrationOwnership` + * Removed `userland_user` from `DataIntegrationOwnership` + * Changed errors for endpoint `POST /user_management/users/{user_id}/connected_accounts/{slug}` + * Changed errors for endpoint `PUT /data-integrations/{slug}/api-key` + * Changed errors for endpoint `PUT /data-integrations/{slug}/client-credentials` + * Changed errors for endpoint `POST /data-integrations/{slug}/credentials` + * Changed errors for endpoint `POST /data-integrations/{provider}/token` + * Changed errors for endpoint `GET /user_management/users/{user_id}/connected_accounts/{slug}` + * Changed errors for endpoint `PUT /user_management/users/{user_id}/connected_accounts/{slug}` + * Changed errors for endpoint `DELETE /user_management/users/{user_id}/connected_accounts/{slug}` + * Changed the type of `DataIntegrationInstallation.user_id` diff --git a/.last-synced-sha b/.last-synced-sha index 2f412fd3..1c2b758d 100644 --- a/.last-synced-sha +++ b/.last-synced-sha @@ -1 +1 @@ -6fba233c50a5b651b2df7b859b75bfe715bc825a +d3fa61120f8b9e73972b9dc5f4b5ea2bc0526636 diff --git a/.oagen-manifest.json b/.oagen-manifest.json index 59c9d114..587c9f92 100644 --- a/.oagen-manifest.json +++ b/.oagen-manifest.json @@ -106,6 +106,7 @@ "src/workos/common/__init__.py", "src/workos/common/models/__init__.py", "src/workos/common/models/access_token_agent_registration_credential_issued_data_detail.py", + "src/workos/common/models/account_selection_required_error.py", "src/workos/common/models/action_authentication_denied.py", "src/workos/common/models/action_authentication_denied_data.py", "src/workos/common/models/action_user_registration_denied.py", @@ -280,6 +281,7 @@ "src/workos/common/models/create_connection_oidc_options_id_token_signature_algorithm.py", "src/workos/common/models/create_connection_oidc_options_token_authentication_method.py", "src/workos/common/models/create_data_integration_auth_methods.py", + "src/workos/common/models/create_data_integration_ownership.py", "src/workos/common/models/create_user_invite_options_locale.py", "src/workos/common/models/create_user_password_hash_type.py", "src/workos/common/models/create_user_password_salt_position.py", @@ -292,11 +294,16 @@ "src/workos/common/models/data_integration_credentials_input_type.py", "src/workos/common/models/data_integration_credentials_response_error.py", "src/workos/common/models/data_integration_custom_provider_authenticate_via.py", + "src/workos/common/models/data_integration_ownership.py", "src/workos/common/models/data_integration_state.py", + "src/workos/common/models/data_integrations_get_user_token_request_connection_owner.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", "src/workos/common/models/data_integrations_list_response_data_ownership.py", + "src/workos/common/models/data_integrations_upsert_api_key_request_connection_owner.py", + "src/workos/common/models/data_integrations_upsert_client_credentials_request_connection_owner.py", + "src/workos/common/models/data_integrations_vend_credentials_request_connection_owner.py", "src/workos/common/models/directory_group.py", "src/workos/common/models/directory_state.py", "src/workos/common/models/directory_type.py", @@ -690,6 +697,7 @@ "src/workos/pipes/models/data_integrations_upsert_api_key_request.py", "src/workos/pipes/models/data_integrations_upsert_client_credentials_request.py", "src/workos/pipes/models/data_integrations_vend_credentials_request.py", + "src/workos/pipes/models/pipes_ownership.py", "src/workos/pipes/models/update_custom_provider_definition.py", "src/workos/pipes/models/update_data_integration.py", "src/workos/pipes_provider/__init__.py", @@ -777,10 +785,12 @@ "src/workos/user_management/models/__init__.py", "src/workos/user_management/models/authenticate_response.py", "src/workos/user_management/models/authenticate_response_oauth_token.py", + "src/workos/user_management/models/authkit_oauth_resource.py", "src/workos/user_management/models/authorization_code_session_authenticate_request.py", "src/workos/user_management/models/authorized_connect_application_list_data.py", "src/workos/user_management/models/confirm_email_change.py", "src/workos/user_management/models/cors_origin_response.py", + "src/workos/user_management/models/create_authkit_oauth_resource.py", "src/workos/user_management/models/create_cors_origin.py", "src/workos/user_management/models/create_magic_code_and_return.py", "src/workos/user_management/models/create_password_reset.py", @@ -868,6 +878,7 @@ "src/workos/widgets/models/widget_session_token.py", "src/workos/widgets/models/widget_session_token_response.py", "tests/fixtures/access_token_agent_registration_credential_issued_data_detail.json", + "tests/fixtures/account_selection_required_error.json", "tests/fixtures/action_authentication_denied.json", "tests/fixtures/action_authentication_denied_data.json", "tests/fixtures/action_user_registration_denied.json", @@ -1032,6 +1043,7 @@ "tests/fixtures/authentication_sso_timed_out_data.json", "tests/fixtures/authentication_sso_timed_out_data_error.json", "tests/fixtures/authentication_sso_timed_out_data_sso.json", + "tests/fixtures/authkit_oauth_resource.json", "tests/fixtures/authorization_check.json", "tests/fixtures/authorization_code_session_authenticate_request.json", "tests/fixtures/authorization_permission.json", @@ -1070,6 +1082,7 @@ "tests/fixtures/connection_saml_certificate_renewed_data_certificate.json", "tests/fixtures/connection_saml_certificate_renewed_data_connection.json", "tests/fixtures/cors_origin_response.json", + "tests/fixtures/create_authkit_oauth_resource.json", "tests/fixtures/create_authorization_permission.json", "tests/fixtures/create_authorization_resource.json", "tests/fixtures/create_connection.json", @@ -1249,6 +1262,7 @@ "tests/fixtures/list_audit_log_action.json", "tests/fixtures/list_audit_log_schema.json", "tests/fixtures/list_authentication_factor.json", + "tests/fixtures/list_authkit_oauth_resource.json", "tests/fixtures/list_authorization_permission.json", "tests/fixtures/list_authorization_resource.json", "tests/fixtures/list_authorized_connect_application_list_data.json", @@ -2550,6 +2564,30 @@ "POST /agents/blueprints/{agent_blueprint_id}/tokens/validate": { "sdkMethod": "validate_blueprint_token", "service": "agents" + }, + "GET /data-integrations/{slug}/organization": { + "sdkMethod": "list_data_integration_organization", + "service": "pipes" + }, + "PUT /data-integrations/{slug}/organization": { + "sdkMethod": "update_data_integration_organization", + "service": "pipes" + }, + "DELETE /data-integrations/{slug}/organization": { + "sdkMethod": "delete_data_integration_organization", + "service": "pipes" + }, + "GET /user_management/authkit_oauth_resources": { + "sdkMethod": "list_authkit_oauth_resources", + "service": "user_management" + }, + "POST /user_management/authkit_oauth_resources": { + "sdkMethod": "create_authkit_oauth_resource", + "service": "user_management" + }, + "DELETE /user_management/authkit_oauth_resources/{id}": { + "sdkMethod": "delete_authkit_oauth_resource", + "service": "user_management" } } } diff --git a/src/workos/admin_portal/_resource.py b/src/workos/admin_portal/_resource.py index 9721a572..79c09348 100644 --- a/src/workos/admin_portal/_resource.py +++ b/src/workos/admin_portal/_resource.py @@ -34,8 +34,8 @@ def generate_link( Generate a Portal Link scoped to an Organization. Args: - return_url: The URL to go to when an admin clicks on your logo in the Admin Portal. If not specified, the return URL configured on the [Redirects](https://dashboard.workos.com/redirects) page will be used. - success_url: The URL to redirect the admin to when they finish setup. If not specified, the success URL configured on the [Redirects](https://dashboard.workos.com/redirects) page will be used. + return_url: The URL to go to when an admin clicks on your logo in the Admin Portal. If not specified, the return URL configured on the [Admin Portal](https://dashboard.workos.com/admin-portal) page will be used. + success_url: The URL to redirect the admin to when they finish setup. If not specified, the success URL configured on the [Admin Portal](https://dashboard.workos.com/admin-portal) page will be used. organization: An [Organization](https://workos.com/docs/reference/organization) identifier. intent: The intent of the Admin Portal. - `sso` - Launch Admin Portal for creating SSO connections @@ -101,8 +101,8 @@ async def generate_link( Generate a Portal Link scoped to an Organization. Args: - return_url: The URL to go to when an admin clicks on your logo in the Admin Portal. If not specified, the return URL configured on the [Redirects](https://dashboard.workos.com/redirects) page will be used. - success_url: The URL to redirect the admin to when they finish setup. If not specified, the success URL configured on the [Redirects](https://dashboard.workos.com/redirects) page will be used. + return_url: The URL to go to when an admin clicks on your logo in the Admin Portal. If not specified, the return URL configured on the [Admin Portal](https://dashboard.workos.com/admin-portal) page will be used. + success_url: The URL to redirect the admin to when they finish setup. If not specified, the success URL configured on the [Admin Portal](https://dashboard.workos.com/admin-portal) page will be used. organization: An [Organization](https://workos.com/docs/reference/organization) identifier. intent: The intent of the Admin Portal. - `sso` - Launch Admin Portal for creating SSO connections diff --git a/src/workos/admin_portal/models/generate_link.py b/src/workos/admin_portal/models/generate_link.py index c3bd20f5..e02c4881 100644 --- a/src/workos/admin_portal/models/generate_link.py +++ b/src/workos/admin_portal/models/generate_link.py @@ -17,9 +17,9 @@ class GenerateLink: organization: str """An [Organization](https://workos.com/docs/reference/organization) identifier.""" return_url: str | None = None - """The URL to go to when an admin clicks on your logo in the Admin Portal. If not specified, the return URL configured on the [Redirects](https://dashboard.workos.com/redirects) page will be used.""" + """The URL to go to when an admin clicks on your logo in the Admin Portal. If not specified, the return URL configured on the [Admin Portal](https://dashboard.workos.com/admin-portal) page will be used.""" success_url: str | None = None - """The URL to redirect the admin to when they finish setup. If not specified, the success URL configured on the [Redirects](https://dashboard.workos.com/redirects) page will be used.""" + """The URL to redirect the admin to when they finish setup. If not specified, the success URL configured on the [Admin Portal](https://dashboard.workos.com/admin-portal) page will be used.""" intent: GenerateLinkIntent | None = None """ The intent of the Admin Portal. diff --git a/src/workos/agents/_resource.py b/src/workos/agents/_resource.py index b8205e78..34ade28a 100644 --- a/src/workos/agents/_resource.py +++ b/src/workos/agents/_resource.py @@ -596,6 +596,7 @@ def list_sessions( before: str | None = None, after: str | None = None, order: PaginationOrder | str | None = "desc", + organization_id: str | None = None, agent_blueprint_id: str | None = None, agent_instance_id: str | None = None, request_options: RequestOptions | None = None, @@ -609,6 +610,7 @@ def list_sessions( before: An object ID that defines your place in the list. When the ID is not present, you are at the end of the list. For example, if you make a list request and receive 100 objects, ending with `"obj_123"`, your subsequent call can include `before="obj_123"` to fetch a new batch of objects before `"obj_123"`. after: An object ID that defines your place in the list. When the ID is not present, you are at the end of the list. For example, if you make a list request and receive 100 objects, ending with `"obj_123"`, your subsequent call can include `after="obj_123"` to fetch a new batch of objects after `"obj_123"`. order: Order the results by the creation time. Supported values are `"asc"` (ascending), `"desc"` (descending), and `"normal"` (descending with reversed cursor semantics where `before` fetches older records and `after` fetches newer records). Defaults to `desc`. + organization_id: Only return sessions of instances acting within this organization. agent_blueprint_id: Only return sessions of instances minted from this blueprint. agent_instance_id: Only return sessions belonging to this agent instance. request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. @@ -629,6 +631,7 @@ def list_sessions( "before": before, "after": after, "order": enum_value(order) if order is not None else None, + "organization_id": organization_id, "agent_blueprint_id": agent_blueprint_id, "agent_instance_id": agent_instance_id, }.items() @@ -1265,6 +1268,7 @@ async def list_sessions( before: str | None = None, after: str | None = None, order: PaginationOrder | str | None = "desc", + organization_id: str | None = None, agent_blueprint_id: str | None = None, agent_instance_id: str | None = None, request_options: RequestOptions | None = None, @@ -1278,6 +1282,7 @@ async def list_sessions( before: An object ID that defines your place in the list. When the ID is not present, you are at the end of the list. For example, if you make a list request and receive 100 objects, ending with `"obj_123"`, your subsequent call can include `before="obj_123"` to fetch a new batch of objects before `"obj_123"`. after: An object ID that defines your place in the list. When the ID is not present, you are at the end of the list. For example, if you make a list request and receive 100 objects, ending with `"obj_123"`, your subsequent call can include `after="obj_123"` to fetch a new batch of objects after `"obj_123"`. order: Order the results by the creation time. Supported values are `"asc"` (ascending), `"desc"` (descending), and `"normal"` (descending with reversed cursor semantics where `before` fetches older records and `after` fetches newer records). Defaults to `desc`. + organization_id: Only return sessions of instances acting within this organization. agent_blueprint_id: Only return sessions of instances minted from this blueprint. agent_instance_id: Only return sessions belonging to this agent instance. request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. @@ -1298,6 +1303,7 @@ async def list_sessions( "before": before, "after": after, "order": enum_value(order) if order is not None else None, + "organization_id": organization_id, "agent_blueprint_id": agent_blueprint_id, "agent_instance_id": agent_instance_id, }.items() diff --git a/src/workos/api_keys/models/validate_api_key.py b/src/workos/api_keys/models/validate_api_key.py index 36737894..276ccaee 100644 --- a/src/workos/api_keys/models/validate_api_key.py +++ b/src/workos/api_keys/models/validate_api_key.py @@ -1,9 +1,32 @@ # This file is auto-generated by oagen. Do not edit. -from typing import TypeAlias +from __future__ import annotations -from workos.sso.models.create_saml_idp_signing_certificate import ( - CreateSAMLIdpSigningCertificate, -) +from dataclasses import dataclass +from typing import Any -ValidateApiKey: TypeAlias = CreateSAMLIdpSigningCertificate +from workos._types import _raise_deserialize_error + + +@dataclass(slots=True) +class ValidateApiKey: + """Validate Api Key model.""" + + value: str + """The value for an API key.""" + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> ValidateApiKey: + """Deserialize from a dictionary.""" + try: + return cls( + value=data["value"], + ) + except (KeyError, ValueError) as e: + _raise_deserialize_error("ValidateApiKey", e) + + def to_dict(self) -> dict[str, Any]: + """Serialize to a dictionary.""" + result: dict[str, Any] = {} + result["value"] = self.value + return result diff --git a/src/workos/common/__init__.py b/src/workos/common/__init__.py index df880899..931b156b 100644 --- a/src/workos/common/__init__.py +++ b/src/workos/common/__init__.py @@ -4,6 +4,7 @@ from .models import ( AccessTokenAgentRegistrationCredentialIssuedDataDetail as AccessTokenAgentRegistrationCredentialIssuedDataDetail, ) +from .models import AccountSelectionRequiredError as AccountSelectionRequiredError from .models import ActionAuthenticationDenied as ActionAuthenticationDenied from .models import ActionAuthenticationDeniedData as ActionAuthenticationDeniedData from .models import ActionUserRegistrationDenied as ActionUserRegistrationDenied @@ -262,6 +263,7 @@ CreateConnectionOIDCOptionsTokenAuthenticationMethod as CreateConnectionOIDCOptionsTokenAuthenticationMethod, ) from .models import CreateDataIntegrationAuthMethods as CreateDataIntegrationAuthMethods +from .models import CreateDataIntegrationOwnership as CreateDataIntegrationOwnership from .models import CreateUserInviteOptionsLocale as CreateUserInviteOptionsLocale from .models import CreateUserPasswordHashType as CreateUserPasswordHashType from .models import CreateUserPasswordSaltPosition as CreateUserPasswordSaltPosition @@ -286,6 +288,10 @@ from .models import ( DataIntegrationCustomProviderAuthenticateVia as DataIntegrationCustomProviderAuthenticateVia, ) +from .models import DataIntegrationOwnership as DataIntegrationOwnership +from .models import ( + DataIntegrationsGetUserTokenRequestConnectionOwner as DataIntegrationsGetUserTokenRequestConnectionOwner, +) from .models import ( DataIntegrationsListResponseDataAuthMethods as DataIntegrationsListResponseDataAuthMethods, ) @@ -299,6 +305,15 @@ DataIntegrationsListResponseDataOwnership as DataIntegrationsListResponseDataOwnership, ) from .models import DataIntegrationState as DataIntegrationState +from .models import ( + DataIntegrationsUpsertApiKeyRequestConnectionOwner as DataIntegrationsUpsertApiKeyRequestConnectionOwner, +) +from .models import ( + DataIntegrationsUpsertClientCredentialsRequestConnectionOwner as DataIntegrationsUpsertClientCredentialsRequestConnectionOwner, +) +from .models import ( + DataIntegrationsVendCredentialsRequestConnectionOwner as DataIntegrationsVendCredentialsRequestConnectionOwner, +) from .models import DirectoryGroup as DirectoryGroup from .models import DirectoryState as DirectoryState from .models import DirectoryType as DirectoryType diff --git a/src/workos/common/models/__init__.py b/src/workos/common/models/__init__.py index 8e5c38d8..5890f9ad 100644 --- a/src/workos/common/models/__init__.py +++ b/src/workos/common/models/__init__.py @@ -3,6 +3,9 @@ from .access_token_agent_registration_credential_issued_data_detail import ( AccessTokenAgentRegistrationCredentialIssuedDataDetail as AccessTokenAgentRegistrationCredentialIssuedDataDetail, ) +from .account_selection_required_error import ( + AccountSelectionRequiredError as AccountSelectionRequiredError, +) from .action_authentication_denied import ( ActionAuthenticationDenied as ActionAuthenticationDenied, ) @@ -429,6 +432,9 @@ from .create_data_integration_auth_methods import ( CreateDataIntegrationAuthMethods as CreateDataIntegrationAuthMethods, ) +from .create_data_integration_ownership import ( + CreateDataIntegrationOwnership as CreateDataIntegrationOwnership, +) from .create_user_invite_options_locale import ( CreateUserInviteOptionsLocale as CreateUserInviteOptionsLocale, ) @@ -465,7 +471,13 @@ from .data_integration_custom_provider_authenticate_via import ( DataIntegrationCustomProviderAuthenticateVia as DataIntegrationCustomProviderAuthenticateVia, ) +from .data_integration_ownership import ( + DataIntegrationOwnership as DataIntegrationOwnership, +) from .data_integration_state import DataIntegrationState as DataIntegrationState +from .data_integrations_get_user_token_request_connection_owner import ( + DataIntegrationsGetUserTokenRequestConnectionOwner as DataIntegrationsGetUserTokenRequestConnectionOwner, +) from .data_integrations_list_response_data_auth_methods import ( DataIntegrationsListResponseDataAuthMethods as DataIntegrationsListResponseDataAuthMethods, ) @@ -478,6 +490,15 @@ from .data_integrations_list_response_data_ownership import ( DataIntegrationsListResponseDataOwnership as DataIntegrationsListResponseDataOwnership, ) +from .data_integrations_upsert_api_key_request_connection_owner import ( + DataIntegrationsUpsertApiKeyRequestConnectionOwner as DataIntegrationsUpsertApiKeyRequestConnectionOwner, +) +from .data_integrations_upsert_client_credentials_request_connection_owner import ( + DataIntegrationsUpsertClientCredentialsRequestConnectionOwner as DataIntegrationsUpsertClientCredentialsRequestConnectionOwner, +) +from .data_integrations_vend_credentials_request_connection_owner import ( + DataIntegrationsVendCredentialsRequestConnectionOwner as DataIntegrationsVendCredentialsRequestConnectionOwner, +) from .directory_group import DirectoryGroup as DirectoryGroup from .directory_state import DirectoryState as DirectoryState from .directory_type import DirectoryType as DirectoryType diff --git a/src/workos/common/models/account_selection_required_error.py b/src/workos/common/models/account_selection_required_error.py new file mode 100644 index 00000000..403ca165 --- /dev/null +++ b/src/workos/common/models/account_selection_required_error.py @@ -0,0 +1,36 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Literal + +from workos._types import _raise_deserialize_error + + +@dataclass(slots=True) +class AccountSelectionRequiredError: + """Account Selection Required Error model.""" + + code: Literal["account_selection_required"] + """Indicates that several connected accounts match and the request must name one.""" + message: str + """A human-readable explanation of the ambiguity.""" + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> AccountSelectionRequiredError: + """Deserialize from a dictionary.""" + try: + return cls( + code=data.get("code", "account_selection_required"), + message=data["message"], + ) + except (KeyError, ValueError) as e: + _raise_deserialize_error("AccountSelectionRequiredError", 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/create_data_integration_ownership.py b/src/workos/common/models/create_data_integration_ownership.py new file mode 100644 index 00000000..f2eef58b --- /dev/null +++ b/src/workos/common/models/create_data_integration_ownership.py @@ -0,0 +1,27 @@ +# This file is auto-generated by oagen. Do not edit. + +"""Enumeration of create data integration ownership values.""" + +from __future__ import annotations + +from enum import Enum +from typing import Literal, TypeAlias + + +class CreateDataIntegrationOwnership(str, Enum): + """Known values for CreateDataIntegrationOwnership.""" + + USER = "user" + ORGANIZATION = "organization" + + @classmethod + def _missing_(cls, value: object) -> CreateDataIntegrationOwnership | None: + if not isinstance(value, str): + return None + unknown = str.__new__(cls, value) + unknown._name_ = value.upper() + unknown._value_ = value + return unknown + + +CreateDataIntegrationOwnershipLiteral: TypeAlias = Literal["user", "organization"] diff --git a/src/workos/common/models/data_integration_ownership.py b/src/workos/common/models/data_integration_ownership.py new file mode 100644 index 00000000..98aac778 --- /dev/null +++ b/src/workos/common/models/data_integration_ownership.py @@ -0,0 +1,8 @@ +# This file is auto-generated by oagen. Do not edit. + +from typing import TypeAlias + +from .create_data_integration_ownership import CreateDataIntegrationOwnership + +DataIntegrationOwnership: TypeAlias = CreateDataIntegrationOwnership +__all__ = ["DataIntegrationOwnership"] diff --git a/src/workos/common/models/data_integrations_get_user_token_request_connection_owner.py b/src/workos/common/models/data_integrations_get_user_token_request_connection_owner.py new file mode 100644 index 00000000..49047c49 --- /dev/null +++ b/src/workos/common/models/data_integrations_get_user_token_request_connection_owner.py @@ -0,0 +1,10 @@ +# This file is auto-generated by oagen. Do not edit. + +from typing import TypeAlias + +from .create_data_integration_ownership import CreateDataIntegrationOwnership + +DataIntegrationsGetUserTokenRequestConnectionOwner: TypeAlias = ( + CreateDataIntegrationOwnership +) +__all__ = ["DataIntegrationsGetUserTokenRequestConnectionOwner"] diff --git a/src/workos/common/models/data_integrations_upsert_api_key_request_connection_owner.py b/src/workos/common/models/data_integrations_upsert_api_key_request_connection_owner.py new file mode 100644 index 00000000..3a5630ac --- /dev/null +++ b/src/workos/common/models/data_integrations_upsert_api_key_request_connection_owner.py @@ -0,0 +1,10 @@ +# This file is auto-generated by oagen. Do not edit. + +from typing import TypeAlias + +from .create_data_integration_ownership import CreateDataIntegrationOwnership + +DataIntegrationsUpsertApiKeyRequestConnectionOwner: TypeAlias = ( + CreateDataIntegrationOwnership +) +__all__ = ["DataIntegrationsUpsertApiKeyRequestConnectionOwner"] diff --git a/src/workos/common/models/data_integrations_upsert_client_credentials_request_connection_owner.py b/src/workos/common/models/data_integrations_upsert_client_credentials_request_connection_owner.py new file mode 100644 index 00000000..f67f8275 --- /dev/null +++ b/src/workos/common/models/data_integrations_upsert_client_credentials_request_connection_owner.py @@ -0,0 +1,10 @@ +# This file is auto-generated by oagen. Do not edit. + +from typing import TypeAlias + +from .create_data_integration_ownership import CreateDataIntegrationOwnership + +DataIntegrationsUpsertClientCredentialsRequestConnectionOwner: TypeAlias = ( + CreateDataIntegrationOwnership +) +__all__ = ["DataIntegrationsUpsertClientCredentialsRequestConnectionOwner"] diff --git a/src/workos/common/models/data_integrations_vend_credentials_request_connection_owner.py b/src/workos/common/models/data_integrations_vend_credentials_request_connection_owner.py new file mode 100644 index 00000000..6569cbfb --- /dev/null +++ b/src/workos/common/models/data_integrations_vend_credentials_request_connection_owner.py @@ -0,0 +1,10 @@ +# This file is auto-generated by oagen. Do not edit. + +from typing import TypeAlias + +from .create_data_integration_ownership import CreateDataIntegrationOwnership + +DataIntegrationsVendCredentialsRequestConnectionOwner: TypeAlias = ( + CreateDataIntegrationOwnership +) +__all__ = ["DataIntegrationsVendCredentialsRequestConnectionOwner"] diff --git a/src/workos/common/models/resource_export_completed_data_resource_type.py b/src/workos/common/models/resource_export_completed_data_resource_type.py index 47f02db0..4d08f5cc 100644 --- a/src/workos/common/models/resource_export_completed_data_resource_type.py +++ b/src/workos/common/models/resource_export_completed_data_resource_type.py @@ -16,6 +16,7 @@ class ResourceExportCompletedDataResourceType(str, Enum): EVENTS = "events" SESSIONS = "sessions" AUDIT_LOG_EVENTS = "auditLogEvents" + CONNECTIONS = "connections" @classmethod def _missing_(cls, value: object) -> ResourceExportCompletedDataResourceType | None: @@ -28,5 +29,5 @@ def _missing_(cls, value: object) -> ResourceExportCompletedDataResourceType | N ResourceExportCompletedDataResourceTypeLiteral: TypeAlias = Literal[ - "users", "organizations", "events", "sessions", "auditLogEvents" + "users", "organizations", "events", "sessions", "auditLogEvents", "connections" ] diff --git a/src/workos/organizations/models/audit_logs_retention.py b/src/workos/organizations/models/audit_logs_retention.py index 6a557706..da4e68d1 100644 --- a/src/workos/organizations/models/audit_logs_retention.py +++ b/src/workos/organizations/models/audit_logs_retention.py @@ -13,7 +13,7 @@ class AuditLogsRetention: """Audit Logs Retention model.""" retention_period_in_days: int | None - """The number of days Audit Log events will be retained before being permanently deleted. Valid values are 30 and 365.""" + """The number of days Audit Log events will be retained before being permanently deleted. Valid values are 30 through 330 in 30-day increments and 365 through 3650 in 365-day increments.""" @classmethod def from_dict(cls, data: dict[str, Any]) -> AuditLogsRetention: diff --git a/src/workos/pipes/_resource.py b/src/workos/pipes/_resource.py index 950eb8c8..a9287d72 100644 --- a/src/workos/pipes/_resource.py +++ b/src/workos/pipes/_resource.py @@ -14,6 +14,21 @@ from workos.common.models.create_data_integration_auth_methods import ( CreateDataIntegrationAuthMethods, ) +from workos.common.models.create_data_integration_ownership import ( + CreateDataIntegrationOwnership, +) +from workos.common.models.data_integrations_get_user_token_request_connection_owner import ( + DataIntegrationsGetUserTokenRequestConnectionOwner, +) +from workos.common.models.data_integrations_upsert_api_key_request_connection_owner import ( + DataIntegrationsUpsertApiKeyRequestConnectionOwner, +) +from workos.common.models.data_integrations_upsert_client_credentials_request_connection_owner import ( + DataIntegrationsUpsertClientCredentialsRequestConnectionOwner, +) +from workos.common.models.data_integrations_vend_credentials_request_connection_owner import ( + DataIntegrationsVendCredentialsRequestConnectionOwner, +) from workos.common.models.pagination_order import PaginationOrder from .._pagination import AsyncPage, SyncPage @@ -27,6 +42,7 @@ DataIntegrationCredentialsInput, DataIntegrationCredentialsResponse, DataIntegrationsListResponse, + PipesOwnership, UpdateCustomProviderDefinition, ) @@ -44,17 +60,19 @@ def list_data_integrations( before: str | None = None, after: str | None = None, order: PaginationOrder | str | None = "desc", + ownership: PipesOwnership | str | None = None, request_options: RequestOptions | None = None, ) -> SyncPage[DataIntegration]: """List data integrations - Lists the environment's data integrations configured with `custom` or `organization` credentials, including custom providers and API key integrations. + Lists the environment's data integrations configured with `custom` or `organization` credentials, including custom providers and API key integrations. Both user-owned and organization-owned roots are returned, each as its own row with an `ownership`; filter with `ownership` to return only one kind. Args: limit: Upper limit on the number of objects to return, between `1` and `100`. Defaults to `10`. before: An object ID that defines your place in the list. When the ID is not present, you are at the end of the list. For example, if you make a list request and receive 100 objects, ending with `"obj_123"`, your subsequent call can include `before="obj_123"` to fetch a new batch of objects before `"obj_123"`. after: An object ID that defines your place in the list. When the ID is not present, you are at the end of the list. For example, if you make a list request and receive 100 objects, ending with `"obj_123"`, your subsequent call can include `after="obj_123"` to fetch a new batch of objects after `"obj_123"`. order: Order the results by the creation time. Supported values are `"asc"` (ascending), `"desc"` (descending), and `"normal"` (descending with reversed cursor semantics where `before` fetches older records and `after` fetches newer records). Defaults to `desc`. + ownership: Only return Data Integrations with this ownership: `user` for the integrations users connect their own accounts to, or `organization` for the roots organizations connect to. Omit to return both. request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. Returns: @@ -62,6 +80,8 @@ def list_data_integrations( Raises: 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. """ @@ -72,6 +92,7 @@ def list_data_integrations( "before": before, "after": after, "order": enum_value(order) if order is not None else None, + "ownership": enum_value(ownership) if ownership is not None else None, }.items() if v is not None } @@ -87,6 +108,7 @@ def create_data_integration( self, *, provider: str, + ownership: CreateDataIntegrationOwnership | str | None = None, description: str | None | NotGiven = NOT_GIVEN, enabled: bool | None = None, scopes: list[str] | None | NotGiven = NOT_GIVEN, @@ -99,10 +121,11 @@ def create_data_integration( ) -> DataIntegration: """Create a data integration - Creates a data integration for a provider. Set `credentials.type` to `custom` to use your own OAuth app credentials or `organization` to have each organization supply its own. Set `auth_methods` to `["api_key"]` to create an API key integration; you may optionally supply an `api_key` block to install a first tenant in the same call. Set `auth_methods` to `["client_credentials"]` to create a client-credentials integration; client credentials are installed per-tenant afterwards. For a built-in provider, pass its slug as `provider`. For a custom provider, pass a new slug plus a `custom_provider` definition. + Creates a data integration for a provider. Set `credentials.type` to `custom` to use your own OAuth app credentials or `organization` to have each organization supply its own. Set `auth_methods` to `["api_key"]` to create an API key integration; you may optionally supply an `api_key` block to install a first tenant in the same call. Set `auth_methods` to `["client_credentials"]` to create a client-credentials integration; client credentials are installed per-tenant afterwards. Set `ownership` to `organization` to create the integration organizations connect to instead of the default user-owned one; a provider may have one of each. For a built-in provider, pass its slug as `provider`. For a custom provider, pass a new slug plus a `custom_provider` definition, or the slug of an existing custom provider (without `custom_provider`) to add the other ownership. Args: provider: The provider to create a Data Integration for. For a built-in provider use its slug (e.g. `github`, `slack`). For a custom provider, this is the new provider slug and `custom_provider` must be supplied. A custom provider slug cannot shadow an existing global provider slug. + ownership: Who owns the Data Integration. `user` (the default) creates the integration users connect their own accounts to; `organization` creates the root organizations connect to. Ownership is fixed at creation, and one integration of each ownership may exist per provider. Independent of `credentials.type`. description: An optional description of the Data Integration. enabled: Whether the Data Integration is enabled. Defaults to `false`. scopes: The OAuth scopes to request for the Data Integration. Defaults to the provider's configured scopes when omitted. @@ -130,6 +153,7 @@ def create_data_integration( k: v for k, v in { "provider": provider, + "ownership": enum_value(ownership) if ownership is not None else None, "enabled": enabled, "auth_methods": auth_methods, "config": config, @@ -163,7 +187,7 @@ def get_data_integration( ) -> DataIntegration: """Get a data integration - Retrieves a data integration by its slug. + Retrieves the user-owned data integration by its slug. Args: slug: The slug identifier of the data integration. @@ -199,7 +223,7 @@ def update_data_integration( ) -> DataIntegration: """Update a data integration - Updates the description, enabled state, or custom credentials of a data integration. For custom providers, `custom_provider` updates the OAuth definition. + Updates the description, enabled state, or custom credentials of the user-owned data integration. For custom providers, `custom_provider` updates the OAuth definition. Args: slug: The slug identifier of the data integration. @@ -258,7 +282,7 @@ def delete_data_integration( ) -> None: """Delete a data integration - Deletes a data integration and all of its connected installations. For a custom provider, also deletes the custom provider definition. + Deletes the user-owned data integration and all of its connected installations. For a custom provider, the provider definition is deleted once no organization-owned root references it either. Args: slug: The slug identifier of the data integration. @@ -283,16 +307,22 @@ def update_data_integration_api_key( user_id: str, secret: str, organization_id: str | None = None, + connected_account_id: str | None = None, + connection_owner: DataIntegrationsUpsertApiKeyRequestConnectionOwner + | str + | None = None, request_options: RequestOptions | None = 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. + Creates or updates an API-key-based installation for the specified integration, owned by the user or, when `connection_owner` is `organization`, shared by the organization. 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. + organization_id: An [Organization](https://workos.com/docs/reference/organization) identifier. Optional parameter to scope the connection to a specific organization. Required when `connection_owner` is `organization`. + connected_account_id: A [connected account](https://workos.com/docs/reference/pipes/connected-account) identifier. Use this to rotate a specific existing connection. + connection_owner: Whose connection to create or rotate. `user` (the default) addresses the connection owned by `user_id`. `organization` addresses the connection shared by every member of `organization_id`; `user_id` then identifies the member performing the request and must be an active member of the 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. @@ -314,6 +344,10 @@ def update_data_integration_api_key( for k, v in { "user_id": user_id, "organization_id": organization_id, + "connected_account_id": connected_account_id, + "connection_owner": enum_value(connection_owner) + if connection_owner is not None + else None, "secret": secret, }.items() if v is not None @@ -385,17 +419,23 @@ def update_data_integration_client_credentials( client_id: str, client_secret: str, organization_id: str | None = None, + connected_account_id: str | None = None, + connection_owner: DataIntegrationsUpsertClientCredentialsRequestConnectionOwner + | str + | None = None, config: dict[str, str] | None = None, request_options: RequestOptions | None = None, ) -> ConnectedAccount: """Upsert client credentials for a connected account - Creates or updates a client-credentials-based installation for the specified integration and user. If an installation already exists, the stored client credentials are rotated to the new values. + Creates or updates a client-credentials-based installation for the specified integration, owned by the user or, when `connection_owner` is `organization`, shared by the organization. If an installation already exists, the stored client credentials are rotated to the new values. 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. + organization_id: An [Organization](https://workos.com/docs/reference/organization) identifier. Optional parameter to scope the connection to a specific organization. Required when `connection_owner` is `organization`. + connected_account_id: A [connected account](https://workos.com/docs/reference/pipes/connected-account) identifier. Use this to rotate a specific existing connection. + connection_owner: Whose connection to create or rotate. `user` (the default) addresses the connection owned by `user_id`. `organization` addresses the connection shared by every member of `organization_id`; `user_id` then identifies the member performing the request and must be an active member of the organization. client_id: The OAuth client ID to store for this integration. client_secret: The OAuth client secret to store for this integration. config: Provider-specific configuration values collected for this installation, keyed by the provider's config field descriptors. @@ -419,6 +459,10 @@ def update_data_integration_client_credentials( for k, v in { "user_id": user_id, "organization_id": organization_id, + "connected_account_id": connected_account_id, + "connection_owner": enum_value(connection_owner) + if connection_owner is not None + else None, "client_id": client_id, "client_secret": client_secret, "config": config, @@ -440,6 +484,10 @@ def create_data_integration_credential( user_id: str, organization_id: str | None = None, connected_account_id: str | None = None, + connection_owner: DataIntegrationsVendCredentialsRequestConnectionOwner + | str + | None = None, + supports_multiple_connections: bool | None = None, request_options: RequestOptions | None = None, ) -> DataIntegrationCredentialsResponse: """Vend credentials for a connected account @@ -448,9 +496,11 @@ def create_data_integration_credential( 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. + user_id: A [User](https://workos.com/docs/reference/authkit/user) identifier. When `connection_owner` is `organization`, this is the user the credentials are vended on behalf of; they must be an active member of the organization. + organization_id: An [Organization](https://workos.com/docs/reference/organization) identifier. Optional parameter to scope the connection to a specific organization. Required when `connection_owner` is `organization`. connected_account_id: A [connected account](https://workos.com/docs/reference/pipes/connected-account) identifier. Use this to select a specific connection when the user has several for this provider. + connection_owner: Which connection to vend from. `user` (the default) vends the user's own connection and requires `user_id`. `organization` vends the organization's shared connection and requires `organization_id`. + supports_multiple_connections: Set to `true` to use the plural connection contract. If no `connected_account_id` is supplied and several connections match, the request returns `account_selection_required`. When omitted or `false`, only the compatibility connection is considered. request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. Returns: @@ -459,6 +509,7 @@ def create_data_integration_credential( 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). ConflictError: If a conflict occurs (409). UnprocessableEntityError: If the request data is unprocessable (422). @@ -471,6 +522,10 @@ def create_data_integration_credential( "user_id": user_id, "organization_id": organization_id, "connected_account_id": connected_account_id, + "connection_owner": enum_value(connection_owner) + if connection_owner is not None + else None, + "supports_multiple_connections": supports_multiple_connections, }.items() if v is not None } @@ -482,6 +537,127 @@ def create_data_integration_credential( request_options=request_options, ) + def list_data_integration_organization( + self, + slug: str, + *, + request_options: RequestOptions | None = None, + ) -> DataIntegration: + """Get an organization-owned data integration + + Retrieves the organization-owned data integration for a provider by its slug. The `/organization` suffix selects the environment-level organization-owned root for the provider; it does not name a particular organization. + + Args: + slug: The slug identifier of the data integration. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + DataIntegration + + Raises: + 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. + """ + return self._client.request( + method="get", + path=("data-integrations", str(slug), "organization"), + model=DataIntegration, + request_options=request_options, + ) + + def update_data_integration_organization( + self, + slug: str, + *, + description: str | None | NotGiven = NOT_GIVEN, + enabled: bool | None = None, + scopes: list[str] | None | NotGiven = NOT_GIVEN, + credentials: DataIntegrationCredentialsInput | None = None, + api_key: ApiKeyInstallation | None = None, + custom_provider: UpdateCustomProviderDefinition | None = None, + request_options: RequestOptions | None = None, + ) -> DataIntegration: + """Update an organization-owned data integration + + Updates the description, enabled state, or custom credentials of the organization-owned data integration for a provider. For custom providers, `custom_provider` updates the OAuth definition, which is shared with the user-owned root. The `/organization` suffix selects the environment-level organization-owned root for the provider; it does not name a particular organization. + + Args: + slug: The slug identifier of the data integration. + description: An optional description of the Data Integration. + enabled: Whether the Data Integration is enabled. + scopes: The OAuth scopes to request for the Data Integration. Pass `null` to reset to the provider's configured scopes. + credentials: New OAuth credentials for the Data Integration. When provided, rotates the stored client secret. Mutually exclusive with `api_key`. + api_key: An API key to install or rotate for a tenant on an `api_key` integration. Upserts the tenant installation identified by `user_id` (and optional `organization_id`). + custom_provider: Updates to a custom provider's OAuth definition. Only valid for custom-provider integrations. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + DataIntegration + + 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). + ConflictError: If a conflict occurs (409). + 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 { + "enabled": enabled, + "credentials": credentials.to_dict() + if credentials is not None + else None, + "api_key": api_key.to_dict() if api_key is not None else None, + "custom_provider": custom_provider.to_dict() + if custom_provider is not None + else None, + }.items() + if v is not None + } + if not isinstance(description, NotGiven): + body["description"] = description + if not isinstance(scopes, NotGiven): + body["scopes"] = scopes + return self._client.request( + method="put", + path=("data-integrations", str(slug), "organization"), + body=body, + model=DataIntegration, + request_options=request_options, + ) + + def delete_data_integration_organization( + self, + slug: str, + *, + request_options: RequestOptions | None = None, + ) -> None: + """Delete an organization-owned data integration + + Deletes the organization-owned data integration for a provider and all of its connected installations. For a custom provider, the provider definition is deleted once no user-owned root references it either. The `/organization` suffix selects the environment-level organization-owned root for the provider; it does not name a particular organization. + + Args: + slug: The slug identifier of the data integration. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Raises: + 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. + """ + self._client.request( + method="delete", + path=("data-integrations", str(slug), "organization"), + request_options=request_options, + ) + def get_access_token( self, provider: str, @@ -489,6 +665,10 @@ def get_access_token( user_id: str, organization_id: str | None | NotGiven = NOT_GIVEN, connected_account_id: str | None = None, + connection_owner: DataIntegrationsGetUserTokenRequestConnectionOwner + | str + | None = None, + supports_multiple_connections: bool | None = None, request_options: RequestOptions | None = None, ) -> DataIntegrationAccessTokenResponse: """Get an access token for a connected account @@ -497,9 +677,11 @@ def get_access_token( Args: provider: 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. + user_id: A [User](https://workos.com/docs/reference/authkit/user) identifier. When `connection_owner` is `organization`, this is the user the credentials are vended on behalf of; they must be an active member of the organization. + organization_id: An [Organization](https://workos.com/docs/reference/organization) identifier. Optional parameter to scope the connection to a specific organization. Required when `connection_owner` is `organization`. connected_account_id: A [connected account](https://workos.com/docs/reference/pipes/connected-account) identifier. Use this to select a specific connection when the user has several for this provider. + connection_owner: Which connection to vend from. `user` (the default) vends the user's own connection and requires `user_id`. `organization` vends the organization's shared connection and requires `organization_id`. + supports_multiple_connections: Set to `true` to use the plural connection contract. If no `connected_account_id` is supplied and several connections match, the request returns `account_selection_required`. When omitted or `false`, only the compatibility connection is considered. request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. Returns: @@ -508,6 +690,7 @@ def get_access_token( 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). ConflictError: If a conflict occurs (409). UnprocessableEntityError: If the request data is unprocessable (422). @@ -519,6 +702,10 @@ def get_access_token( for k, v in { "user_id": user_id, "connected_account_id": connected_account_id, + "connection_owner": enum_value(connection_owner) + if connection_owner is not None + else None, + "supports_multiple_connections": supports_multiple_connections, }.items() if v is not None } @@ -538,6 +725,7 @@ def get_user_connected_account( slug: str, *, organization_id: str | None = None, + supports_multiple_connections: bool | None = None, connected_account_id: str | None = None, request_options: RequestOptions | None = None, ) -> ConnectedAccount: @@ -549,6 +737,7 @@ def get_user_connected_account( user_id: A [User](https://workos.com/docs/reference/authkit/user) identifier. slug: The slug identifier of the provider (e.g., `github`, `slack`, `notion`). organization_id: An [Organization](https://workos.com/docs/reference/organization) identifier. Optional parameter if the connection is scoped to an organization. + supports_multiple_connections: Set to `true` to use the plural connection contract. When omitted or `false`, only the compatibility connection is considered. connected_account_id: A [connected account](https://workos.com/docs/reference/pipes/connected-account) identifier. Use this to select a specific connection when the user has several for this provider. request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. @@ -567,6 +756,7 @@ def get_user_connected_account( k: v for k, v in { "organization_id": organization_id, + "supports_multiple_connections": supports_multiple_connections, "connected_account_id": connected_account_id, }.items() if v is not None @@ -668,6 +858,7 @@ def update_user_connected_account( scopes: list[str] | None = None, state: ConnectedAccountInputState | str | None = None, organization_id: str | None = None, + supports_multiple_connections: bool | None = None, connected_account_id: str | None = None, request_options: RequestOptions | None = None, ) -> ConnectedAccount: @@ -684,6 +875,7 @@ def update_user_connected_account( scopes: The OAuth scopes granted for this connection. state: Explicitly set the state of the connected account. When omitted, the state is derived from the token combination provided. organization_id: An [Organization](https://workos.com/docs/reference/organization) identifier. Optional parameter if the connection is scoped to an organization. + supports_multiple_connections: Set to `true` to use the plural connection contract. When omitted or `false`, only the compatibility connection is considered. connected_account_id: A [connected account](https://workos.com/docs/reference/pipes/connected-account) identifier. Use this to select the connection to update. request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. @@ -695,6 +887,7 @@ def update_user_connected_account( AuthenticationError: If the API key is invalid (401). NotFoundError: If the resource is not found (404). ConflictError: If a conflict occurs (409). + UnprocessableEntityError: If the request data is unprocessable (422). RateLimitExceededError: If rate limited (429). ServerError: If the server returns a 5xx error. """ @@ -713,6 +906,7 @@ def update_user_connected_account( k: v for k, v in { "organization_id": organization_id, + "supports_multiple_connections": supports_multiple_connections, "connected_account_id": connected_account_id, }.items() if v is not None @@ -738,17 +932,19 @@ def delete_user_connected_account( slug: str, *, organization_id: str | None = None, + supports_multiple_connections: bool | None = None, connected_account_id: str | None = None, request_options: RequestOptions | None = None, ) -> None: """Delete a connected account - Disconnects WorkOS's account for the user, including removing any stored access and refresh tokens. The user will need to reauthorize if they want to reconnect. This does not revoke access on the provider side. + Disconnects WorkOS's account for the user, including removing any stored access and refresh tokens. The user will need to reauthorize if they want to reconnect. Access is not revoked on the provider side, except for the WorkOS OAuth provider, whose underlying AuthKit grant is revoked. Args: user_id: A [User](https://workos.com/docs/reference/authkit/user) identifier. slug: The slug identifier of the provider (e.g., `github`, `slack`, `notion`). organization_id: An [Organization](https://workos.com/docs/reference/organization) identifier. Optional parameter if the connection is scoped to an organization. + supports_multiple_connections: Set to `true` to use the plural connection contract. When omitted or `false`, only the compatibility connection is considered. connected_account_id: A [connected account](https://workos.com/docs/reference/pipes/connected-account) identifier. Use this to select the connection to delete. request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. @@ -764,6 +960,7 @@ def delete_user_connected_account( k: v for k, v in { "organization_id": organization_id, + "supports_multiple_connections": supports_multiple_connections, "connected_account_id": connected_account_id, }.items() if v is not None @@ -786,6 +983,7 @@ def list_user_data_providers( user_id: str, *, organization_id: str | None = None, + supports_multiple_connections: bool | None = None, request_options: RequestOptions | None = None, ) -> DataIntegrationsListResponse: """List providers for a user @@ -795,6 +993,7 @@ def list_user_data_providers( Args: user_id: A [User](https://workos.com/docs/reference/authkit/user) identifier to list providers and connected accounts for. organization_id: An [Organization](https://workos.com/docs/reference/organization) identifier. Optional parameter to filter connections for a specific organization. + supports_multiple_connections: Set to `true` to use the plural connection contract. When omitted or `false`, only the compatibility connection is considered. request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. Returns: @@ -810,6 +1009,7 @@ def list_user_data_providers( k: v for k, v in { "organization_id": organization_id, + "supports_multiple_connections": supports_multiple_connections, }.items() if v is not None } @@ -835,17 +1035,19 @@ async def list_data_integrations( before: str | None = None, after: str | None = None, order: PaginationOrder | str | None = "desc", + ownership: PipesOwnership | str | None = None, request_options: RequestOptions | None = None, ) -> AsyncPage[DataIntegration]: """List data integrations - Lists the environment's data integrations configured with `custom` or `organization` credentials, including custom providers and API key integrations. + Lists the environment's data integrations configured with `custom` or `organization` credentials, including custom providers and API key integrations. Both user-owned and organization-owned roots are returned, each as its own row with an `ownership`; filter with `ownership` to return only one kind. Args: limit: Upper limit on the number of objects to return, between `1` and `100`. Defaults to `10`. before: An object ID that defines your place in the list. When the ID is not present, you are at the end of the list. For example, if you make a list request and receive 100 objects, ending with `"obj_123"`, your subsequent call can include `before="obj_123"` to fetch a new batch of objects before `"obj_123"`. after: An object ID that defines your place in the list. When the ID is not present, you are at the end of the list. For example, if you make a list request and receive 100 objects, ending with `"obj_123"`, your subsequent call can include `after="obj_123"` to fetch a new batch of objects after `"obj_123"`. order: Order the results by the creation time. Supported values are `"asc"` (ascending), `"desc"` (descending), and `"normal"` (descending with reversed cursor semantics where `before` fetches older records and `after` fetches newer records). Defaults to `desc`. + ownership: Only return Data Integrations with this ownership: `user` for the integrations users connect their own accounts to, or `organization` for the roots organizations connect to. Omit to return both. request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. Returns: @@ -853,6 +1055,8 @@ async def list_data_integrations( Raises: 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. """ @@ -863,6 +1067,7 @@ async def list_data_integrations( "before": before, "after": after, "order": enum_value(order) if order is not None else None, + "ownership": enum_value(ownership) if ownership is not None else None, }.items() if v is not None } @@ -878,6 +1083,7 @@ async def create_data_integration( self, *, provider: str, + ownership: CreateDataIntegrationOwnership | str | None = None, description: str | None | NotGiven = NOT_GIVEN, enabled: bool | None = None, scopes: list[str] | None | NotGiven = NOT_GIVEN, @@ -890,10 +1096,11 @@ async def create_data_integration( ) -> DataIntegration: """Create a data integration - Creates a data integration for a provider. Set `credentials.type` to `custom` to use your own OAuth app credentials or `organization` to have each organization supply its own. Set `auth_methods` to `["api_key"]` to create an API key integration; you may optionally supply an `api_key` block to install a first tenant in the same call. Set `auth_methods` to `["client_credentials"]` to create a client-credentials integration; client credentials are installed per-tenant afterwards. For a built-in provider, pass its slug as `provider`. For a custom provider, pass a new slug plus a `custom_provider` definition. + Creates a data integration for a provider. Set `credentials.type` to `custom` to use your own OAuth app credentials or `organization` to have each organization supply its own. Set `auth_methods` to `["api_key"]` to create an API key integration; you may optionally supply an `api_key` block to install a first tenant in the same call. Set `auth_methods` to `["client_credentials"]` to create a client-credentials integration; client credentials are installed per-tenant afterwards. Set `ownership` to `organization` to create the integration organizations connect to instead of the default user-owned one; a provider may have one of each. For a built-in provider, pass its slug as `provider`. For a custom provider, pass a new slug plus a `custom_provider` definition, or the slug of an existing custom provider (without `custom_provider`) to add the other ownership. Args: provider: The provider to create a Data Integration for. For a built-in provider use its slug (e.g. `github`, `slack`). For a custom provider, this is the new provider slug and `custom_provider` must be supplied. A custom provider slug cannot shadow an existing global provider slug. + ownership: Who owns the Data Integration. `user` (the default) creates the integration users connect their own accounts to; `organization` creates the root organizations connect to. Ownership is fixed at creation, and one integration of each ownership may exist per provider. Independent of `credentials.type`. description: An optional description of the Data Integration. enabled: Whether the Data Integration is enabled. Defaults to `false`. scopes: The OAuth scopes to request for the Data Integration. Defaults to the provider's configured scopes when omitted. @@ -921,6 +1128,7 @@ async def create_data_integration( k: v for k, v in { "provider": provider, + "ownership": enum_value(ownership) if ownership is not None else None, "enabled": enabled, "auth_methods": auth_methods, "config": config, @@ -954,7 +1162,7 @@ async def get_data_integration( ) -> DataIntegration: """Get a data integration - Retrieves a data integration by its slug. + Retrieves the user-owned data integration by its slug. Args: slug: The slug identifier of the data integration. @@ -990,7 +1198,7 @@ async def update_data_integration( ) -> DataIntegration: """Update a data integration - Updates the description, enabled state, or custom credentials of a data integration. For custom providers, `custom_provider` updates the OAuth definition. + Updates the description, enabled state, or custom credentials of the user-owned data integration. For custom providers, `custom_provider` updates the OAuth definition. Args: slug: The slug identifier of the data integration. @@ -1049,7 +1257,7 @@ async def delete_data_integration( ) -> None: """Delete a data integration - Deletes a data integration and all of its connected installations. For a custom provider, also deletes the custom provider definition. + Deletes the user-owned data integration and all of its connected installations. For a custom provider, the provider definition is deleted once no organization-owned root references it either. Args: slug: The slug identifier of the data integration. @@ -1074,16 +1282,22 @@ async def update_data_integration_api_key( user_id: str, secret: str, organization_id: str | None = None, + connected_account_id: str | None = None, + connection_owner: DataIntegrationsUpsertApiKeyRequestConnectionOwner + | str + | None = None, request_options: RequestOptions | None = 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. + Creates or updates an API-key-based installation for the specified integration, owned by the user or, when `connection_owner` is `organization`, shared by the organization. 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. + organization_id: An [Organization](https://workos.com/docs/reference/organization) identifier. Optional parameter to scope the connection to a specific organization. Required when `connection_owner` is `organization`. + connected_account_id: A [connected account](https://workos.com/docs/reference/pipes/connected-account) identifier. Use this to rotate a specific existing connection. + connection_owner: Whose connection to create or rotate. `user` (the default) addresses the connection owned by `user_id`. `organization` addresses the connection shared by every member of `organization_id`; `user_id` then identifies the member performing the request and must be an active member of the 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. @@ -1105,6 +1319,10 @@ async def update_data_integration_api_key( for k, v in { "user_id": user_id, "organization_id": organization_id, + "connected_account_id": connected_account_id, + "connection_owner": enum_value(connection_owner) + if connection_owner is not None + else None, "secret": secret, }.items() if v is not None @@ -1176,17 +1394,23 @@ async def update_data_integration_client_credentials( client_id: str, client_secret: str, organization_id: str | None = None, + connected_account_id: str | None = None, + connection_owner: DataIntegrationsUpsertClientCredentialsRequestConnectionOwner + | str + | None = None, config: dict[str, str] | None = None, request_options: RequestOptions | None = None, ) -> ConnectedAccount: """Upsert client credentials for a connected account - Creates or updates a client-credentials-based installation for the specified integration and user. If an installation already exists, the stored client credentials are rotated to the new values. + Creates or updates a client-credentials-based installation for the specified integration, owned by the user or, when `connection_owner` is `organization`, shared by the organization. If an installation already exists, the stored client credentials are rotated to the new values. 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. + organization_id: An [Organization](https://workos.com/docs/reference/organization) identifier. Optional parameter to scope the connection to a specific organization. Required when `connection_owner` is `organization`. + connected_account_id: A [connected account](https://workos.com/docs/reference/pipes/connected-account) identifier. Use this to rotate a specific existing connection. + connection_owner: Whose connection to create or rotate. `user` (the default) addresses the connection owned by `user_id`. `organization` addresses the connection shared by every member of `organization_id`; `user_id` then identifies the member performing the request and must be an active member of the organization. client_id: The OAuth client ID to store for this integration. client_secret: The OAuth client secret to store for this integration. config: Provider-specific configuration values collected for this installation, keyed by the provider's config field descriptors. @@ -1210,6 +1434,10 @@ async def update_data_integration_client_credentials( for k, v in { "user_id": user_id, "organization_id": organization_id, + "connected_account_id": connected_account_id, + "connection_owner": enum_value(connection_owner) + if connection_owner is not None + else None, "client_id": client_id, "client_secret": client_secret, "config": config, @@ -1231,6 +1459,10 @@ async def create_data_integration_credential( user_id: str, organization_id: str | None = None, connected_account_id: str | None = None, + connection_owner: DataIntegrationsVendCredentialsRequestConnectionOwner + | str + | None = None, + supports_multiple_connections: bool | None = None, request_options: RequestOptions | None = None, ) -> DataIntegrationCredentialsResponse: """Vend credentials for a connected account @@ -1239,9 +1471,11 @@ async def create_data_integration_credential( 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. + user_id: A [User](https://workos.com/docs/reference/authkit/user) identifier. When `connection_owner` is `organization`, this is the user the credentials are vended on behalf of; they must be an active member of the organization. + organization_id: An [Organization](https://workos.com/docs/reference/organization) identifier. Optional parameter to scope the connection to a specific organization. Required when `connection_owner` is `organization`. connected_account_id: A [connected account](https://workos.com/docs/reference/pipes/connected-account) identifier. Use this to select a specific connection when the user has several for this provider. + connection_owner: Which connection to vend from. `user` (the default) vends the user's own connection and requires `user_id`. `organization` vends the organization's shared connection and requires `organization_id`. + supports_multiple_connections: Set to `true` to use the plural connection contract. If no `connected_account_id` is supplied and several connections match, the request returns `account_selection_required`. When omitted or `false`, only the compatibility connection is considered. request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. Returns: @@ -1250,6 +1484,7 @@ async def create_data_integration_credential( 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). ConflictError: If a conflict occurs (409). UnprocessableEntityError: If the request data is unprocessable (422). @@ -1262,6 +1497,10 @@ async def create_data_integration_credential( "user_id": user_id, "organization_id": organization_id, "connected_account_id": connected_account_id, + "connection_owner": enum_value(connection_owner) + if connection_owner is not None + else None, + "supports_multiple_connections": supports_multiple_connections, }.items() if v is not None } @@ -1273,6 +1512,127 @@ async def create_data_integration_credential( request_options=request_options, ) + async def list_data_integration_organization( + self, + slug: str, + *, + request_options: RequestOptions | None = None, + ) -> DataIntegration: + """Get an organization-owned data integration + + Retrieves the organization-owned data integration for a provider by its slug. The `/organization` suffix selects the environment-level organization-owned root for the provider; it does not name a particular organization. + + Args: + slug: The slug identifier of the data integration. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + DataIntegration + + Raises: + 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. + """ + return await self._client.request( + method="get", + path=("data-integrations", str(slug), "organization"), + model=DataIntegration, + request_options=request_options, + ) + + async def update_data_integration_organization( + self, + slug: str, + *, + description: str | None | NotGiven = NOT_GIVEN, + enabled: bool | None = None, + scopes: list[str] | None | NotGiven = NOT_GIVEN, + credentials: DataIntegrationCredentialsInput | None = None, + api_key: ApiKeyInstallation | None = None, + custom_provider: UpdateCustomProviderDefinition | None = None, + request_options: RequestOptions | None = None, + ) -> DataIntegration: + """Update an organization-owned data integration + + Updates the description, enabled state, or custom credentials of the organization-owned data integration for a provider. For custom providers, `custom_provider` updates the OAuth definition, which is shared with the user-owned root. The `/organization` suffix selects the environment-level organization-owned root for the provider; it does not name a particular organization. + + Args: + slug: The slug identifier of the data integration. + description: An optional description of the Data Integration. + enabled: Whether the Data Integration is enabled. + scopes: The OAuth scopes to request for the Data Integration. Pass `null` to reset to the provider's configured scopes. + credentials: New OAuth credentials for the Data Integration. When provided, rotates the stored client secret. Mutually exclusive with `api_key`. + api_key: An API key to install or rotate for a tenant on an `api_key` integration. Upserts the tenant installation identified by `user_id` (and optional `organization_id`). + custom_provider: Updates to a custom provider's OAuth definition. Only valid for custom-provider integrations. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + DataIntegration + + 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). + ConflictError: If a conflict occurs (409). + 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 { + "enabled": enabled, + "credentials": credentials.to_dict() + if credentials is not None + else None, + "api_key": api_key.to_dict() if api_key is not None else None, + "custom_provider": custom_provider.to_dict() + if custom_provider is not None + else None, + }.items() + if v is not None + } + if not isinstance(description, NotGiven): + body["description"] = description + if not isinstance(scopes, NotGiven): + body["scopes"] = scopes + return await self._client.request( + method="put", + path=("data-integrations", str(slug), "organization"), + body=body, + model=DataIntegration, + request_options=request_options, + ) + + async def delete_data_integration_organization( + self, + slug: str, + *, + request_options: RequestOptions | None = None, + ) -> None: + """Delete an organization-owned data integration + + Deletes the organization-owned data integration for a provider and all of its connected installations. For a custom provider, the provider definition is deleted once no user-owned root references it either. The `/organization` suffix selects the environment-level organization-owned root for the provider; it does not name a particular organization. + + Args: + slug: The slug identifier of the data integration. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Raises: + 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. + """ + await self._client.request( + method="delete", + path=("data-integrations", str(slug), "organization"), + request_options=request_options, + ) + async def get_access_token( self, provider: str, @@ -1280,6 +1640,10 @@ async def get_access_token( user_id: str, organization_id: str | None | NotGiven = NOT_GIVEN, connected_account_id: str | None = None, + connection_owner: DataIntegrationsGetUserTokenRequestConnectionOwner + | str + | None = None, + supports_multiple_connections: bool | None = None, request_options: RequestOptions | None = None, ) -> DataIntegrationAccessTokenResponse: """Get an access token for a connected account @@ -1288,9 +1652,11 @@ async def get_access_token( Args: provider: 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. + user_id: A [User](https://workos.com/docs/reference/authkit/user) identifier. When `connection_owner` is `organization`, this is the user the credentials are vended on behalf of; they must be an active member of the organization. + organization_id: An [Organization](https://workos.com/docs/reference/organization) identifier. Optional parameter to scope the connection to a specific organization. Required when `connection_owner` is `organization`. connected_account_id: A [connected account](https://workos.com/docs/reference/pipes/connected-account) identifier. Use this to select a specific connection when the user has several for this provider. + connection_owner: Which connection to vend from. `user` (the default) vends the user's own connection and requires `user_id`. `organization` vends the organization's shared connection and requires `organization_id`. + supports_multiple_connections: Set to `true` to use the plural connection contract. If no `connected_account_id` is supplied and several connections match, the request returns `account_selection_required`. When omitted or `false`, only the compatibility connection is considered. request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. Returns: @@ -1299,6 +1665,7 @@ async def get_access_token( 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). ConflictError: If a conflict occurs (409). UnprocessableEntityError: If the request data is unprocessable (422). @@ -1310,6 +1677,10 @@ async def get_access_token( for k, v in { "user_id": user_id, "connected_account_id": connected_account_id, + "connection_owner": enum_value(connection_owner) + if connection_owner is not None + else None, + "supports_multiple_connections": supports_multiple_connections, }.items() if v is not None } @@ -1329,6 +1700,7 @@ async def get_user_connected_account( slug: str, *, organization_id: str | None = None, + supports_multiple_connections: bool | None = None, connected_account_id: str | None = None, request_options: RequestOptions | None = None, ) -> ConnectedAccount: @@ -1340,6 +1712,7 @@ async def get_user_connected_account( user_id: A [User](https://workos.com/docs/reference/authkit/user) identifier. slug: The slug identifier of the provider (e.g., `github`, `slack`, `notion`). organization_id: An [Organization](https://workos.com/docs/reference/organization) identifier. Optional parameter if the connection is scoped to an organization. + supports_multiple_connections: Set to `true` to use the plural connection contract. When omitted or `false`, only the compatibility connection is considered. connected_account_id: A [connected account](https://workos.com/docs/reference/pipes/connected-account) identifier. Use this to select a specific connection when the user has several for this provider. request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. @@ -1358,6 +1731,7 @@ async def get_user_connected_account( k: v for k, v in { "organization_id": organization_id, + "supports_multiple_connections": supports_multiple_connections, "connected_account_id": connected_account_id, }.items() if v is not None @@ -1459,6 +1833,7 @@ async def update_user_connected_account( scopes: list[str] | None = None, state: ConnectedAccountInputState | str | None = None, organization_id: str | None = None, + supports_multiple_connections: bool | None = None, connected_account_id: str | None = None, request_options: RequestOptions | None = None, ) -> ConnectedAccount: @@ -1475,6 +1850,7 @@ async def update_user_connected_account( scopes: The OAuth scopes granted for this connection. state: Explicitly set the state of the connected account. When omitted, the state is derived from the token combination provided. organization_id: An [Organization](https://workos.com/docs/reference/organization) identifier. Optional parameter if the connection is scoped to an organization. + supports_multiple_connections: Set to `true` to use the plural connection contract. When omitted or `false`, only the compatibility connection is considered. connected_account_id: A [connected account](https://workos.com/docs/reference/pipes/connected-account) identifier. Use this to select the connection to update. request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. @@ -1486,6 +1862,7 @@ async def update_user_connected_account( AuthenticationError: If the API key is invalid (401). NotFoundError: If the resource is not found (404). ConflictError: If a conflict occurs (409). + UnprocessableEntityError: If the request data is unprocessable (422). RateLimitExceededError: If rate limited (429). ServerError: If the server returns a 5xx error. """ @@ -1504,6 +1881,7 @@ async def update_user_connected_account( k: v for k, v in { "organization_id": organization_id, + "supports_multiple_connections": supports_multiple_connections, "connected_account_id": connected_account_id, }.items() if v is not None @@ -1529,17 +1907,19 @@ async def delete_user_connected_account( slug: str, *, organization_id: str | None = None, + supports_multiple_connections: bool | None = None, connected_account_id: str | None = None, request_options: RequestOptions | None = None, ) -> None: """Delete a connected account - Disconnects WorkOS's account for the user, including removing any stored access and refresh tokens. The user will need to reauthorize if they want to reconnect. This does not revoke access on the provider side. + Disconnects WorkOS's account for the user, including removing any stored access and refresh tokens. The user will need to reauthorize if they want to reconnect. Access is not revoked on the provider side, except for the WorkOS OAuth provider, whose underlying AuthKit grant is revoked. Args: user_id: A [User](https://workos.com/docs/reference/authkit/user) identifier. slug: The slug identifier of the provider (e.g., `github`, `slack`, `notion`). organization_id: An [Organization](https://workos.com/docs/reference/organization) identifier. Optional parameter if the connection is scoped to an organization. + supports_multiple_connections: Set to `true` to use the plural connection contract. When omitted or `false`, only the compatibility connection is considered. connected_account_id: A [connected account](https://workos.com/docs/reference/pipes/connected-account) identifier. Use this to select the connection to delete. request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. @@ -1555,6 +1935,7 @@ async def delete_user_connected_account( k: v for k, v in { "organization_id": organization_id, + "supports_multiple_connections": supports_multiple_connections, "connected_account_id": connected_account_id, }.items() if v is not None @@ -1577,6 +1958,7 @@ async def list_user_data_providers( user_id: str, *, organization_id: str | None = None, + supports_multiple_connections: bool | None = None, request_options: RequestOptions | None = None, ) -> DataIntegrationsListResponse: """List providers for a user @@ -1586,6 +1968,7 @@ async def list_user_data_providers( Args: user_id: A [User](https://workos.com/docs/reference/authkit/user) identifier to list providers and connected accounts for. organization_id: An [Organization](https://workos.com/docs/reference/organization) identifier. Optional parameter to filter connections for a specific organization. + supports_multiple_connections: Set to `true` to use the plural connection contract. When omitted or `false`, only the compatibility connection is considered. request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. Returns: @@ -1601,6 +1984,7 @@ async def list_user_data_providers( k: v for k, v in { "organization_id": organization_id, + "supports_multiple_connections": supports_multiple_connections, }.items() if v is not None } diff --git a/src/workos/pipes/models/__init__.py b/src/workos/pipes/models/__init__.py index e1493c0f..1a6cff7a 100644 --- a/src/workos/pipes/models/__init__.py +++ b/src/workos/pipes/models/__init__.py @@ -60,6 +60,7 @@ from .data_integrations_vend_credentials_request import ( DataIntegrationsVendCredentialsRequest as DataIntegrationsVendCredentialsRequest, ) +from .pipes_ownership import PipesOwnership as PipesOwnership from .update_custom_provider_definition import ( UpdateCustomProviderDefinition as UpdateCustomProviderDefinition, ) diff --git a/src/workos/pipes/models/api_key_installation.py b/src/workos/pipes/models/api_key_installation.py index 65219f4f..6186d1bc 100644 --- a/src/workos/pipes/models/api_key_installation.py +++ b/src/workos/pipes/models/api_key_installation.py @@ -1,9 +1,41 @@ # This file is auto-generated by oagen. Do not edit. -from typing import TypeAlias +from __future__ import annotations -from .data_integrations_upsert_api_key_request import ( - DataIntegrationsUpsertApiKeyRequest, -) +from dataclasses import dataclass +from typing import Any -ApiKeyInstallation: TypeAlias = DataIntegrationsUpsertApiKeyRequest +from workos._types import _raise_deserialize_error + + +@dataclass(slots=True) +class ApiKeyInstallation: + """Api Key Installation model.""" + + secret: str + """The API key secret to store for the tenant.""" + user_id: str + """The User identifier the API key is installed for.""" + organization_id: str | None = None + """An Organization identifier to scope the installation to a specific organization.""" + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> ApiKeyInstallation: + """Deserialize from a dictionary.""" + try: + return cls( + secret=data["secret"], + user_id=data["user_id"], + organization_id=data.get("organization_id"), + ) + except (KeyError, ValueError) as e: + _raise_deserialize_error("ApiKeyInstallation", e) + + def to_dict(self) -> dict[str, Any]: + """Serialize to a dictionary.""" + result: dict[str, Any] = {} + result["secret"] = self.secret + 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/pipes/models/create_data_integration.py b/src/workos/pipes/models/create_data_integration.py index 580be727..36709675 100644 --- a/src/workos/pipes/models/create_data_integration.py +++ b/src/workos/pipes/models/create_data_integration.py @@ -10,6 +10,9 @@ from workos.common.models.create_data_integration_auth_methods import ( CreateDataIntegrationAuthMethods, ) +from workos.common.models.create_data_integration_ownership import ( + CreateDataIntegrationOwnership, +) from .api_key_installation import ApiKeyInstallation from .custom_provider_definition import CustomProviderDefinition @@ -22,6 +25,8 @@ class CreateDataIntegration: provider: str """The provider to create a Data Integration for. For a built-in provider use its slug (e.g. `github`, `slack`). For a custom provider, this is the new provider slug and `custom_provider` must be supplied. A custom provider slug cannot shadow an existing global provider slug.""" + ownership: CreateDataIntegrationOwnership | None = None + """Who owns the Data Integration. `user` (the default) creates the integration users connect their own accounts to; `organization` creates the root organizations connect to. Ownership is fixed at creation, and one integration of each ownership may exist per provider. Independent of `credentials.type`.""" description: str | None = None """An optional description of the Data Integration.""" enabled: bool | None = None @@ -45,6 +50,9 @@ def from_dict(cls, data: dict[str, Any]) -> CreateDataIntegration: try: return cls( provider=data["provider"], + ownership=CreateDataIntegrationOwnership(_v_ownership) + if (_v_ownership := data.get("ownership")) is not None + else None, description=data.get("description"), enabled=data.get("enabled"), scopes=data.get("scopes"), @@ -76,6 +84,12 @@ def to_dict(self) -> dict[str, Any]: """Serialize to a dictionary.""" result: dict[str, Any] = {} result["provider"] = self.provider + if self.ownership is not None: + result["ownership"] = ( + self.ownership.value + if isinstance(self.ownership, Enum) + else self.ownership + ) if self.description is not None: result["description"] = self.description else: diff --git a/src/workos/pipes/models/data_integration.py b/src/workos/pipes/models/data_integration.py index 69dff813..ffa61821 100644 --- a/src/workos/pipes/models/data_integration.py +++ b/src/workos/pipes/models/data_integration.py @@ -11,6 +11,7 @@ from workos.common.models.data_integration_auth_methods import ( DataIntegrationAuthMethods, ) +from workos.common.models.data_integration_ownership import DataIntegrationOwnership from workos.common.models.data_integration_state import DataIntegrationState from .data_integration_credential import DataIntegrationCredential @@ -30,6 +31,8 @@ class DataIntegration: """The provider slug for this Data Integration.""" integration_type: str """The integration type derived from the provider.""" + ownership: DataIntegrationOwnership + """Who owns the Data Integration: `user` when users connect their own accounts, `organization` when organizations connect. Fixed at creation.""" description: str | None """An optional description of the Data Integration.""" enabled: bool @@ -64,6 +67,7 @@ def from_dict(cls, data: dict[str, Any]) -> DataIntegration: id=data["id"], slug=data["slug"], integration_type=data["integration_type"], + ownership=DataIntegrationOwnership(data["ownership"]), description=data["description"], enabled=data["enabled"], state=DataIntegrationState(data["state"]), @@ -102,6 +106,9 @@ def to_dict(self) -> dict[str, Any]: result["id"] = self.id result["slug"] = self.slug result["integration_type"] = self.integration_type + result["ownership"] = ( + self.ownership.value if isinstance(self.ownership, Enum) else self.ownership + ) if self.description is not None: result["description"] = self.description else: diff --git a/src/workos/pipes/models/data_integration_installation.py b/src/workos/pipes/models/data_integration_installation.py index 544b385f..983f9036 100644 --- a/src/workos/pipes/models/data_integration_installation.py +++ b/src/workos/pipes/models/data_integration_installation.py @@ -14,10 +14,10 @@ class DataIntegrationInstallation: id: str """Unique identifier of the installation.""" - user_id: str - """The User the API key was installed for.""" + user_id: str | None + """The User the API key was installed for. Null on an `organization`-owned integration, whose installations belong to the organization.""" organization_id: str | None - """The Organization the installation is scoped to, or null when unscoped.""" + """The Organization the installation is scoped to (or owned by, on an `organization`-owned integration), or null when unscoped.""" api_key_last_4: str | None """The last four characters of the stored API key. The full key is never returned.""" @@ -38,7 +38,10 @@ def to_dict(self) -> dict[str, Any]: """Serialize to a dictionary.""" result: dict[str, Any] = {} result["id"] = self.id - result["user_id"] = self.user_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: diff --git a/src/workos/pipes/models/data_integrations_get_user_token_request.py b/src/workos/pipes/models/data_integrations_get_user_token_request.py index 7c93c3b1..b0627b4e 100644 --- a/src/workos/pipes/models/data_integrations_get_user_token_request.py +++ b/src/workos/pipes/models/data_integrations_get_user_token_request.py @@ -3,9 +3,13 @@ from __future__ import annotations from dataclasses import dataclass +from enum import Enum from typing import Any from workos._types import _raise_deserialize_error +from workos.common.models.data_integrations_get_user_token_request_connection_owner import ( + DataIntegrationsGetUserTokenRequestConnectionOwner, +) @dataclass(slots=True) @@ -13,11 +17,15 @@ class DataIntegrationsGetUserTokenRequest: """Data Integrations Get User Token Request model.""" user_id: str - """A [User](https://workos.com/docs/reference/authkit/user) identifier.""" + """A [User](https://workos.com/docs/reference/authkit/user) identifier. When `connection_owner` is `organization`, this is the user the credentials are vended on behalf of; they must be an active member of the organization.""" organization_id: str | None = None - """An [Organization](https://workos.com/docs/reference/organization) identifier. Optional parameter to scope the connection to a specific organization.""" + """An [Organization](https://workos.com/docs/reference/organization) identifier. Optional parameter to scope the connection to a specific organization. Required when `connection_owner` is `organization`.""" connected_account_id: str | None = None """A [connected account](https://workos.com/docs/reference/pipes/connected-account) identifier. Use this to select a specific connection when the user has several for this provider.""" + connection_owner: DataIntegrationsGetUserTokenRequestConnectionOwner | None = None + """Which connection to vend from. `user` (the default) vends the user's own connection and requires `user_id`. `organization` vends the organization's shared connection and requires `organization_id`.""" + supports_multiple_connections: bool | None = None + """Set to `true` to use the plural connection contract. If no `connected_account_id` is supplied and several connections match, the request returns `account_selection_required`. When omitted or `false`, only the compatibility connection is considered.""" @classmethod def from_dict(cls, data: dict[str, Any]) -> DataIntegrationsGetUserTokenRequest: @@ -27,6 +35,12 @@ def from_dict(cls, data: dict[str, Any]) -> DataIntegrationsGetUserTokenRequest: user_id=data["user_id"], organization_id=data.get("organization_id"), connected_account_id=data.get("connected_account_id"), + connection_owner=DataIntegrationsGetUserTokenRequestConnectionOwner( + _v_connection_owner + ) + if (_v_connection_owner := data.get("connection_owner")) is not None + else None, + supports_multiple_connections=data.get("supports_multiple_connections"), ) except (KeyError, ValueError) as e: _raise_deserialize_error("DataIntegrationsGetUserTokenRequest", e) @@ -41,4 +55,12 @@ def to_dict(self) -> dict[str, Any]: result["organization_id"] = None if self.connected_account_id is not None: result["connected_account_id"] = self.connected_account_id + if self.connection_owner is not None: + result["connection_owner"] = ( + self.connection_owner.value + if isinstance(self.connection_owner, Enum) + else self.connection_owner + ) + if self.supports_multiple_connections is not None: + result["supports_multiple_connections"] = self.supports_multiple_connections return result diff --git a/src/workos/pipes/models/data_integrations_list_response_data.py b/src/workos/pipes/models/data_integrations_list_response_data.py index d4cbac8e..cd02d5a5 100644 --- a/src/workos/pipes/models/data_integrations_list_response_data.py +++ b/src/workos/pipes/models/data_integrations_list_response_data.py @@ -46,9 +46,9 @@ class DataIntegrationsListResponseData: updated_at: str """The timestamp when the provider was last updated.""" connected_account: DataIntegrationsListResponseDataConnectedAccount | None - """The user's [connected account](https://workos.com/docs/reference/pipes/connected-account) for this provider, or `null` if the user has not connected.""" + """The user's compatibility [connected account](https://workos.com/docs/reference/pipes/connected-account) for this provider, or `null` when the compatibility slot is empty. This legacy field never selects a standard connection.""" connected_accounts: list[DataIntegrationsListResponseDataConnectedAccount] - """The user's connected accounts for this provider in the requested ownership context.""" + """The user's connected accounts for this provider in the requested ownership context. This contains only the compatibility connection unless `supports_multiple_connections` is `true`.""" auth_methods: list[DataIntegrationsListResponseDataAuthMethods] | None = None """The authentication methods supported by this provider (`oauth`, `api_key`, `client_credentials`, or a combination). Defaults to `["oauth"]` if absent.""" 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 index 3172e394..199f6cf8 100644 --- a/src/workos/pipes/models/data_integrations_upsert_api_key_request.py +++ b/src/workos/pipes/models/data_integrations_upsert_api_key_request.py @@ -3,9 +3,13 @@ from __future__ import annotations from dataclasses import dataclass +from enum import Enum from typing import Any from workos._types import _raise_deserialize_error +from workos.common.models.data_integrations_upsert_api_key_request_connection_owner import ( + DataIntegrationsUpsertApiKeyRequestConnectionOwner, +) @dataclass(slots=True) @@ -17,7 +21,11 @@ class DataIntegrationsUpsertApiKeyRequest: secret: str """The API key secret to store for this integration.""" organization_id: str | None = None - """An [Organization](https://workos.com/docs/reference/organization) identifier. Optional parameter to scope the connection to a specific organization.""" + """An [Organization](https://workos.com/docs/reference/organization) identifier. Optional parameter to scope the connection to a specific organization. Required when `connection_owner` is `organization`.""" + connected_account_id: str | None = None + """A [connected account](https://workos.com/docs/reference/pipes/connected-account) identifier. Use this to rotate a specific existing connection.""" + connection_owner: DataIntegrationsUpsertApiKeyRequestConnectionOwner | None = None + """Whose connection to create or rotate. `user` (the default) addresses the connection owned by `user_id`. `organization` addresses the connection shared by every member of `organization_id`; `user_id` then identifies the member performing the request and must be an active member of the organization.""" @classmethod def from_dict(cls, data: dict[str, Any]) -> DataIntegrationsUpsertApiKeyRequest: @@ -27,6 +35,12 @@ def from_dict(cls, data: dict[str, Any]) -> DataIntegrationsUpsertApiKeyRequest: user_id=data["user_id"], secret=data["secret"], organization_id=data.get("organization_id"), + connected_account_id=data.get("connected_account_id"), + connection_owner=DataIntegrationsUpsertApiKeyRequestConnectionOwner( + _v_connection_owner + ) + if (_v_connection_owner := data.get("connection_owner")) is not None + else None, ) except (KeyError, ValueError) as e: _raise_deserialize_error("DataIntegrationsUpsertApiKeyRequest", e) @@ -38,4 +52,12 @@ def to_dict(self) -> dict[str, Any]: result["secret"] = self.secret if self.organization_id is not None: result["organization_id"] = self.organization_id + if self.connected_account_id is not None: + result["connected_account_id"] = self.connected_account_id + if self.connection_owner is not None: + result["connection_owner"] = ( + self.connection_owner.value + if isinstance(self.connection_owner, Enum) + else self.connection_owner + ) return result diff --git a/src/workos/pipes/models/data_integrations_upsert_client_credentials_request.py b/src/workos/pipes/models/data_integrations_upsert_client_credentials_request.py index ea1e0e76..56efd7ba 100644 --- a/src/workos/pipes/models/data_integrations_upsert_client_credentials_request.py +++ b/src/workos/pipes/models/data_integrations_upsert_client_credentials_request.py @@ -3,9 +3,13 @@ from __future__ import annotations from dataclasses import dataclass +from enum import Enum from typing import Any from workos._types import _raise_deserialize_error +from workos.common.models.data_integrations_upsert_client_credentials_request_connection_owner import ( + DataIntegrationsUpsertClientCredentialsRequestConnectionOwner, +) @dataclass(slots=True) @@ -19,7 +23,13 @@ class DataIntegrationsUpsertClientCredentialsRequest: client_secret: str """The OAuth client secret to store for this integration.""" organization_id: str | None = None - """An [Organization](https://workos.com/docs/reference/organization) identifier. Optional parameter to scope the connection to a specific organization.""" + """An [Organization](https://workos.com/docs/reference/organization) identifier. Optional parameter to scope the connection to a specific organization. Required when `connection_owner` is `organization`.""" + connected_account_id: str | None = None + """A [connected account](https://workos.com/docs/reference/pipes/connected-account) identifier. Use this to rotate a specific existing connection.""" + connection_owner: ( + DataIntegrationsUpsertClientCredentialsRequestConnectionOwner | None + ) = None + """Whose connection to create or rotate. `user` (the default) addresses the connection owned by `user_id`. `organization` addresses the connection shared by every member of `organization_id`; `user_id` then identifies the member performing the request and must be an active member of the organization.""" config: dict[str, str] | None = None """Provider-specific configuration values collected for this installation, keyed by the provider's config field descriptors.""" @@ -34,6 +44,12 @@ def from_dict( client_id=data["client_id"], client_secret=data["client_secret"], organization_id=data.get("organization_id"), + connected_account_id=data.get("connected_account_id"), + connection_owner=DataIntegrationsUpsertClientCredentialsRequestConnectionOwner( + _v_connection_owner + ) + if (_v_connection_owner := data.get("connection_owner")) is not None + else None, config=data.get("config"), ) except (KeyError, ValueError) as e: @@ -49,6 +65,14 @@ def to_dict(self) -> dict[str, Any]: result["client_secret"] = self.client_secret if self.organization_id is not None: result["organization_id"] = self.organization_id + if self.connected_account_id is not None: + result["connected_account_id"] = self.connected_account_id + if self.connection_owner is not None: + result["connection_owner"] = ( + self.connection_owner.value + if isinstance(self.connection_owner, Enum) + else self.connection_owner + ) if self.config is not None: result["config"] = self.config 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 index 941de6e3..3afebbe0 100644 --- a/src/workos/pipes/models/data_integrations_vend_credentials_request.py +++ b/src/workos/pipes/models/data_integrations_vend_credentials_request.py @@ -3,9 +3,13 @@ from __future__ import annotations from dataclasses import dataclass +from enum import Enum from typing import Any from workos._types import _raise_deserialize_error +from workos.common.models.data_integrations_vend_credentials_request_connection_owner import ( + DataIntegrationsVendCredentialsRequestConnectionOwner, +) @dataclass(slots=True) @@ -13,11 +17,17 @@ class DataIntegrationsVendCredentialsRequest: """Data Integrations Vend Credentials Request model.""" user_id: str - """A [User](https://workos.com/docs/reference/authkit/user) identifier.""" + """A [User](https://workos.com/docs/reference/authkit/user) identifier. When `connection_owner` is `organization`, this is the user the credentials are vended on behalf of; they must be an active member of the organization.""" organization_id: str | None = None - """An [Organization](https://workos.com/docs/reference/organization) identifier. Optional parameter to scope the connection to a specific organization.""" + """An [Organization](https://workos.com/docs/reference/organization) identifier. Optional parameter to scope the connection to a specific organization. Required when `connection_owner` is `organization`.""" connected_account_id: str | None = None """A [connected account](https://workos.com/docs/reference/pipes/connected-account) identifier. Use this to select a specific connection when the user has several for this provider.""" + connection_owner: DataIntegrationsVendCredentialsRequestConnectionOwner | None = ( + None + ) + """Which connection to vend from. `user` (the default) vends the user's own connection and requires `user_id`. `organization` vends the organization's shared connection and requires `organization_id`.""" + supports_multiple_connections: bool | None = None + """Set to `true` to use the plural connection contract. If no `connected_account_id` is supplied and several connections match, the request returns `account_selection_required`. When omitted or `false`, only the compatibility connection is considered.""" @classmethod def from_dict(cls, data: dict[str, Any]) -> DataIntegrationsVendCredentialsRequest: @@ -27,6 +37,12 @@ def from_dict(cls, data: dict[str, Any]) -> DataIntegrationsVendCredentialsReque user_id=data["user_id"], organization_id=data.get("organization_id"), connected_account_id=data.get("connected_account_id"), + connection_owner=DataIntegrationsVendCredentialsRequestConnectionOwner( + _v_connection_owner + ) + if (_v_connection_owner := data.get("connection_owner")) is not None + else None, + supports_multiple_connections=data.get("supports_multiple_connections"), ) except (KeyError, ValueError) as e: _raise_deserialize_error("DataIntegrationsVendCredentialsRequest", e) @@ -39,4 +55,12 @@ def to_dict(self) -> dict[str, Any]: result["organization_id"] = self.organization_id if self.connected_account_id is not None: result["connected_account_id"] = self.connected_account_id + if self.connection_owner is not None: + result["connection_owner"] = ( + self.connection_owner.value + if isinstance(self.connection_owner, Enum) + else self.connection_owner + ) + if self.supports_multiple_connections is not None: + result["supports_multiple_connections"] = self.supports_multiple_connections return result diff --git a/src/workos/pipes/models/pipes_ownership.py b/src/workos/pipes/models/pipes_ownership.py new file mode 100644 index 00000000..2abef504 --- /dev/null +++ b/src/workos/pipes/models/pipes_ownership.py @@ -0,0 +1,21 @@ +# This file is auto-generated by oagen. Do not edit. + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from workos.common.models.create_data_integration_ownership import ( + CreateDataIntegrationOwnership as PipesOwnership, + ) +else: + + def __getattr__(name: str): + if name == "PipesOwnership": + from workos.common.models.create_data_integration_ownership import ( + CreateDataIntegrationOwnership, + ) + + return CreateDataIntegrationOwnership + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") + + +__all__ = ["PipesOwnership"] diff --git a/src/workos/user_management/_resource.py b/src/workos/user_management/_resource.py index 53629ec7..9bb150c8 100644 --- a/src/workos/user_management/_resource.py +++ b/src/workos/user_management/_resource.py @@ -35,6 +35,7 @@ ) from .models import ( AuthenticateResponse, + AuthkitOAuthResource, AuthorizationCodeSessionAuthenticateRequest, AuthorizedConnectApplicationListData, CORSOriginResponse, @@ -816,6 +817,119 @@ def revoke_session( request_options=request_options, ) + def list_authkit_oauth_resources( + self, + *, + limit: int | None = None, + before: str | None = None, + after: str | None = None, + order: PaginationOrder | str | None = "desc", + request_options: RequestOptions | None = None, + ) -> SyncPage[AuthkitOAuthResource]: + """List MCP resource indicators + + Lists the MCP resource indicators configured for an environment. + + Args: + limit: Upper limit on the number of objects to return, between `1` and `100`. Defaults to `10`. + before: An object ID that defines your place in the list. When the ID is not present, you are at the end of the list. For example, if you make a list request and receive 100 objects, ending with `"obj_123"`, your subsequent call can include `before="obj_123"` to fetch a new batch of objects before `"obj_123"`. + after: An object ID that defines your place in the list. When the ID is not present, you are at the end of the list. For example, if you make a list request and receive 100 objects, ending with `"obj_123"`, your subsequent call can include `after="obj_123"` to fetch a new batch of objects after `"obj_123"`. + order: Order the results by the creation time. Supported values are `"asc"` (ascending), `"desc"` (descending), and `"normal"` (descending with reversed cursor semantics where `before` fetches older records and `after` fetches newer records). Defaults to `desc`. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + SyncPage[AuthkitOAuthResource] + + Raises: + AuthenticationError: If the API key is invalid (401). + RateLimitExceededError: If rate limited (429). + ServerError: If the server returns a 5xx error. + """ + params = { + k: v + for k, v in { + "limit": limit, + "before": before, + "after": after, + "order": enum_value(order) if order is not None else None, + }.items() + if v is not None + } + return self._client.request_page( + method="get", + path=("user_management", "authkit_oauth_resources"), + model=AuthkitOAuthResource, + params=params, + request_options=request_options, + ) + + def create_authkit_oauth_resource( + self, + *, + uri: str, + default: bool | None = None, + request_options: RequestOptions | None = None, + ) -> AuthkitOAuthResource: + """Create an MCP resource indicator + + Adds an MCP resource indicator (RFC 8707) to an environment, leaving any others in place. + + Args: + uri: The resource URI. May be a wildcard pattern with a single `*` in the leftmost hostname label, where enabled for the environment. + default: Whether the resource being created becomes the environment default, clearing any previous default. Applies at creation only — this API has no update endpoint yet, so changing the default on an existing resource is done from the dashboard. A wildcard pattern cannot be the default. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + AuthkitOAuthResource + + Raises: + AuthenticationError: If the API key is invalid (401). + 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 { + "uri": uri, + "default": default, + }.items() + if v is not None + } + return self._client.request( + method="post", + path=("user_management", "authkit_oauth_resources"), + body=body, + model=AuthkitOAuthResource, + request_options=request_options, + ) + + def delete_authkit_oauth_resource( + self, + id: str, + *, + request_options: RequestOptions | None = None, + ) -> None: + """Delete an MCP resource indicator + + Removes an MCP resource indicator from an environment. Any application consents granted against it are removed too. + + Args: + id: The ID of the MCP resource indicator to delete. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Raises: + 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. + """ + self._client.request( + method="delete", + path=("user_management", "authkit_oauth_resources", str(id)), + request_options=request_options, + ) + def list_cors_origins( self, *, @@ -3455,6 +3569,119 @@ async def revoke_session( request_options=request_options, ) + async def list_authkit_oauth_resources( + self, + *, + limit: int | None = None, + before: str | None = None, + after: str | None = None, + order: PaginationOrder | str | None = "desc", + request_options: RequestOptions | None = None, + ) -> AsyncPage[AuthkitOAuthResource]: + """List MCP resource indicators + + Lists the MCP resource indicators configured for an environment. + + Args: + limit: Upper limit on the number of objects to return, between `1` and `100`. Defaults to `10`. + before: An object ID that defines your place in the list. When the ID is not present, you are at the end of the list. For example, if you make a list request and receive 100 objects, ending with `"obj_123"`, your subsequent call can include `before="obj_123"` to fetch a new batch of objects before `"obj_123"`. + after: An object ID that defines your place in the list. When the ID is not present, you are at the end of the list. For example, if you make a list request and receive 100 objects, ending with `"obj_123"`, your subsequent call can include `after="obj_123"` to fetch a new batch of objects after `"obj_123"`. + order: Order the results by the creation time. Supported values are `"asc"` (ascending), `"desc"` (descending), and `"normal"` (descending with reversed cursor semantics where `before` fetches older records and `after` fetches newer records). Defaults to `desc`. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + AsyncPage[AuthkitOAuthResource] + + Raises: + AuthenticationError: If the API key is invalid (401). + RateLimitExceededError: If rate limited (429). + ServerError: If the server returns a 5xx error. + """ + params = { + k: v + for k, v in { + "limit": limit, + "before": before, + "after": after, + "order": enum_value(order) if order is not None else None, + }.items() + if v is not None + } + return await self._client.request_page( + method="get", + path=("user_management", "authkit_oauth_resources"), + model=AuthkitOAuthResource, + params=params, + request_options=request_options, + ) + + async def create_authkit_oauth_resource( + self, + *, + uri: str, + default: bool | None = None, + request_options: RequestOptions | None = None, + ) -> AuthkitOAuthResource: + """Create an MCP resource indicator + + Adds an MCP resource indicator (RFC 8707) to an environment, leaving any others in place. + + Args: + uri: The resource URI. May be a wildcard pattern with a single `*` in the leftmost hostname label, where enabled for the environment. + default: Whether the resource being created becomes the environment default, clearing any previous default. Applies at creation only — this API has no update endpoint yet, so changing the default on an existing resource is done from the dashboard. A wildcard pattern cannot be the default. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + AuthkitOAuthResource + + Raises: + AuthenticationError: If the API key is invalid (401). + 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 { + "uri": uri, + "default": default, + }.items() + if v is not None + } + return await self._client.request( + method="post", + path=("user_management", "authkit_oauth_resources"), + body=body, + model=AuthkitOAuthResource, + request_options=request_options, + ) + + async def delete_authkit_oauth_resource( + self, + id: str, + *, + request_options: RequestOptions | None = None, + ) -> None: + """Delete an MCP resource indicator + + Removes an MCP resource indicator from an environment. Any application consents granted against it are removed too. + + Args: + id: The ID of the MCP resource indicator to delete. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Raises: + 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. + """ + await self._client.request( + method="delete", + path=("user_management", "authkit_oauth_resources", str(id)), + request_options=request_options, + ) + async def list_cors_origins( self, *, diff --git a/src/workos/user_management/models/__init__.py b/src/workos/user_management/models/__init__.py index b5f36473..110ab110 100644 --- a/src/workos/user_management/models/__init__.py +++ b/src/workos/user_management/models/__init__.py @@ -15,6 +15,7 @@ from .authenticate_response_oauth_token import ( AuthenticateResponseOAuthToken as AuthenticateResponseOAuthToken, ) +from .authkit_oauth_resource import AuthkitOAuthResource as AuthkitOAuthResource from .authorization_code_session_authenticate_request import ( AuthorizationCodeSessionAuthenticateRequest as AuthorizationCodeSessionAuthenticateRequest, ) @@ -23,6 +24,9 @@ ) from .confirm_email_change import ConfirmEmailChange as ConfirmEmailChange from .cors_origin_response import CORSOriginResponse as CORSOriginResponse +from .create_authkit_oauth_resource import ( + CreateAuthkitOAuthResource as CreateAuthkitOAuthResource, +) from .create_cors_origin import CreateCORSOrigin as CreateCORSOrigin from .create_magic_code_and_return import ( CreateMagicCodeAndReturn as CreateMagicCodeAndReturn, diff --git a/src/workos/user_management/models/authkit_oauth_resource.py b/src/workos/user_management/models/authkit_oauth_resource.py new file mode 100644 index 00000000..e25fff77 --- /dev/null +++ b/src/workos/user_management/models/authkit_oauth_resource.py @@ -0,0 +1,52 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Literal + +from workos._types import _raise_deserialize_error + + +@dataclass(slots=True) +class AuthkitOAuthResource: + """Authkit O Auth Resource model.""" + + object: Literal["authkit_oauth_resource"] + """The object type.""" + id: str + """The ID of the MCP resource indicator.""" + uri: str + """The resource URI.""" + default: bool + """Whether this is the default MCP resource indicator for the environment.""" + created_at: str + """The timestamp when the MCP resource indicator was created.""" + updated_at: str + """The timestamp when the MCP resource indicator was last updated.""" + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> AuthkitOAuthResource: + """Deserialize from a dictionary.""" + try: + return cls( + object=data.get("object", "authkit_oauth_resource"), + id=data["id"], + uri=data["uri"], + default=data["default"], + created_at=data["created_at"], + updated_at=data["updated_at"], + ) + except (KeyError, ValueError) as e: + _raise_deserialize_error("AuthkitOAuthResource", e) + + def to_dict(self) -> dict[str, Any]: + """Serialize to a dictionary.""" + result: dict[str, Any] = {} + result["object"] = self.object + result["id"] = self.id + result["uri"] = self.uri + result["default"] = self.default + result["created_at"] = self.created_at + result["updated_at"] = self.updated_at + return result diff --git a/src/workos/user_management/models/create_authkit_oauth_resource.py b/src/workos/user_management/models/create_authkit_oauth_resource.py new file mode 100644 index 00000000..2db9b4bc --- /dev/null +++ b/src/workos/user_management/models/create_authkit_oauth_resource.py @@ -0,0 +1,37 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + +from workos._types import _raise_deserialize_error + + +@dataclass(slots=True) +class CreateAuthkitOAuthResource: + """Create Authkit O Auth Resource model.""" + + uri: str + """The resource URI. May be a wildcard pattern with a single `*` in the leftmost hostname label, where enabled for the environment.""" + default: bool | None = None + """Whether the resource being created becomes the environment default, clearing any previous default. Applies at creation only — this API has no update endpoint yet, so changing the default on an existing resource is done from the dashboard. A wildcard pattern cannot be the default.""" + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> CreateAuthkitOAuthResource: + """Deserialize from a dictionary.""" + try: + return cls( + uri=data["uri"], + default=data.get("default"), + ) + except (KeyError, ValueError) as e: + _raise_deserialize_error("CreateAuthkitOAuthResource", e) + + def to_dict(self) -> dict[str, Any]: + """Serialize to a dictionary.""" + result: dict[str, Any] = {} + result["uri"] = self.uri + if self.default is not None: + result["default"] = self.default + return result diff --git a/tests/fixtures/account_selection_required_error.json b/tests/fixtures/account_selection_required_error.json new file mode 100644 index 00000000..f946d293 --- /dev/null +++ b/tests/fixtures/account_selection_required_error.json @@ -0,0 +1,4 @@ +{ + "code": "account_selection_required", + "message": "Several connected accounts match this user for this provider. Name one with a connected account id." +} diff --git a/tests/fixtures/authkit_oauth_resource.json b/tests/fixtures/authkit_oauth_resource.json new file mode 100644 index 00000000..34219a5d --- /dev/null +++ b/tests/fixtures/authkit_oauth_resource.json @@ -0,0 +1,8 @@ +{ + "object": "authkit_oauth_resource", + "id": "authkit_oauth_resource_01EHZNVPK3SFK441A1RGBFSHRT", + "uri": "https://api.example.com", + "default": false, + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z" +} diff --git a/tests/fixtures/create_authkit_oauth_resource.json b/tests/fixtures/create_authkit_oauth_resource.json new file mode 100644 index 00000000..29d4f9d8 --- /dev/null +++ b/tests/fixtures/create_authkit_oauth_resource.json @@ -0,0 +1,4 @@ +{ + "uri": "https://api.example.com", + "default": false +} diff --git a/tests/fixtures/create_data_integration.json b/tests/fixtures/create_data_integration.json index 311643a1..019066f2 100644 --- a/tests/fixtures/create_data_integration.json +++ b/tests/fixtures/create_data_integration.json @@ -1,5 +1,6 @@ { "provider": "github", + "ownership": "user", "description": "Production GitHub app", "enabled": true, "scopes": [ diff --git a/tests/fixtures/data_integration.json b/tests/fixtures/data_integration.json index 5a4a1532..c61f861f 100644 --- a/tests/fixtures/data_integration.json +++ b/tests/fixtures/data_integration.json @@ -3,6 +3,7 @@ "id": "data_integration_01EHZNVPK3SFK441A1RGBFSHRT", "slug": "github", "integration_type": "github", + "ownership": "user", "description": "Production GitHub app", "enabled": true, "state": "valid", diff --git a/tests/fixtures/data_integrations_get_user_token_request.json b/tests/fixtures/data_integrations_get_user_token_request.json index 66f0412e..9b96a12f 100644 --- a/tests/fixtures/data_integrations_get_user_token_request.json +++ b/tests/fixtures/data_integrations_get_user_token_request.json @@ -1,5 +1,7 @@ { "user_id": "user_01EHZNVPK3SFK441A1RGBFSHRT", "organization_id": "org_01EHZNVPK3SFK441A1RGBFSHRT", - "connected_account_id": "data_installation_01EHZNVPK3SFK441A1RGBFSHRT" + "connected_account_id": "data_installation_01EHZNVPK3SFK441A1RGBFSHRT", + "connection_owner": "user", + "supports_multiple_connections": true } diff --git a/tests/fixtures/data_integrations_upsert_api_key_request.json b/tests/fixtures/data_integrations_upsert_api_key_request.json index bf0bd81b..45952523 100644 --- a/tests/fixtures/data_integrations_upsert_api_key_request.json +++ b/tests/fixtures/data_integrations_upsert_api_key_request.json @@ -1,5 +1,7 @@ { "user_id": "user_01EHZNVPK3SFK441A1RGBFSHRT", "organization_id": "org_01EHZNVPK3SFK441A1RGBFSHRT", + "connected_account_id": "data_installation_01EHZNVPK3SFK441A1RGBFSHRT", + "connection_owner": "user", "secret": "sk-1234567890abcdef" } diff --git a/tests/fixtures/data_integrations_upsert_client_credentials_request.json b/tests/fixtures/data_integrations_upsert_client_credentials_request.json index 1dc0fac0..9497d49c 100644 --- a/tests/fixtures/data_integrations_upsert_client_credentials_request.json +++ b/tests/fixtures/data_integrations_upsert_client_credentials_request.json @@ -1,6 +1,8 @@ { "user_id": "user_01EHZNVPK3SFK441A1RGBFSHRT", "organization_id": "org_01EHZNVPK3SFK441A1RGBFSHRT", + "connected_account_id": "data_installation_01EHZNVPK3SFK441A1RGBFSHRT", + "connection_owner": "user", "client_id": "3MVG9...", "client_secret": "shhh-secret", "config": { diff --git a/tests/fixtures/data_integrations_vend_credentials_request.json b/tests/fixtures/data_integrations_vend_credentials_request.json index 66f0412e..9b96a12f 100644 --- a/tests/fixtures/data_integrations_vend_credentials_request.json +++ b/tests/fixtures/data_integrations_vend_credentials_request.json @@ -1,5 +1,7 @@ { "user_id": "user_01EHZNVPK3SFK441A1RGBFSHRT", "organization_id": "org_01EHZNVPK3SFK441A1RGBFSHRT", - "connected_account_id": "data_installation_01EHZNVPK3SFK441A1RGBFSHRT" + "connected_account_id": "data_installation_01EHZNVPK3SFK441A1RGBFSHRT", + "connection_owner": "user", + "supports_multiple_connections": true } diff --git a/tests/fixtures/list_authkit_oauth_resource.json b/tests/fixtures/list_authkit_oauth_resource.json new file mode 100644 index 00000000..4d79551c --- /dev/null +++ b/tests/fixtures/list_authkit_oauth_resource.json @@ -0,0 +1,16 @@ +{ + "data": [ + { + "object": "authkit_oauth_resource", + "id": "authkit_oauth_resource_01EHZNVPK3SFK441A1RGBFSHRT", + "uri": "https://api.example.com", + "default": false, + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z" + } + ], + "list_metadata": { + "before": null, + "after": null + } +} diff --git a/tests/fixtures/list_data_integration.json b/tests/fixtures/list_data_integration.json index d41b44b8..b6860c1b 100644 --- a/tests/fixtures/list_data_integration.json +++ b/tests/fixtures/list_data_integration.json @@ -5,6 +5,7 @@ "id": "data_integration_01EHZNVPK3SFK441A1RGBFSHRT", "slug": "github", "integration_type": "github", + "ownership": "user", "description": "Production GitHub app", "enabled": true, "state": "valid", diff --git a/tests/test_agents.py b/tests/test_agents.py index bd2519cc..cc29fdd0 100644 --- a/tests/test_agents.py +++ b/tests/test_agents.py @@ -279,6 +279,7 @@ def test_list_sessions_encodes_query_params(self, workos, httpx_mock): before="cursor before", after="cursor/after", order=PaginationOrder("value_order"), + organization_id="value organization_id/test", agent_blueprint_id="value agent_blueprint_id/test", agent_instance_id="value agent_instance_id/test", ) @@ -287,6 +288,7 @@ def test_list_sessions_encodes_query_params(self, workos, httpx_mock): assert request.url.params["before"] == "cursor before" assert request.url.params["after"] == "cursor/after" assert request.url.params["order"] == "value_order" + assert request.url.params["organization_id"] == "value organization_id/test" assert ( request.url.params["agent_blueprint_id"] == "value agent_blueprint_id/test" ) @@ -623,6 +625,7 @@ async def test_list_sessions_encodes_query_params(self, async_workos, httpx_mock before="cursor before", after="cursor/after", order=PaginationOrder("value_order"), + organization_id="value organization_id/test", agent_blueprint_id="value agent_blueprint_id/test", agent_instance_id="value agent_instance_id/test", ) @@ -631,6 +634,7 @@ async def test_list_sessions_encodes_query_params(self, async_workos, httpx_mock assert request.url.params["before"] == "cursor before" assert request.url.params["after"] == "cursor/after" assert request.url.params["order"] == "value_order" + assert request.url.params["organization_id"] == "value organization_id/test" assert ( request.url.params["agent_blueprint_id"] == "value agent_blueprint_id/test" ) diff --git a/tests/test_common_models_round_trip.py b/tests/test_common_models_round_trip.py index c9476f4e..8498f807 100644 --- a/tests/test_common_models_round_trip.py +++ b/tests/test_common_models_round_trip.py @@ -5,6 +5,7 @@ from tests.generated_helpers import load_fixture from workos.common.models import ( AccessTokenAgentRegistrationCredentialIssuedDataDetail, + AccountSelectionRequiredError, ActionAuthenticationDenied, ActionAuthenticationDeniedData, ActionUserRegistrationDenied, @@ -14942,6 +14943,24 @@ def test_waitlist_user_denied_omits_absent_optional_non_nullable_fields(self): serialized = instance.to_dict() assert "context" not in serialized + def test_account_selection_required_error_round_trip(self): + data = load_fixture("account_selection_required_error.json") + instance = AccountSelectionRequiredError.from_dict(data) + serialized = instance.to_dict() + assert serialized == data + restored = AccountSelectionRequiredError.from_dict(serialized) + assert restored.to_dict() == serialized + + def test_account_selection_required_error_minimal_payload(self): + data = { + "code": "account_selection_required", + "message": "Several connected accounts match this user for this provider. Name one with a connected account id.", + } + instance = AccountSelectionRequiredError.from_dict(data) + serialized = instance.to_dict() + assert serialized["code"] == data["code"] + assert serialized["message"] == data["message"] + def test_auth_method_mismatch_error_round_trip(self): data = load_fixture("auth_method_mismatch_error.json") instance = AuthMethodMismatchError.from_dict(data) diff --git a/tests/test_pipes.py b/tests/test_pipes.py index f2b19494..9955f115 100644 --- a/tests/test_pipes.py +++ b/tests/test_pipes.py @@ -25,6 +25,7 @@ DataIntegrationAuthorizeUrlResponse, DataIntegrationCredentialsResponse, DataIntegrationsListResponse, + PipesOwnership, ) @@ -51,12 +52,14 @@ def test_list_data_integrations_encodes_query_params(self, workos, httpx_mock): before="cursor before", after="cursor/after", order=PaginationOrder("value_order"), + ownership=PipesOwnership("user"), ) request = httpx_mock.get_request() assert request.url.params["limit"] == "10" assert request.url.params["before"] == "cursor before" assert request.url.params["after"] == "cursor/after" assert request.url.params["order"] == "value_order" + assert request.url.params["ownership"] == "user" def test_create_data_integration(self, workos, httpx_mock): httpx_mock.add_response( @@ -178,6 +181,38 @@ def test_create_data_integration_credential(self, workos, httpx_mock): body = json.loads(request.content) assert body["user_id"] == "test_user_id" + def test_list_data_integration_organization(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("data_integration.json"), + ) + result = workos.pipes.list_data_integration_organization("test_slug") + assert isinstance(result, DataIntegration) + assert result.object == "data_integration" + assert result.id == "data_integration_01EHZNVPK3SFK441A1RGBFSHRT" + request = httpx_mock.get_request() + assert request.method == "GET" + assert request.url.path.endswith("/data-integrations/test_slug/organization") + + def test_update_data_integration_organization(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("data_integration.json"), + ) + result = workos.pipes.update_data_integration_organization("test_slug") + assert isinstance(result, DataIntegration) + assert result.object == "data_integration" + assert result.id == "data_integration_01EHZNVPK3SFK441A1RGBFSHRT" + request = httpx_mock.get_request() + assert request.method == "PUT" + assert request.url.path.endswith("/data-integrations/test_slug/organization") + + def test_delete_data_integration_organization(self, workos, httpx_mock): + httpx_mock.add_response(status_code=204) + result = workos.pipes.delete_data_integration_organization("test_slug") + assert result is None + request = httpx_mock.get_request() + assert request.method == "DELETE" + assert request.url.path.endswith("/data-integrations/test_slug/organization") + def test_get_access_token(self, workos, httpx_mock): httpx_mock.add_response( json=load_fixture("data_integration_access_token_response.json"), @@ -212,10 +247,12 @@ def test_get_user_connected_account_encodes_query_params(self, workos, httpx_moc "test_user_id", "test_slug", organization_id="value organization_id/test", + supports_multiple_connections=True, connected_account_id="value connected_account_id/test", ) request = httpx_mock.get_request() assert request.url.params["organization_id"] == "value organization_id/test" + assert request.url.params["supports_multiple_connections"] == "true" assert ( request.url.params["connected_account_id"] == "value connected_account_id/test" @@ -267,10 +304,12 @@ def test_update_user_connected_account_encodes_query_params( "test_user_id", "test_slug", organization_id="value organization_id/test", + supports_multiple_connections=True, connected_account_id="value connected_account_id/test", ) request = httpx_mock.get_request() assert request.url.params["organization_id"] == "value organization_id/test" + assert request.url.params["supports_multiple_connections"] == "true" assert ( request.url.params["connected_account_id"] == "value connected_account_id/test" @@ -294,10 +333,12 @@ def test_delete_user_connected_account_encodes_query_params( "test_user_id", "test_slug", organization_id="value organization_id/test", + supports_multiple_connections=True, connected_account_id="value connected_account_id/test", ) request = httpx_mock.get_request() assert request.url.params["organization_id"] == "value organization_id/test" + assert request.url.params["supports_multiple_connections"] == "true" assert ( request.url.params["connected_account_id"] == "value connected_account_id/test" @@ -321,10 +362,13 @@ def test_list_user_data_providers_encodes_query_params(self, workos, httpx_mock) json=load_fixture("data_integrations_list_response.json") ) workos.pipes.list_user_data_providers( - "test_user_id", organization_id="value organization_id/test" + "test_user_id", + organization_id="value organization_id/test", + supports_multiple_connections=True, ) request = httpx_mock.get_request() assert request.url.params["organization_id"] == "value organization_id/test" + assert request.url.params["supports_multiple_connections"] == "true" def test_list_data_integrations_with_request_options(self, workos, httpx_mock): httpx_mock.add_response(json={"data": [], "list_metadata": {}}) @@ -428,12 +472,14 @@ async def test_list_data_integrations_encodes_query_params( before="cursor before", after="cursor/after", order=PaginationOrder("value_order"), + ownership=PipesOwnership("user"), ) request = httpx_mock.get_request() assert request.url.params["limit"] == "10" assert request.url.params["before"] == "cursor before" assert request.url.params["after"] == "cursor/after" assert request.url.params["order"] == "value_order" + assert request.url.params["ownership"] == "user" @pytest.mark.asyncio async def test_create_data_integration(self, async_workos, httpx_mock): @@ -544,6 +590,43 @@ async def test_create_data_integration_credential(self, async_workos, httpx_mock assert request.method == "POST" assert request.url.path.endswith("/data-integrations/test_slug/credentials") + @pytest.mark.asyncio + async def test_list_data_integration_organization(self, async_workos, httpx_mock): + httpx_mock.add_response(json=load_fixture("data_integration.json")) + result = await async_workos.pipes.list_data_integration_organization( + "test_slug" + ) + assert isinstance(result, DataIntegration) + assert result.object == "data_integration" + assert result.id == "data_integration_01EHZNVPK3SFK441A1RGBFSHRT" + request = httpx_mock.get_request() + assert request.method == "GET" + assert request.url.path.endswith("/data-integrations/test_slug/organization") + + @pytest.mark.asyncio + async def test_update_data_integration_organization(self, async_workos, httpx_mock): + httpx_mock.add_response(json=load_fixture("data_integration.json")) + result = await async_workos.pipes.update_data_integration_organization( + "test_slug" + ) + assert isinstance(result, DataIntegration) + assert result.object == "data_integration" + assert result.id == "data_integration_01EHZNVPK3SFK441A1RGBFSHRT" + request = httpx_mock.get_request() + assert request.method == "PUT" + assert request.url.path.endswith("/data-integrations/test_slug/organization") + + @pytest.mark.asyncio + async def test_delete_data_integration_organization(self, async_workos, httpx_mock): + httpx_mock.add_response(status_code=204) + result = await async_workos.pipes.delete_data_integration_organization( + "test_slug" + ) + assert result is None + request = httpx_mock.get_request() + assert request.method == "DELETE" + assert request.url.path.endswith("/data-integrations/test_slug/organization") + @pytest.mark.asyncio async def test_get_access_token(self, async_workos, httpx_mock): httpx_mock.add_response( @@ -583,10 +666,12 @@ async def test_get_user_connected_account_encodes_query_params( "test_user_id", "test_slug", organization_id="value organization_id/test", + supports_multiple_connections=True, connected_account_id="value connected_account_id/test", ) request = httpx_mock.get_request() assert request.url.params["organization_id"] == "value organization_id/test" + assert request.url.params["supports_multiple_connections"] == "true" assert ( request.url.params["connected_account_id"] == "value connected_account_id/test" @@ -642,10 +727,12 @@ async def test_update_user_connected_account_encodes_query_params( "test_user_id", "test_slug", organization_id="value organization_id/test", + supports_multiple_connections=True, connected_account_id="value connected_account_id/test", ) request = httpx_mock.get_request() assert request.url.params["organization_id"] == "value organization_id/test" + assert request.url.params["supports_multiple_connections"] == "true" assert ( request.url.params["connected_account_id"] == "value connected_account_id/test" @@ -673,10 +760,12 @@ async def test_delete_user_connected_account_encodes_query_params( "test_user_id", "test_slug", organization_id="value organization_id/test", + supports_multiple_connections=True, connected_account_id="value connected_account_id/test", ) request = httpx_mock.get_request() assert request.url.params["organization_id"] == "value organization_id/test" + assert request.url.params["supports_multiple_connections"] == "true" assert ( request.url.params["connected_account_id"] == "value connected_account_id/test" @@ -704,10 +793,13 @@ async def test_list_user_data_providers_encodes_query_params( json=load_fixture("data_integrations_list_response.json") ) await async_workos.pipes.list_user_data_providers( - "test_user_id", organization_id="value organization_id/test" + "test_user_id", + organization_id="value organization_id/test", + supports_multiple_connections=True, ) request = httpx_mock.get_request() assert request.url.params["organization_id"] == "value organization_id/test" + assert request.url.params["supports_multiple_connections"] == "true" @pytest.mark.asyncio async def test_list_data_integrations_with_request_options( diff --git a/tests/test_pipes_models_round_trip.py b/tests/test_pipes_models_round_trip.py index be400f2a..cab9c812 100644 --- a/tests/test_pipes_models_round_trip.py +++ b/tests/test_pipes_models_round_trip.py @@ -229,6 +229,7 @@ def test_data_integration_minimal_payload(self): "id": "data_integration_01EHZNVPK3SFK441A1RGBFSHRT", "slug": "github", "integration_type": "github", + "ownership": "user", "description": None, "enabled": True, "state": "valid", @@ -248,6 +249,7 @@ def test_data_integration_minimal_payload(self): assert serialized["id"] == data["id"] assert serialized["slug"] == data["slug"] assert serialized["integration_type"] == data["integration_type"] + assert serialized["ownership"] == data["ownership"] assert serialized["description"] == data["description"] assert serialized["enabled"] == data["enabled"] assert serialized["state"] == data["state"] @@ -267,6 +269,7 @@ def test_data_integration_preserves_nullable_fields(self): "id": "data_integration_01EHZNVPK3SFK441A1RGBFSHRT", "slug": "github", "integration_type": "github", + "ownership": "user", "description": None, "enabled": True, "state": "valid", @@ -294,9 +297,10 @@ def test_data_integration_round_trips_unknown_enum_values(self): "id": "data_integration_01EHZNVPK3SFK441A1RGBFSHRT", "slug": "github", "integration_type": "github", + "ownership": "unexpected_data_integration_ownership", "description": "Production GitHub app", "enabled": True, - "state": "unexpected_data_integration_state", + "state": "valid", "scopes": ["repo", "read:org"], "redirect_uri": "https://api.workos.com/data-integrations/github/dik_01EHZNVPK3SFK441A1RGBFSHRT/callback", "auth_methods": ["oauth"], @@ -909,7 +913,7 @@ def test_data_integration_installation_round_trip(self): def test_data_integration_installation_minimal_payload(self): data = { "id": "data_installation_01EHZNVPK3SFK441A1RGBFSHRT", - "user_id": "user_01EHZNVPK3SFK441A1RGBFSHRT", + "user_id": None, "organization_id": None, "api_key_last_4": None, } @@ -923,12 +927,13 @@ def test_data_integration_installation_minimal_payload(self): def test_data_integration_installation_preserves_nullable_fields(self): data = { "id": "data_installation_01EHZNVPK3SFK441A1RGBFSHRT", - "user_id": "user_01EHZNVPK3SFK441A1RGBFSHRT", + "user_id": None, "organization_id": None, "api_key_last_4": None, } instance = DataIntegrationInstallation.from_dict(data) serialized = instance.to_dict() + assert serialized["user_id"] is None assert serialized["organization_id"] is None assert serialized["api_key_last_4"] is None diff --git a/tests/test_user_management.py b/tests/test_user_management.py index 3a76684d..ca1faa4f 100644 --- a/tests/test_user_management.py +++ b/tests/test_user_management.py @@ -23,6 +23,7 @@ from workos.user_management._resource import PasswordPlaintext from workos.user_management.models import ( AuthenticateResponse, + AuthkitOAuthResource, AuthorizedConnectApplicationListData, CORSOriginResponse, DeviceAuthorizationResponse, @@ -147,6 +148,61 @@ def test_revoke_session(self, workos, httpx_mock): assert request.method == "POST" assert request.url.path.endswith("/user_management/sessions/revoke") + def test_list_authkit_oauth_resources(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("list_authkit_oauth_resource.json"), + ) + page = workos.user_management.list_authkit_oauth_resources() + assert isinstance(page, SyncPage) + assert len(page.data) == 1 + assert isinstance(page.data[0], AuthkitOAuthResource) + + def test_list_authkit_oauth_resources_empty_page(self, workos, httpx_mock): + httpx_mock.add_response(json={"data": [], "list_metadata": {}}) + page = workos.user_management.list_authkit_oauth_resources() + assert isinstance(page, SyncPage) + assert page.data == [] + + def test_list_authkit_oauth_resources_encodes_query_params( + self, workos, httpx_mock + ): + httpx_mock.add_response(json={"data": [], "list_metadata": {}}) + workos.user_management.list_authkit_oauth_resources( + limit=10, + before="cursor before", + after="cursor/after", + order=PaginationOrder("value_order"), + ) + request = httpx_mock.get_request() + assert request.url.params["limit"] == "10" + assert request.url.params["before"] == "cursor before" + assert request.url.params["after"] == "cursor/after" + assert request.url.params["order"] == "value_order" + + def test_create_authkit_oauth_resource(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("authkit_oauth_resource.json"), + ) + result = workos.user_management.create_authkit_oauth_resource(uri="test_uri") + assert isinstance(result, AuthkitOAuthResource) + assert result.object == "authkit_oauth_resource" + assert result.id == "authkit_oauth_resource_01EHZNVPK3SFK441A1RGBFSHRT" + request = httpx_mock.get_request() + assert request.method == "POST" + assert request.url.path.endswith("/user_management/authkit_oauth_resources") + body = json.loads(request.content) + assert body["uri"] == "test_uri" + + def test_delete_authkit_oauth_resource(self, workos, httpx_mock): + httpx_mock.add_response(status_code=204) + result = workos.user_management.delete_authkit_oauth_resource("test_id") + assert result is None + request = httpx_mock.get_request() + assert request.method == "DELETE" + assert request.url.path.endswith( + "/user_management/authkit_oauth_resources/test_id" + ) + def test_list_cors_origins(self, workos, httpx_mock): httpx_mock.add_response( json=load_fixture("list_cors_origin_response.json"), @@ -1149,6 +1205,66 @@ async def test_revoke_session(self, async_workos, httpx_mock): assert request.method == "POST" assert request.url.path.endswith("/user_management/sessions/revoke") + @pytest.mark.asyncio + async def test_list_authkit_oauth_resources(self, async_workos, httpx_mock): + httpx_mock.add_response(json=load_fixture("list_authkit_oauth_resource.json")) + page = await async_workos.user_management.list_authkit_oauth_resources() + assert isinstance(page, AsyncPage) + assert len(page.data) == 1 + assert isinstance(page.data[0], AuthkitOAuthResource) + + @pytest.mark.asyncio + async def test_list_authkit_oauth_resources_empty_page( + self, async_workos, httpx_mock + ): + httpx_mock.add_response(json={"data": [], "list_metadata": {}}) + page = await async_workos.user_management.list_authkit_oauth_resources() + assert isinstance(page, AsyncPage) + assert page.data == [] + + @pytest.mark.asyncio + async def test_list_authkit_oauth_resources_encodes_query_params( + self, async_workos, httpx_mock + ): + httpx_mock.add_response(json={"data": [], "list_metadata": {}}) + await async_workos.user_management.list_authkit_oauth_resources( + limit=10, + before="cursor before", + after="cursor/after", + order=PaginationOrder("value_order"), + ) + request = httpx_mock.get_request() + assert request.url.params["limit"] == "10" + assert request.url.params["before"] == "cursor before" + assert request.url.params["after"] == "cursor/after" + assert request.url.params["order"] == "value_order" + + @pytest.mark.asyncio + async def test_create_authkit_oauth_resource(self, async_workos, httpx_mock): + httpx_mock.add_response(json=load_fixture("authkit_oauth_resource.json")) + result = await async_workos.user_management.create_authkit_oauth_resource( + uri="test_uri" + ) + assert isinstance(result, AuthkitOAuthResource) + assert result.object == "authkit_oauth_resource" + assert result.id == "authkit_oauth_resource_01EHZNVPK3SFK441A1RGBFSHRT" + request = httpx_mock.get_request() + assert request.method == "POST" + assert request.url.path.endswith("/user_management/authkit_oauth_resources") + + @pytest.mark.asyncio + async def test_delete_authkit_oauth_resource(self, async_workos, httpx_mock): + httpx_mock.add_response(status_code=204) + result = await async_workos.user_management.delete_authkit_oauth_resource( + "test_id" + ) + assert result is None + request = httpx_mock.get_request() + assert request.method == "DELETE" + assert request.url.path.endswith( + "/user_management/authkit_oauth_resources/test_id" + ) + @pytest.mark.asyncio async def test_list_cors_origins(self, async_workos, httpx_mock): httpx_mock.add_response(json=load_fixture("list_cors_origin_response.json")) diff --git a/tests/test_user_management_models_round_trip.py b/tests/test_user_management_models_round_trip.py index 07a63f68..c84fdf7e 100644 --- a/tests/test_user_management_models_round_trip.py +++ b/tests/test_user_management_models_round_trip.py @@ -7,6 +7,7 @@ AuthenticateResponse, AuthenticateResponseImpersonator, AuthenticateResponseOAuthToken, + AuthkitOAuthResource, AuthorizedConnectApplicationListData, CORSOriginResponse, DeviceAuthorizationResponse, @@ -200,6 +201,32 @@ def test_send_radar_sms_challenge_response_minimal_payload(self): assert serialized["verification_id"] == data["verification_id"] assert serialized["phone_number"] == data["phone_number"] + def test_authkit_oauth_resource_round_trip(self): + data = load_fixture("authkit_oauth_resource.json") + instance = AuthkitOAuthResource.from_dict(data) + serialized = instance.to_dict() + assert serialized == data + restored = AuthkitOAuthResource.from_dict(serialized) + assert restored.to_dict() == serialized + + def test_authkit_oauth_resource_minimal_payload(self): + data = { + "object": "authkit_oauth_resource", + "id": "authkit_oauth_resource_01EHZNVPK3SFK441A1RGBFSHRT", + "uri": "https://api.example.com", + "default": False, + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + } + instance = AuthkitOAuthResource.from_dict(data) + serialized = instance.to_dict() + assert serialized["object"] == data["object"] + assert serialized["id"] == data["id"] + assert serialized["uri"] == data["uri"] + assert serialized["default"] == data["default"] + assert serialized["created_at"] == data["created_at"] + assert serialized["updated_at"] == data["updated_at"] + def test_redirect_uri_round_trip(self): data = load_fixture("redirect_uri.json") instance = RedirectUri.from_dict(data)