diff --git a/tests/conftest.py b/tests/conftest.py index 5591f638..1c22d0a1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,6 +4,7 @@ import httpx import pytest +from tests.utils.client_configuration import ClientConfiguration from tests.utils.list_resource import list_data_to_dicts, list_response_of from workos.types.list_resource import WorkOsListResource from workos.utils.http_client import AsyncHTTPClient, HTTPClient, SyncHTTPClient @@ -13,7 +14,7 @@ def sync_http_client_for_test(): return SyncHTTPClient( api_key="sk_test", - base_url="https://api.workos.test", + base_url="https://api.workos.test/", client_id="client_b27needthisforssotemxo", version="test", ) @@ -23,7 +24,7 @@ def sync_http_client_for_test(): def async_http_client_for_test(): return AsyncHTTPClient( api_key="sk_test", - base_url="https://api.workos.test", + base_url="https://api.workos.test/", client_id="client_b27needthisforssotemxo", version="test", ) diff --git a/tests/test_async_http_client.py b/tests/test_async_http_client.py index 78cf9e6c..6d2c01dd 100644 --- a/tests/test_async_http_client.py +++ b/tests/test_async_http_client.py @@ -20,7 +20,7 @@ def handler(request: httpx.Request) -> httpx.Response: self.http_client = AsyncHTTPClient( api_key="sk_test", - base_url="https://api.workos.test", + base_url="https://api.workos.test/", client_id="client_b27needthisforssotemxo", version="test", transport=httpx.MockTransport(handler), diff --git a/tests/test_client.py b/tests/test_client.py index bd813380..0e1e868e 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -1,3 +1,4 @@ +from http import client import os import pytest from workos import AsyncWorkOSClient, WorkOSClient @@ -64,6 +65,16 @@ def test_initialize_portal(self, default_client): def test_initialize_user_management(self, default_client): assert bool(default_client.user_management) + def test_enforce_trailing_slash_for_base_url( + self, + ): + client = WorkOSClient( + api_key="sk_test", + client_id="client_b27needthisforssotemxo", + base_url="https://api.workos.com", + ) + assert client.base_url == "https://api.workos.com/" + class TestAsyncClient: @pytest.fixture diff --git a/tests/test_sso.py b/tests/test_sso.py index 1182cbe1..187426f4 100644 --- a/tests/test_sso.py +++ b/tests/test_sso.py @@ -1,6 +1,7 @@ import json from six.moves.urllib.parse import parse_qsl, urlparse import pytest +from tests.utils.client_configuration import client_configuration_for_http_client from tests.utils.fixtures.mock_profile import MockProfile from tests.utils.list_resource import list_data_to_dicts, list_response_of from tests.utils.fixtures.mock_connection import MockConnection @@ -51,7 +52,12 @@ class TestSSOBase(SSOFixtures): @pytest.fixture(autouse=True) def setup(self, sync_http_client_for_test): self.http_client = sync_http_client_for_test - self.sso = SSO(http_client=self.http_client) + self.sso = SSO( + http_client=self.http_client, + client_configuration=client_configuration_for_http_client( + sync_http_client_for_test + ), + ) self.provider = "GoogleOAuth" self.customer_domain = "workos.com" self.login_hint = "foo@workos.com" @@ -214,7 +220,12 @@ class TestSSO(SSOFixtures): @pytest.fixture(autouse=True) def setup(self, sync_http_client_for_test): self.http_client = sync_http_client_for_test - self.sso = SSO(http_client=self.http_client) + self.sso = SSO( + http_client=self.http_client, + client_configuration=client_configuration_for_http_client( + sync_http_client_for_test + ), + ) self.provider = "GoogleOAuth" self.customer_domain = "workos.com" self.login_hint = "foo@workos.com" @@ -333,7 +344,12 @@ class TestAsyncSSO(SSOFixtures): @pytest.fixture(autouse=True) def setup(self, async_http_client_for_test): self.http_client = async_http_client_for_test - self.sso = AsyncSSO(http_client=self.http_client) + self.sso = AsyncSSO( + http_client=self.http_client, + client_configuration=client_configuration_for_http_client( + async_http_client_for_test + ), + ) self.provider = "GoogleOAuth" self.customer_domain = "workos.com" self.login_hint = "foo@workos.com" diff --git a/tests/test_sync_http_client.py b/tests/test_sync_http_client.py index 1a09cfeb..bb58f144 100644 --- a/tests/test_sync_http_client.py +++ b/tests/test_sync_http_client.py @@ -32,7 +32,7 @@ def handler(request: httpx.Request) -> httpx.Response: self.http_client = SyncHTTPClient( api_key="sk_test", - base_url="https://api.workos.test", + base_url="https://api.workos.test/", client_id="client_b27needthisforssotemxo", version="test", transport=httpx.MockTransport(handler), @@ -63,7 +63,6 @@ def test_request_without_body( "events", method=method, params={"test_param": "test_value"}, - token="test", ) self.http_client._client.request.assert_called_with( @@ -101,7 +100,7 @@ def test_request_with_body( ) response = self.http_client.request( - "events", method=method, json={"test_param": "test_value"}, token="test" + "events", method=method, json={"test_param": "test_value"} ) self.http_client._client.request.assert_called_with( @@ -144,7 +143,6 @@ def test_request_with_body_and_query_parameters( method=method, params={"test_param": "test_param_value"}, json={"test_json": "test_json_value"}, - token="test", ) self.http_client._client.request.assert_called_with( diff --git a/tests/test_user_management.py b/tests/test_user_management.py index de7a82a0..43cf81f0 100644 --- a/tests/test_user_management.py +++ b/tests/test_user_management.py @@ -1,4 +1,5 @@ import json +from os import sync from six.moves.urllib.parse import parse_qsl, urlparse import pytest @@ -11,6 +12,9 @@ from tests.utils.fixtures.mock_password_reset import MockPasswordReset from tests.utils.fixtures.mock_user import MockUser from tests.utils.list_resource import list_data_to_dicts, list_response_of +from tests.utils.client_configuration import ( + client_configuration_for_http_client, +) from workos.user_management import AsyncUserManagement, UserManagement from workos.utils.request_helper import RESPONSE_TYPE_CODE @@ -144,7 +148,12 @@ class TestUserManagementBase(UserManagementFixtures): @pytest.fixture(autouse=True) def setup(self, sync_http_client_for_test): self.http_client = sync_http_client_for_test - self.user_management = UserManagement(http_client=self.http_client) + self.user_management = UserManagement( + http_client=self.http_client, + client_configuration=client_configuration_for_http_client( + sync_http_client_for_test + ), + ) def test_authorization_url_throws_value_error_with_missing_connection_organization_and_provider( self, @@ -311,7 +320,12 @@ class TestUserManagement(UserManagementFixtures): @pytest.fixture(autouse=True) def setup(self, sync_http_client_for_test): self.http_client = sync_http_client_for_test - self.user_management = UserManagement(http_client=self.http_client) + self.user_management = UserManagement( + http_client=self.http_client, + client_configuration=client_configuration_for_http_client( + sync_http_client_for_test + ), + ) def test_get_user(self, mock_user, capture_and_mock_http_client_request): request_kwargs = capture_and_mock_http_client_request( @@ -946,7 +960,12 @@ class TestAsyncUserManagement(UserManagementFixtures): @pytest.fixture(autouse=True) def setup(self, async_http_client_for_test): self.http_client = async_http_client_for_test - self.user_management = AsyncUserManagement(http_client=self.http_client) + self.user_management = AsyncUserManagement( + http_client=self.http_client, + client_configuration=client_configuration_for_http_client( + async_http_client_for_test + ), + ) async def test_get_user(self, mock_user, capture_and_mock_http_client_request): request_kwargs = capture_and_mock_http_client_request( @@ -1005,7 +1024,7 @@ async def test_update_user(self, mock_user, capture_and_mock_http_client_request "password": "password", } user = await self.user_management.update_user( - "user_01H7ZGXFP5C6BBQY6Z7277ZCT0", **params + user_id="user_01H7ZGXFP5C6BBQY6Z7277ZCT0", **params ) assert request_kwargs["url"].endswith("users/user_01H7ZGXFP5C6BBQY6Z7277ZCT0") diff --git a/tests/utils/client_configuration.py b/tests/utils/client_configuration.py new file mode 100644 index 00000000..78cfac19 --- /dev/null +++ b/tests/utils/client_configuration.py @@ -0,0 +1,33 @@ +from workos._client_configuration import ( + ClientConfiguration as ClientConfigurationProtocol, +) +from workos.utils._base_http_client import BaseHTTPClient + + +class ClientConfiguration(ClientConfigurationProtocol): + def __init__(self, base_url: str, client_id: str, request_timeout: int): + self._base_url = base_url + self._client_id = client_id + self._request_timeout = request_timeout + + @property + def base_url(self) -> str: + return self._base_url + + @property + def client_id(self) -> str: + return self._client_id + + @property + def request_timeout(self) -> int: + return self._request_timeout + + +def client_configuration_for_http_client( + http_client: BaseHTTPClient, +) -> ClientConfiguration: + return ClientConfiguration( + base_url=http_client.base_url, + client_id=http_client.client_id, + request_timeout=http_client.timeout, + ) diff --git a/workos/_base_client.py b/workos/_base_client.py index b68f9b93..41f31a66 100644 --- a/workos/_base_client.py +++ b/workos/_base_client.py @@ -1,8 +1,8 @@ from abc import abstractmethod import os -from typing import Generic, Optional, Type, TypeVar - +from typing import Optional from workos.__about__ import __version__ +from workos._client_configuration import ClientConfiguration from workos.fga import FGAModule from workos.utils._base_http_client import DEFAULT_REQUEST_TIMEOUT from workos.utils.http_client import HTTPClient @@ -18,17 +18,13 @@ from workos.webhooks import WebhooksModule -HTTPClientType = TypeVar("HTTPClientType", bound=HTTPClient) - - -class BaseClient(Generic[HTTPClientType]): +class BaseClient(ClientConfiguration): """Base client for accessing the WorkOS feature set.""" _api_key: str _base_url: str _client_id: str _request_timeout: int - _http_client: HTTPClient def __init__( self, @@ -37,7 +33,6 @@ def __init__( client_id: Optional[str], base_url: Optional[str] = None, request_timeout: Optional[int] = None, - http_client_cls: Type[HTTPClientType], ) -> None: api_key = api_key or os.getenv("WORKOS_API_KEY") if api_key is None: @@ -55,25 +50,20 @@ def __init__( self._client_id = client_id - self._base_url = ( - base_url - if base_url - else os.getenv("WORKOS_BASE_URL", "https://api.workos.com/") + self._base_url = self._enforce_trailing_slash( + url=( + base_url + if base_url + else os.getenv("WORKOS_BASE_URL", "https://api.workos.com/") + ) ) + self._request_timeout = ( request_timeout if request_timeout else int(os.getenv("WORKOS_REQUEST_TIMEOUT", DEFAULT_REQUEST_TIMEOUT)) ) - self._http_client = http_client_cls( - api_key=self._api_key, - base_url=self._base_url, - client_id=self._client_id, - version=__version__, - timeout=self._request_timeout, - ) - @property @abstractmethod def audit_logs(self) -> AuditLogsModule: ... @@ -117,3 +107,18 @@ def user_management(self) -> UserManagementModule: ... @property @abstractmethod def webhooks(self) -> WebhooksModule: ... + + def _enforce_trailing_slash(self, url: str) -> str: + return url if url.endswith("/") else url + "/" + + @property + def base_url(self) -> str: + return self._base_url + + @property + def client_id(self) -> str: + return self._client_id + + @property + def request_timeout(self) -> int: + return self._request_timeout diff --git a/workos/_client_configuration.py b/workos/_client_configuration.py new file mode 100644 index 00000000..c682f83e --- /dev/null +++ b/workos/_client_configuration.py @@ -0,0 +1,10 @@ +from typing import Protocol + + +class ClientConfiguration(Protocol): + @property + def base_url(self) -> str: ... + @property + def client_id(self) -> str: ... + @property + def request_timeout(self) -> int: ... diff --git a/workos/async_client.py b/workos/async_client.py index 4be138a3..5908b348 100644 --- a/workos/async_client.py +++ b/workos/async_client.py @@ -1,5 +1,5 @@ from typing import Optional - +from workos.__about__ import __version__ from workos._base_client import BaseClient from workos.audit_logs import AuditLogsModule from workos.directory_sync import AsyncDirectorySync @@ -15,7 +15,7 @@ from workos.webhooks import WebhooksModule -class AsyncClient(BaseClient[AsyncHTTPClient]): +class AsyncClient(BaseClient): """Client for a convenient way to access the WorkOS feature set.""" _http_client: AsyncHTTPClient @@ -33,13 +33,21 @@ def __init__( client_id=client_id, base_url=base_url, request_timeout=request_timeout, - http_client_cls=AsyncHTTPClient, + ) + self._http_client = AsyncHTTPClient( + api_key=self._api_key, + base_url=self.base_url, + client_id=self._client_id, + version=__version__, + timeout=self.request_timeout, ) @property def sso(self) -> AsyncSSO: if not getattr(self, "_sso", None): - self._sso = AsyncSSO(self._http_client) + self._sso = AsyncSSO( + http_client=self._http_client, client_configuration=self + ) return self._sso @property @@ -93,5 +101,7 @@ def mfa(self) -> MFAModule: @property def user_management(self) -> AsyncUserManagement: if not getattr(self, "_user_management", None): - self._user_management = AsyncUserManagement(self._http_client) + self._user_management = AsyncUserManagement( + http_client=self._http_client, client_configuration=self + ) return self._user_management diff --git a/workos/client.py b/workos/client.py index 16f00c0f..e51e167f 100644 --- a/workos/client.py +++ b/workos/client.py @@ -1,5 +1,5 @@ from typing import Optional - +from workos.__about__ import __version__ from workos._base_client import BaseClient from workos.audit_logs import AuditLogs from workos.directory_sync import DirectorySync @@ -15,7 +15,7 @@ from workos.utils.http_client import SyncHTTPClient -class SyncClient(BaseClient[SyncHTTPClient]): +class SyncClient(BaseClient): """Client for a convenient way to access the WorkOS feature set.""" _http_client: SyncHTTPClient @@ -33,13 +33,19 @@ def __init__( client_id=client_id, base_url=base_url, request_timeout=request_timeout, - http_client_cls=SyncHTTPClient, + ) + self._http_client = SyncHTTPClient( + api_key=self._api_key, + base_url=self.base_url, + client_id=self._client_id, + version=__version__, + timeout=self.request_timeout, ) @property def sso(self) -> SSO: if not getattr(self, "_sso", None): - self._sso = SSO(self._http_client) + self._sso = SSO(http_client=self._http_client, client_configuration=self) return self._sso @property @@ -99,5 +105,7 @@ def mfa(self) -> Mfa: @property def user_management(self) -> UserManagement: if not getattr(self, "_user_management", None): - self._user_management = UserManagement(self._http_client) + self._user_management = UserManagement( + http_client=self._http_client, client_configuration=self + ) return self._user_management diff --git a/workos/directory_sync.py b/workos/directory_sync.py index b681dceb..2f4245ee 100644 --- a/workos/directory_sync.py +++ b/workos/directory_sync.py @@ -60,11 +60,11 @@ def list_groups( order: PaginationOrder = "desc", ) -> SyncOrAsync[DirectoryGroupsListResource]: ... - def get_user(self, user: str) -> SyncOrAsync[DirectoryUserWithGroups]: ... + def get_user(self, user_id: str) -> SyncOrAsync[DirectoryUserWithGroups]: ... - def get_group(self, group: str) -> SyncOrAsync[DirectoryGroup]: ... + def get_group(self, group_id: str) -> SyncOrAsync[DirectoryGroup]: ... - def get_directory(self, directory: str) -> SyncOrAsync[Directory]: ... + def get_directory(self, directory_id: str) -> SyncOrAsync[Directory]: ... def list_directories( self, @@ -77,7 +77,7 @@ def list_directories( order: PaginationOrder = "desc", ) -> SyncOrAsync[DirectoriesListResource]: ... - def delete_directory(self, directory: str) -> SyncOrAsync[None]: ... + def delete_directory(self, directory_id: str) -> SyncOrAsync[None]: ... class DirectorySync(DirectorySyncModule): diff --git a/workos/sso.py b/workos/sso.py index 50f70e26..5757fec0 100644 --- a/workos/sso.py +++ b/workos/sso.py @@ -1,4 +1,5 @@ from typing import Optional, Protocol +from workos._client_configuration import ClientConfiguration from workos.types.sso.connection import ConnectionType from workos.types.sso.sso_provider_type import SsoProviderType from workos.typing.sync_or_async import SyncOrAsync @@ -40,7 +41,7 @@ class ConnectionsListFilters(ListArgs, total=False): class SSOModule(Protocol): - _http_client: HTTPClient + _client_configuration: ClientConfiguration def get_authorization_url( self, @@ -70,7 +71,7 @@ def get_authorization_url( str: URL to redirect a User to to begin the OAuth workflow with WorkOS """ params: QueryParameters = { - "client_id": self._http_client.client_id, + "client_id": self._client_configuration.client_id, "redirect_uri": redirect_uri, "response_type": RESPONSE_TYPE_CODE, } @@ -94,10 +95,12 @@ def get_authorization_url( params["state"] = state return RequestHelper.build_url_with_query_params( - base_url=self._http_client.base_url, path=AUTHORIZATION_PATH, **params + base_url=self._client_configuration.base_url, + path=AUTHORIZATION_PATH, + **params, ) - def get_profile(self, accessToken: str) -> SyncOrAsync[Profile]: ... + def get_profile(self, access_token: str) -> SyncOrAsync[Profile]: ... def get_profile_and_token(self, code: str) -> SyncOrAsync[ProfileAndToken]: ... @@ -117,7 +120,7 @@ def list_connections( order: PaginationOrder = "desc", ) -> SyncOrAsync[ConnectionsListResource]: ... - def delete_connection(self, connection: str) -> SyncOrAsync[None]: ... + def delete_connection(self, connection_id: str) -> SyncOrAsync[None]: ... class SSO(SSOModule): @@ -125,7 +128,10 @@ class SSO(SSOModule): _http_client: SyncHTTPClient - def __init__(self, http_client: SyncHTTPClient): + def __init__( + self, http_client: SyncHTTPClient, client_configuration: ClientConfiguration + ): + self._client_configuration = client_configuration self._http_client = http_client def get_profile(self, access_token: str) -> Profile: @@ -139,7 +145,10 @@ def get_profile(self, access_token: str) -> Profile: Profile """ response = self._http_client.request( - PROFILE_PATH, method=REQUEST_METHOD_GET, token=access_token + PROFILE_PATH, + method=REQUEST_METHOD_GET, + headers={**self._http_client.auth_header_from_token(access_token)}, + exclude_default_auth_headers=True, ) return Profile.model_validate(response) @@ -250,7 +259,10 @@ class AsyncSSO(SSOModule): _http_client: AsyncHTTPClient - def __init__(self, http_client: AsyncHTTPClient): + def __init__( + self, http_client: AsyncHTTPClient, client_configuration: ClientConfiguration + ): + self._client_configuration = client_configuration self._http_client = http_client async def get_profile(self, access_token: str) -> Profile: diff --git a/workos/user_management.py b/workos/user_management.py index ffe2cfd6..2593ec7b 100644 --- a/workos/user_management.py +++ b/workos/user_management.py @@ -1,4 +1,5 @@ from typing import Optional, Protocol, Set +from workos._client_configuration import ClientConfiguration from workos.types.list_resource import ( ListArgs, ListMetadata, @@ -100,7 +101,7 @@ class UserManagementModule(Protocol): - _http_client: HTTPClient + _client_configuration: ClientConfiguration def get_user(self, user_id: str) -> SyncOrAsync[User]: ... @@ -215,7 +216,7 @@ def get_authorization_url( str: URL to redirect a User to to begin the OAuth workflow with WorkOS """ params: QueryParameters = { - "client_id": self._http_client.client_id, + "client_id": self._client_configuration.client_id, "redirect_uri": redirect_uri, "response_type": RESPONSE_TYPE_CODE, } @@ -242,7 +243,9 @@ def get_authorization_url( params["code_challenge_method"] = "S256" return RequestHelper.build_url_with_query_params( - base_url=self._http_client.base_url, path=USER_AUTHORIZATION_PATH, **params + base_url=self._client_configuration.base_url, + path=USER_AUTHORIZATION_PATH, + **params, ) def _authenticate_with( @@ -321,7 +324,7 @@ def get_jwks_url(self) -> str: (str): The public JWKS URL. """ - return f"{self._http_client.base_url}sso/jwks/{self._http_client.client_id}" + return f"{self._client_configuration.base_url}sso/jwks/{self._client_configuration.client_id}" def get_logout_url(self, session_id: str) -> str: """Get the URL for ending the session and redirecting the user @@ -333,7 +336,7 @@ def get_logout_url(self, session_id: str) -> str: (str): URL to redirect the user to to end the session. """ - return f"{self._http_client.base_url}user_management/sessions/logout?session_id={session_id}" + return f"{self._client_configuration.base_url}user_management/sessions/logout?session_id={session_id}" def get_password_reset( self, password_reset_id: str @@ -412,7 +415,10 @@ class UserManagement(UserManagementModule): _http_client: SyncHTTPClient - def __init__(self, http_client: SyncHTTPClient): + def __init__( + self, http_client: SyncHTTPClient, client_configuration: ClientConfiguration + ): + self._client_configuration = client_configuration self._http_client = http_client def get_user(self, user_id: str) -> User: @@ -1344,7 +1350,10 @@ class AsyncUserManagement(UserManagementModule): _http_client: AsyncHTTPClient - def __init__(self, http_client: AsyncHTTPClient): + def __init__( + self, http_client: AsyncHTTPClient, client_configuration: ClientConfiguration + ): + self._client_configuration = client_configuration self._http_client = http_client async def get_user(self, user_id: str) -> User: @@ -1363,6 +1372,7 @@ async def get_user(self, user_id: str) -> User: async def list_users( self, + *, email: Optional[str] = None, organization_id: Optional[str] = None, limit: int = DEFAULT_LIST_RESPONSE_LIMIT, @@ -1405,6 +1415,7 @@ async def list_users( async def create_user( self, + *, email: str, password: Optional[str] = None, password_hash: Optional[str] = None, @@ -1445,6 +1456,7 @@ async def create_user( async def update_user( self, + *, user_id: str, first_name: Optional[str] = None, last_name: Optional[str] = None, @@ -1493,7 +1505,7 @@ async def delete_user(self, user_id: str) -> None: ) async def create_organization_membership( - self, user_id: str, organization_id: str, role_slug: Optional[str] = None + self, *, user_id: str, organization_id: str, role_slug: Optional[str] = None ) -> OrganizationMembership: """Create a new OrganizationMembership for the given Organization and User. @@ -1520,7 +1532,7 @@ async def create_organization_membership( return OrganizationMembership.model_validate(response) async def update_organization_membership( - self, organization_membership_id: str, role_slug: Optional[str] = None + self, *, organization_membership_id: str, role_slug: Optional[str] = None ) -> OrganizationMembership: """Updates an OrganizationMembership for the given id. @@ -1565,6 +1577,7 @@ async def get_organization_membership( async def list_organization_memberships( self, + *, user_id: Optional[str] = None, organization_id: Optional[str] = None, statuses: Optional[Set[OrganizationMembershipStatus]] = None, @@ -1674,6 +1687,7 @@ async def _authenticate_with( async def authenticate_with_password( self, + *, email: str, password: str, ip_address: Optional[str] = None, @@ -1703,6 +1717,7 @@ async def authenticate_with_password( async def authenticate_with_code( self, + *, code: str, code_verifier: Optional[str] = None, ip_address: Optional[str] = None, @@ -1735,6 +1750,7 @@ async def authenticate_with_code( async def authenticate_with_magic_auth( self, + *, code: str, email: str, link_authorization_code: Optional[str] = None, @@ -1767,6 +1783,7 @@ async def authenticate_with_magic_auth( async def authenticate_with_email_verification( self, + *, code: str, pending_authentication_token: str, ip_address: Optional[str] = None, @@ -1796,6 +1813,7 @@ async def authenticate_with_email_verification( async def authenticate_with_totp( self, + *, code: str, authentication_challenge_id: str, pending_authentication_token: str, @@ -1828,6 +1846,7 @@ async def authenticate_with_totp( async def authenticate_with_organization_selection( self, + *, organization_id: str, pending_authentication_token: str, ip_address: Optional[str] = None, @@ -1857,6 +1876,7 @@ async def authenticate_with_organization_selection( async def authenticate_with_refresh_token( self, + *, refresh_token: str, organization_id: Optional[str] = None, ip_address: Optional[str] = None, @@ -1929,7 +1949,7 @@ async def create_password_reset(self, email: str) -> PasswordReset: return PasswordReset.model_validate(response) - async def reset_password(self, token: str, new_password: str) -> User: + async def reset_password(self, *, token: str, new_password: str) -> User: """Resets user password using token that was sent to the user. Kwargs: @@ -1987,7 +2007,7 @@ async def send_verification_email(self, user_id: str) -> User: return User.model_validate(response["user"]) - async def verify_email(self, user_id: str, code: str) -> User: + async def verify_email(self, *, user_id: str, code: str) -> User: """Verifies user email using one-time code that was sent to the user. Kwargs: @@ -2028,6 +2048,7 @@ async def get_magic_auth(self, magic_auth_id: str) -> MagicAuth: async def create_magic_auth( self, + *, email: str, invitation_token: Optional[str] = None, ) -> MagicAuth: @@ -2054,6 +2075,7 @@ async def create_magic_auth( async def enroll_auth_factor( self, + *, user_id: str, type: AuthenticationFactorType, totp_issuer: Optional[str] = None, @@ -2089,6 +2111,7 @@ async def enroll_auth_factor( async def list_auth_factors( self, + *, user_id: str, limit: int = DEFAULT_LIST_RESPONSE_LIMIT, before: Optional[str] = None, @@ -2167,6 +2190,7 @@ async def find_invitation_by_token(self, invitation_token: str) -> Invitation: async def list_invitations( self, + *, email: Optional[str] = None, organization_id: Optional[str] = None, limit: int = DEFAULT_LIST_RESPONSE_LIMIT, @@ -2209,6 +2233,7 @@ async def list_invitations( async def send_invitation( self, + *, email: str, organization_id: Optional[str] = None, expires_in_days: Optional[int] = None, diff --git a/workos/utils/_base_http_client.py b/workos/utils/_base_http_client.py index 06453c66..ff9c127f 100644 --- a/workos/utils/_base_http_client.py +++ b/workos/utils/_base_http_client.py @@ -55,14 +55,12 @@ def __init__( timeout: Optional[int] = DEFAULT_REQUEST_TIMEOUT, ) -> None: self._api_key = api_key - self.base_url = base_url + # Template for constructing the URL for an API request + self._base_url = "{}{{}}".format(base_url) self._client_id = client_id self._version = version self._timeout = DEFAULT_REQUEST_TIMEOUT if timeout is None else timeout - def _enforce_trailing_slash(self, url: str) -> str: - return url if url.endswith("/") else url + "/" - def _generate_api_url(self, path: str) -> str: return self._base_url.format(path) @@ -206,15 +204,6 @@ def api_key(self) -> str: def base_url(self) -> str: return self._base_url - @base_url.setter - def base_url(self, url: str) -> None: - """Creates an accessible template for constructing the URL for an API request. - - Args: - base_api_url (str): Base URL for api requests - """ - self._base_url = "{}{{}}".format(self._enforce_trailing_slash(url)) - @property def client_id(self) -> str: return self._client_id diff --git a/workos/utils/http_client.py b/workos/utils/http_client.py index 6da22677..bc8dd426 100644 --- a/workos/utils/http_client.py +++ b/workos/utils/http_client.py @@ -1,6 +1,8 @@ import asyncio from types import TracebackType from typing import Optional, Type, Union + +# Self was added to typing in Python 3.11 from typing_extensions import Self import httpx @@ -83,7 +85,7 @@ def request( params: ParamsType = None, json: JsonType = None, headers: HeadersType = None, - token: Optional[str] = None, + exclude_default_auth_headers: bool = False, ) -> ResponseJson: """Executes a request against the WorkOS API. @@ -94,13 +96,17 @@ def request( method (str): One of the supported methods as defined by the REQUEST_METHOD_X constants params (ParamsType): Query params to be added to the request json (JsonType): Body payload to be added to the request - token (str): Bearer token Returns: ResponseJson: Response from WorkOS """ prepared_request_parameters = self._prepare_request( - path=path, method=method, params=params, json=json, headers=headers + path=path, + method=method, + params=params, + json=json, + headers=headers, + exclude_default_auth_headers=exclude_default_auth_headers, ) response = self._client.request(**prepared_request_parameters) return self._handle_response(response) @@ -185,7 +191,6 @@ async def request( method (str): One of the supported methods as defined by the REQUEST_METHOD_X constants params (ParamsType): Query params to be added to the request json (JsonType): Body payload to be added to the request - token (str): Bearer token Returns: ResponseJson: Response from WorkOS