diff --git a/tests/test_organizations.py b/tests/test_organizations.py index b7cf5322..ddd08060 100644 --- a/tests/test_organizations.py +++ b/tests/test_organizations.py @@ -109,7 +109,7 @@ def test_get_organization( assert request_kwargs["method"] == "get" assert request_kwargs["url"].endswith("/organizations/organization_id") - def test_get_organization_by_lookup_key( + def test_get_organization_by_external_id( self, mock_organization, capture_and_mock_http_client_request ): request_kwargs = capture_and_mock_http_client_request( @@ -117,12 +117,12 @@ def test_get_organization_by_lookup_key( ) organization = syncify( - self.organizations.get_organization_by_lookup_key(lookup_key="test") + self.organizations.get_organization_by_external_id(external_id="test") ) assert organization.dict() == mock_organization assert request_kwargs["method"] == "get" - assert request_kwargs["url"].endswith("/organizations/by_lookup_key/test") + assert request_kwargs["url"].endswith("/organizations/external_id/test") def test_create_organization_with_domain_data( self, mock_organization, capture_and_mock_http_client_request diff --git a/tests/test_user_management.py b/tests/test_user_management.py index f0aed6e9..e41dd99a 100644 --- a/tests/test_user_management.py +++ b/tests/test_user_management.py @@ -392,6 +392,27 @@ def test_get_user(self, mock_user, capture_and_mock_http_client_request): assert user.profile_picture_url == "https://example.com/profile-picture.jpg" assert user.last_sign_in_at == "2021-06-25T19:07:33.155Z" + def test_get_user_by_external_id( + self, mock_user, capture_and_mock_http_client_request + ): + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_user, 200 + ) + + external_id = "external-id" + user = syncify( + self.user_management.get_user_by_external_id(external_id=external_id) + ) + + assert request_kwargs["url"].endswith( + f"user_management/users/external_id/{external_id}" + ) + assert request_kwargs["method"] == "get" + assert user.id == "user_01H7ZGXFP5C6BBQY6Z7277ZCT0" + assert user.profile_picture_url == "https://example.com/profile-picture.jpg" + assert user.last_sign_in_at == "2021-06-25T19:07:33.155Z" + assert user.metadata == mock_user["metadata"] + def test_list_users_auto_pagination( self, mock_users_multiple_pages, diff --git a/tests/utils/fixtures/mock_organization.py b/tests/utils/fixtures/mock_organization.py index 04905845..3316105b 100644 --- a/tests/utils/fixtures/mock_organization.py +++ b/tests/utils/fixtures/mock_organization.py @@ -22,4 +22,5 @@ def __init__(self, id): domain="example.io", ) ], + metadata={"key": "value"}, ) diff --git a/tests/utils/fixtures/mock_user.py b/tests/utils/fixtures/mock_user.py index 6a12710c..82bbfcfc 100644 --- a/tests/utils/fixtures/mock_user.py +++ b/tests/utils/fixtures/mock_user.py @@ -17,4 +17,5 @@ def __init__(self, id): last_sign_in_at="2021-06-25T19:07:33.155Z", created_at=now, updated_at=now, + metadata={"key": "value"}, ) diff --git a/workos/organizations.py b/workos/organizations.py index 4ff234f5..f60ff703 100644 --- a/workos/organizations.py +++ b/workos/organizations.py @@ -1,5 +1,6 @@ from typing import Optional, Protocol, Sequence +from workos.types.metadata import Metadata from workos.types.organizations.domain_data_input import DomainDataInput from workos.types.organizations.list_filters import OrganizationListFilters from workos.types.roles.role import RoleList @@ -60,13 +61,13 @@ def get_organization(self, organization_id: str) -> SyncOrAsync[Organization]: """ ... - def get_organization_by_lookup_key( - self, lookup_key: str + def get_organization_by_external_id( + self, external_id: str ) -> SyncOrAsync[Organization]: - """Gets details for a single Organization by lookup key + """Gets details for a single Organization by external id Args: - lookup_key (str): Organization's lookup key + external_id (str): Organization's external id Returns: Organization: Organization response from WorkOS @@ -79,6 +80,8 @@ def create_organization( name: str, domain_data: Optional[Sequence[DomainDataInput]] = None, idempotency_key: Optional[str] = None, + external_id: Optional[str] = None, + metadata: Optional[Metadata] = None, ) -> SyncOrAsync[Organization]: """Create an organization @@ -98,6 +101,8 @@ def update_organization( organization_id: str, name: Optional[str] = None, domain_data: Optional[Sequence[DomainDataInput]] = None, + external_id: Optional[str] = None, + metadata: Optional[Metadata] = None, ) -> SyncOrAsync[Organization]: """Update an organization @@ -125,7 +130,6 @@ def delete_organization(self, organization_id: str) -> SyncOrAsync[None]: class Organizations(OrganizationsModule): - _http_client: SyncHTTPClient def __init__(self, http_client: SyncHTTPClient): @@ -167,9 +171,9 @@ def get_organization(self, organization_id: str) -> Organization: return Organization.model_validate(response) - def get_organization_by_lookup_key(self, lookup_key: str) -> Organization: + def get_organization_by_external_id(self, external_id: str) -> Organization: response = self._http_client.request( - "organizations/by_lookup_key/{lookup_key}".format(lookup_key=lookup_key), + "organizations/external_id/{external_id}".format(external_id=external_id), method=REQUEST_METHOD_GET, ) @@ -181,6 +185,8 @@ def create_organization( name: str, domain_data: Optional[Sequence[DomainDataInput]] = None, idempotency_key: Optional[str] = None, + external_id: Optional[str] = None, + metadata: Optional[Metadata] = None, ) -> Organization: headers = {} if idempotency_key: @@ -190,6 +196,8 @@ def create_organization( "name": name, "domain_data": domain_data, "idempotency_key": idempotency_key, + "external_id": external_id, + "metadata": metadata, } response = self._http_client.request( @@ -208,11 +216,15 @@ def update_organization( name: Optional[str] = None, domain_data: Optional[Sequence[DomainDataInput]] = None, stripe_customer_id: Optional[str] = None, + external_id: Optional[str] = None, + metadata: Optional[Metadata] = None, ) -> Organization: json = { "name": name, "domain_data": domain_data, "stripe_customer_id": stripe_customer_id, + "external_id": external_id, + "metadata": metadata, } response = self._http_client.request( @@ -237,7 +249,6 @@ def list_organization_roles(self, organization_id: str) -> RoleList: class AsyncOrganizations(OrganizationsModule): - _http_client: AsyncHTTPClient def __init__(self, http_client: AsyncHTTPClient): @@ -279,9 +290,9 @@ async def get_organization(self, organization_id: str) -> Organization: return Organization.model_validate(response) - async def get_organization_by_lookup_key(self, lookup_key: str) -> Organization: + async def get_organization_by_external_id(self, external_id: str) -> Organization: response = await self._http_client.request( - "organizations/by_lookup_key/{lookup_key}".format(lookup_key=lookup_key), + "organizations/external_id/{external_id}".format(external_id=external_id), method=REQUEST_METHOD_GET, ) @@ -293,6 +304,8 @@ async def create_organization( name: str, domain_data: Optional[Sequence[DomainDataInput]] = None, idempotency_key: Optional[str] = None, + external_id: Optional[str] = None, + metadata: Optional[Metadata] = None, ) -> Organization: headers = {} if idempotency_key: @@ -302,6 +315,8 @@ async def create_organization( "name": name, "domain_data": domain_data, "idempotency_key": idempotency_key, + "external_id": external_id, + "metadata": metadata, } response = await self._http_client.request( @@ -320,11 +335,15 @@ async def update_organization( name: Optional[str] = None, domain_data: Optional[Sequence[DomainDataInput]] = None, stripe_customer_id: Optional[str] = None, + external_id: Optional[str] = None, + metadata: Optional[Metadata] = None, ) -> Organization: json = { "name": name, "domain_data": domain_data, "stripe_customer_id": stripe_customer_id, + "external_id": external_id, + "metadata": metadata, } response = await self._http_client.request( diff --git a/workos/types/metadata.py b/workos/types/metadata.py new file mode 100644 index 00000000..9a108d10 --- /dev/null +++ b/workos/types/metadata.py @@ -0,0 +1,4 @@ +from typing import Dict + + +Metadata = Dict[str, str] diff --git a/workos/types/organizations/organization.py b/workos/types/organizations/organization.py index 23186498..74846aee 100644 --- a/workos/types/organizations/organization.py +++ b/workos/types/organizations/organization.py @@ -1,4 +1,6 @@ +from dataclasses import field from typing import Optional, Sequence +from workos.types.metadata import Metadata from workos.types.organizations.organization_common import OrganizationCommon from workos.types.organizations.organization_domain import OrganizationDomain @@ -6,5 +8,6 @@ class Organization(OrganizationCommon): allow_profiles_outside_organization: bool domains: Sequence[OrganizationDomain] - lookup_key: Optional[str] = None stripe_customer_id: Optional[str] = None + external_id: Optional[str] = None + metadata: Metadata = field(default_factory=dict) diff --git a/workos/types/user_management/user.py b/workos/types/user_management/user.py index ca1e4c1b..2bd72ae3 100644 --- a/workos/types/user_management/user.py +++ b/workos/types/user_management/user.py @@ -1,4 +1,6 @@ +from dataclasses import field from typing import Literal, Optional +from workos.types.metadata import Metadata from workos.types.workos_model import WorkOSModel @@ -15,3 +17,5 @@ class User(WorkOSModel): last_sign_in_at: Optional[str] = None created_at: str updated_at: str + external_id: Optional[str] = None + metadata: Metadata = field(default_factory=dict) diff --git a/workos/user_management.py b/workos/user_management.py index c684eb4e..da6e7498 100644 --- a/workos/user_management.py +++ b/workos/user_management.py @@ -8,6 +8,7 @@ ListPage, WorkOSListResource, ) +from workos.types.metadata import Metadata from workos.types.mfa import ( AuthenticationFactor, AuthenticationFactorTotpAndChallengeResponse, @@ -66,6 +67,7 @@ USER_PATH = "user_management/users" USER_DETAIL_PATH = "user_management/users/{0}" +USER_DETAIL_BY_EXTERNAL_ID_PATH = "user_management/users/external_id/{0}" ORGANIZATION_MEMBERSHIP_PATH = "user_management/organization_memberships" ORGANIZATION_MEMBERSHIP_DETAIL_PATH = "user_management/organization_memberships/{0}" ORGANIZATION_MEMBERSHIP_DEACTIVATE_PATH = ( @@ -137,6 +139,16 @@ def get_user(self, user_id: str) -> SyncOrAsync[User]: """ ... + def get_user_by_external_id(self, external_id: str) -> SyncOrAsync[User]: + """Get the details of an existing user by external id. + + Args: + external_id (str): User's external id + Returns: + User: User response from WorkOS. + """ + ... + def list_users( self, *, @@ -172,6 +184,8 @@ def create_user( first_name: Optional[str] = None, last_name: Optional[str] = None, email_verified: Optional[bool] = None, + external_id: Optional[str] = None, + metadata: Optional[Metadata] = None, ) -> SyncOrAsync[User]: """Create a new user. @@ -199,6 +213,8 @@ def update_user( password: Optional[str] = None, password_hash: Optional[str] = None, password_hash_type: Optional[PasswordHashType] = None, + external_id: Optional[str] = None, + metadata: Optional[Metadata] = None, ) -> SyncOrAsync[User]: """Update user attributes. @@ -389,7 +405,6 @@ def get_authorization_url( ) if connection_id is not None: - params["connection_id"] = connection_id if organization_id is not None: params["organization_id"] = organization_id @@ -860,6 +875,14 @@ def get_user(self, user_id: str) -> User: return User.model_validate(response) + def get_user_by_external_id(self, external_id: str) -> User: + response = self._http_client.request( + USER_DETAIL_BY_EXTERNAL_ID_PATH.format(external_id), + method=REQUEST_METHOD_GET, + ) + + return User.model_validate(response) + def list_users( self, *, @@ -899,6 +922,8 @@ def create_user( first_name: Optional[str] = None, last_name: Optional[str] = None, email_verified: Optional[bool] = None, + external_id: Optional[str] = None, + metadata: Optional[Metadata] = None, ) -> User: json = { "email": email, @@ -908,6 +933,8 @@ def create_user( "first_name": first_name, "last_name": last_name, "email_verified": email_verified or False, + "external_id": external_id, + "metadata": metadata, } response = self._http_client.request( @@ -926,6 +953,8 @@ def update_user( password: Optional[str] = None, password_hash: Optional[str] = None, password_hash_type: Optional[PasswordHashType] = None, + external_id: Optional[str] = None, + metadata: Optional[Metadata] = None, ) -> User: json = { "first_name": first_name, @@ -934,6 +963,8 @@ def update_user( "password": password, "password_hash": password_hash, "password_hash_type": password_hash_type, + "external_id": external_id, + "metadata": metadata, } response = self._http_client.request( @@ -1464,6 +1495,14 @@ async def get_user(self, user_id: str) -> User: return User.model_validate(response) + async def get_user_by_external_id(self, external_id: str) -> User: + response = await self._http_client.request( + USER_DETAIL_BY_EXTERNAL_ID_PATH.format(external_id), + method=REQUEST_METHOD_GET, + ) + + return User.model_validate(response) + async def list_users( self, *, @@ -1503,6 +1542,8 @@ async def create_user( first_name: Optional[str] = None, last_name: Optional[str] = None, email_verified: Optional[bool] = None, + external_id: Optional[str] = None, + metadata: Optional[Metadata] = None, ) -> User: json = { "email": email, @@ -1512,6 +1553,8 @@ async def create_user( "first_name": first_name, "last_name": last_name, "email_verified": email_verified or False, + "external_id": external_id, + "metadata": metadata, } response = await self._http_client.request( @@ -1530,6 +1573,8 @@ async def update_user( password: Optional[str] = None, password_hash: Optional[str] = None, password_hash_type: Optional[PasswordHashType] = None, + external_id: Optional[str] = None, + metadata: Optional[Metadata] = None, ) -> User: json = { "first_name": first_name, @@ -1538,6 +1583,8 @@ async def update_user( "password": password, "password_hash": password_hash, "password_hash_type": password_hash_type, + "external_id": external_id, + "metadata": metadata, } response = await self._http_client.request(