From 71fce53915c0002c8d93b3fd0e84eab7a37be571 Mon Sep 17 00:00:00 2001 From: mellowCaribou Date: Sun, 5 Oct 2025 10:47:37 +0300 Subject: [PATCH 1/4] Add async support to authentication methods Introduces async_mode and refactors authentication and client methods to support both synchronous and asynchronous usage. All relevant methods now return either direct results or awaitables, using futu_apply and futu_awaitable utilities. Updates all authentication method classes and DescopeClient to propagate async support, improving flexibility for async applications. --- descope/auth.py | 233 ++++++++++++++------- descope/authmethod/enchantedlink.py | 49 +++-- descope/authmethod/magiclink.py | 57 +++-- descope/authmethod/oauth.py | 36 +++- descope/authmethod/otp.py | 67 ++++-- descope/authmethod/password.py | 57 +++-- descope/authmethod/saml.py | 9 +- descope/authmethod/sso.py | 9 +- descope/authmethod/totp.py | 28 ++- descope/authmethod/webauthn.py | 63 +++--- descope/descope_client.py | 41 ++-- descope/future_utils.py | 38 ++++ descope/management/access_key.py | 35 ++-- descope/management/audit.py | 24 ++- descope/management/authz.py | 92 ++++---- descope/management/fga.py | 51 +++-- descope/management/flow.py | 45 ++-- descope/management/group.py | 24 ++- descope/management/jwt.py | 59 ++++-- descope/management/outbound_application.py | 128 ++++++----- descope/management/permission.py | 22 +- descope/management/project.py | 53 +++-- descope/management/role.py | 26 ++- descope/management/sso_application.py | 34 +-- descope/management/sso_settings.py | 46 ++-- descope/management/tenant.py | 29 +-- descope/management/user.py | 212 ++++++++++--------- tests/test_auth.py | 10 +- 28 files changed, 1000 insertions(+), 577 deletions(-) create mode 100644 descope/future_utils.py diff --git a/descope/auth.py b/descope/auth.py index d3b3ab883..c8a12e56e 100644 --- a/descope/auth.py +++ b/descope/auth.py @@ -8,11 +8,13 @@ from http import HTTPStatus import ssl from threading import Lock -from typing import Iterable import certifi +from typing import Awaitable, Iterable, Union import jwt +from descope.future_utils import futu_apply, futu_awaitable + try: from importlib.metadata import version except ImportError: @@ -22,6 +24,7 @@ from email_validator import EmailNotValidError, validate_email from jwt import ExpiredSignatureError, ImmatureSignatureError + from descope.common import ( COOKIE_DATA_NAME, DEFAULT_BASE_URL, @@ -76,6 +79,7 @@ def __init__( timeout_seconds: float = DEFAULT_TIMEOUT_SECONDS, jwt_validation_leeway: int = 5, auth_management_key: str | None = None, + async_mode: bool = False, fga_cache_url: str | None = None, ): self.lock_public_keys = Lock() @@ -93,6 +97,7 @@ def __init__( self.project_id = project_id self.jwt_validation_leeway = jwt_validation_leeway self.secure = not skip_verify + self.async_mode = async_mode self.base_url = os.getenv("DESCOPE_BASE_URI") if not self.base_url: @@ -112,18 +117,66 @@ def __init__( kid, pub_key, alg = self._validate_and_load_public_key(public_key) self.public_keys = {kid: (pub_key, alg)} - if skip_verify: - self.ssl_ctx = False - else: + self.client_timeout = timeout_seconds + self.client_verify: bool | ssl.SSLContext = False + if not skip_verify: # Backwards compatibility with requests - self.ssl_ctx = ssl.create_default_context( + ssl_ctx = ssl.create_default_context( cafile=os.environ.get("SSL_CERT_FILE", certifi.where()), capath=os.environ.get("SSL_CERT_DIR"), ) if os.environ.get("REQUESTS_CA_BUNDLE"): - self.ssl_ctx.load_cert_chain( - certfile=os.environ.get("REQUESTS_CA_BUNDLE") - ) + # ignore - is valid string + ssl_ctx.load_cert_chain(certfile=os.environ.get("REQUESTS_CA_BUNDLE")) # type: ignore[arg-type] + self.client_verify = ssl_ctx + # ignore - is valid string + + def _request( + self, method: str, url: str, **kwargs + ) -> Union[httpx.Response, Awaitable[httpx.Response]]: + kwargs = {**kwargs} + if self.async_mode: + return self._async_request(method, url, **kwargs) + else: + return self._sync_request(method, url, **kwargs) + + def _sync_request(self, method: str, url: str, **kwargs) -> httpx.Response: + req_kwargs = { + "verify": self.client_verify, + "timeout": self.client_timeout, + **kwargs, + } + method_lower = method.lower() + if method_lower == "get": + return httpx.get(url, **req_kwargs) + elif method_lower == "post": + return httpx.post(url, **req_kwargs) + elif method_lower == "patch": + return httpx.patch(url, **req_kwargs) + elif method_lower == "delete": + return httpx.delete(url, **req_kwargs) + elif method_lower == "put": + return httpx.put(url, **req_kwargs) + else: + return httpx.request(method, url, **req_kwargs) + + async def _async_request(self, method: str, url: str, **kwargs) -> httpx.Response: + async with httpx.AsyncClient( + verify=self.client_verify, timeout=self.client_timeout + ) as client: + method_lower = method.lower() + if method_lower == "get": + return await client.get(url, **kwargs) + elif method_lower == "post": + return await client.post(url, **kwargs) + elif method_lower == "patch": + return await client.patch(url, **kwargs) + elif method_lower == "delete": + return await client.delete(url, **kwargs) + elif method_lower == "put": + return await client.put(url, **kwargs) + else: + return await client.request(method, url, **kwargs) def _raise_rate_limit_exception(self, response): try: @@ -161,17 +214,16 @@ def do_get( params=None, follow_redirects=None, pswd: str | None = None, - ) -> httpx.Response: - response = httpx.get( + ) -> Union[httpx.Response, Awaitable[httpx.Response]]: + """Make GET request, returning Response or awaitable Response based on async_mode.""" + response = self._request( + "GET", f"{self.base_url}{uri}", headers=self._get_default_headers(pswd), params=params, follow_redirects=follow_redirects, - verify=self.ssl_ctx, - timeout=self.timeout_seconds, ) - self._raise_from_response(response) - return response + return futu_apply(response, self._raise_from_response_and_return) def do_post( self, @@ -179,18 +231,17 @@ def do_post( body: dict | list[dict] | list[str] | None, params=None, pswd: str | None = None, - ) -> httpx.Response: - response = httpx.post( + ) -> Union[httpx.Response, Awaitable[httpx.Response]]: + """Make POST request, returning Response or awaitable Response based on async_mode.""" + response = self._request( + "POST", f"{self.base_url}{uri}", headers=self._get_default_headers(pswd), json=body, - follow_redirects=False, - verify=self.ssl_ctx, params=params, - timeout=self.timeout_seconds, + follow_redirects=False, ) - self._raise_from_response(response) - return response + return futu_apply(response, self._raise_from_response_and_return) def do_patch( self, @@ -198,30 +249,35 @@ def do_patch( body: dict | list[dict] | list[str] | None, params=None, pswd: str | None = None, - ) -> httpx.Response: - response = httpx.patch( + ) -> Union[httpx.Response, Awaitable[httpx.Response]]: + """Make PATCH request, returning Response or awaitable Response based on async_mode.""" + response = self._request( + "PATCH", f"{self.base_url}{uri}", headers=self._get_default_headers(pswd), json=body, - follow_redirects=False, - verify=self.ssl_ctx, params=params, - timeout=self.timeout_seconds, + follow_redirects=False, ) - self._raise_from_response(response) - return response + return futu_apply(response, self._raise_from_response_and_return) def do_delete( self, uri: str, params=None, pswd: str | None = None - ) -> httpx.Response: - response = httpx.delete( + ) -> Union[httpx.Response, Awaitable[httpx.Response]]: + """Make DELETE request, returning Response or awaitable Response based on async_mode.""" + response = self._request( + "DELETE", f"{self.base_url}{uri}", params=params, headers=self._get_default_headers(pswd), follow_redirects=False, - verify=self.ssl_ctx, - timeout=self.timeout_seconds, ) + return futu_apply(response, self._raise_from_response_and_return) + + def _raise_from_response_and_return( + self, response: httpx.Response + ) -> httpx.Response: + """Helper method to raise exception if needed, then return response.""" self._raise_from_response(response) return response @@ -232,27 +288,25 @@ def do_post_with_custom_base_url( custom_base_url: str | None = None, params=None, pswd: str | None = None, - ) -> httpx.Response: + ) -> Union[httpx.Response, Awaitable[httpx.Response]]: """ - Post request with optional custom base URL. - If base_url is provided, use it instead of self.base_url. + Make POST request to a custom base URL when provided; otherwise use default base URL. + Returns Response or awaitable Response based on async_mode. """ effective_base_url = custom_base_url if custom_base_url else self.base_url - response = httpx.post( + response = self._request( + "POST", f"{effective_base_url}{uri}", headers=self._get_default_headers(pswd), json=body, - follow_redirects=False, - verify=self.ssl_ctx, params=params, - timeout=self.timeout_seconds, + follow_redirects=False, ) - self._raise_from_response(response) - return response + return futu_apply(response, self._raise_from_response_and_return) def exchange_token( self, uri, code: str, audience: str | None | Iterable[str] = None - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: if not code: raise AuthException( 400, @@ -262,11 +316,14 @@ def exchange_token( body = Auth._compose_exchange_body(code) response = self.do_post(uri=uri, body=body, params=None) - resp = response.json() - jwt_response = self.generate_jwt_response( - resp, response.cookies.get(REFRESH_SESSION_COOKIE_NAME), audience + return futu_apply( + response, + lambda response: self.generate_jwt_response( + response.json(), + response.cookies.get(REFRESH_SESSION_COOKIE_NAME), + audience, + ), ) - return jwt_response @staticmethod def base_url_for_project_id(project_id): @@ -406,15 +463,20 @@ def exchange_access_key( access_key: str, audience: str | Iterable[str] | None = None, login_options: AccessKeyLoginOptions | None = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: uri = EndpointsV1.exchange_auth_access_key_path body = { "loginOptions": login_options.__dict__ if login_options else {}, } server_response = self.do_post(uri=uri, body=body, params=None, pswd=access_key) - json = server_response.json() - return self._generate_auth_info( - response_body=json, refresh_token=None, user_jwt=False, audience=audience + return futu_apply( + server_response, + lambda response: self._generate_auth_info( + response_body=response.json(), + refresh_token=None, + user_jwt=False, + audience=audience, + ), ) @staticmethod @@ -466,7 +528,7 @@ def _validate_and_load_public_key(public_key) -> tuple[str, jwt.PyJWK, str]: ) def _raise_from_response(self, response: httpx.Response): - """Raise appropriate exception from response, does nothing if response.ok is True.""" + """Raise appropriate exception from response, does nothing if response.is_success is True.""" if response.is_success: return @@ -479,13 +541,12 @@ def _raise_from_response(self, response: httpx.Response): response.text, ) - def _fetch_public_keys(self) -> None: + def _fetch_public_keys_sync(self) -> None: # This function called under mutex protection so no need to acquire it once again - response = httpx.get( + response = self._sync_request( + "GET", f"{self.base_url}{EndpointsV2.public_key_path}/{self.project_id}", headers=self._get_default_headers(), - verify=self.ssl_ctx, - timeout=self.timeout_seconds, ) self._raise_from_response(response) @@ -569,14 +630,16 @@ def _generate_auth_info( jwt_response = {} st_jwt = response_body.get("sessionJwt", "") if st_jwt: - jwt_response[SESSION_TOKEN_NAME] = self._validate_token(st_jwt, audience) + jwt_response[SESSION_TOKEN_NAME] = self._validate_token_sync( + st_jwt, audience + ) rt_jwt = response_body.get("refreshJwt", "") if rt_jwt: - jwt_response[REFRESH_SESSION_TOKEN_NAME] = self._validate_token( + jwt_response[REFRESH_SESSION_TOKEN_NAME] = self._validate_token_sync( rt_jwt, audience ) elif refresh_token: - jwt_response[REFRESH_SESSION_TOKEN_NAME] = self._validate_token( + jwt_response[REFRESH_SESSION_TOKEN_NAME] = self._validate_token_sync( refresh_token, audience ) @@ -617,8 +680,8 @@ def _get_default_headers(self, pswd: str | None = None): headers["Authorization"] = f"Bearer {bearer}" return headers - # Validate a token and load the public key if needed - def _validate_token( + # Validate a token and load the public key if needed. + def _validate_token_sync( self, token: str, audience: str | None | Iterable[str] = None ) -> dict: if not token: @@ -650,7 +713,7 @@ def _validate_token( with self.lock_public_keys: if self.public_keys == {} or self.public_keys.get(kid, None) is None: - self._fetch_public_keys() + self._fetch_public_keys_sync() found_key = self.public_keys.get(kid, None) if found_key is None: @@ -697,7 +760,8 @@ def _validate_token( def validate_session( self, session_token: str, audience: str | None | Iterable[str] = None - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: + """Validate a session token, returning dict or awaitable dict based on async_mode.""" if not session_token: raise AuthException( 400, @@ -705,15 +769,16 @@ def validate_session( "Session token is required for validation", ) - res = self._validate_token(session_token, audience) + res = self._validate_token_sync(session_token, audience) res[SESSION_TOKEN_NAME] = copy.deepcopy( res ) # Duplicate for saving backward compatibility but keep the same structure as the refresh operation response - return self.adjust_properties(res, True) + return futu_awaitable(self.adjust_properties(res, True), self.async_mode) def refresh_session( self, refresh_token: str, audience: str | None | Iterable[str] = None - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: + """Refresh a session token, returning dict or awaitable dict based on async_mode.""" if not refresh_token: raise AuthException( 400, @@ -721,23 +786,28 @@ def refresh_session( "Refresh token is required to refresh a session", ) - self._validate_token(refresh_token, audience) - - uri = EndpointsV1.refresh_token_path - response = self.do_post(uri=uri, body={}, params=None, pswd=refresh_token) + self._validate_token_sync(refresh_token, audience) - resp = response.json() - refresh_token = ( - response.cookies.get(REFRESH_SESSION_COOKIE_NAME, None) or refresh_token + response = self.do_post( + uri=EndpointsV1.refresh_token_path, body={}, params=None, pswd=refresh_token ) - return self.generate_jwt_response(resp, refresh_token, audience) + + def process_response(resp_obj): + resp = resp_obj.json() + refresh_token_from_cookie = ( + resp_obj.cookies.get(REFRESH_SESSION_COOKIE_NAME, None) or refresh_token + ) + return self.generate_jwt_response(resp, refresh_token_from_cookie, audience) + + return futu_apply(response, process_response) def validate_and_refresh_session( self, session_token: str, refresh_token: str, audience: str | None | Iterable[str] = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: + """Validate session, refresh if needed, returning dict or awaitable dict based on async_mode.""" if not session_token: raise AuthException( 400, @@ -762,7 +832,8 @@ def select_tenant( tenant_id: str, refresh_token: str, audience: str | None | Iterable[str] = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: + """Select a tenant, returning dict or awaitable dict based on async_mode.""" if not refresh_token: raise AuthException( 400, @@ -775,11 +846,13 @@ def select_tenant( uri=uri, body={"tenant": tenant_id}, params=None, pswd=refresh_token ) - resp = response.json() - jwt_response = self.generate_jwt_response( - resp, response.cookies.get(REFRESH_SESSION_COOKIE_NAME, None), audience - ) - return jwt_response + def process_response(resp_obj): + resp = resp_obj.json() + return self.generate_jwt_response( + resp, resp_obj.cookies.get(REFRESH_SESSION_COOKIE_NAME, None), audience + ) + + return futu_apply(response, process_response) @staticmethod def extract_masked_address(response: dict, method: DeliveryMethod) -> str: diff --git a/descope/authmethod/enchantedlink.py b/descope/authmethod/enchantedlink.py index 179ac433c..8f1cb6bcd 100644 --- a/descope/authmethod/enchantedlink.py +++ b/descope/authmethod/enchantedlink.py @@ -1,6 +1,7 @@ from __future__ import annotations import httpx +from typing import Awaitable, Union from descope._auth_base import AuthBase from descope.auth import Auth @@ -14,6 +15,7 @@ validate_refresh_token_provided, ) from descope.exceptions import ERROR_TYPE_INVALID_ARGUMENT, AuthException +from descope.future_utils import futu_apply class EnchantedLink(AuthBase): @@ -23,7 +25,7 @@ def sign_in( uri: str, login_options: LoginOptions | None = None, refresh_token: str | None = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: if not login_id: raise AuthException( 400, @@ -37,7 +39,10 @@ def sign_in( uri = EnchantedLink._compose_signin_url() response = self._auth.do_post(uri, body, None, refresh_token) - return EnchantedLink._get_pending_ref_from_response(response) + return futu_apply( + response, + lambda response: response.json(), + ) def sign_up( self, @@ -45,7 +50,7 @@ def sign_up( uri: str, user: dict | None, signup_options: SignUpOptions | None = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: if not user: user = {} @@ -61,11 +66,14 @@ def sign_up( body = EnchantedLink._compose_signup_body(login_id, uri, user, signup_options) uri = EnchantedLink._compose_signup_url() response = self._auth.do_post(uri, body, None) - return EnchantedLink._get_pending_ref_from_response(response) + return futu_apply( + response, + lambda response: response.json(), + ) def sign_up_or_in( self, login_id: str, uri: str, signup_options: SignUpOptions | None = None - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: login_options: LoginOptions | None = None if signup_options is not None: login_options = LoginOptions( @@ -81,23 +89,29 @@ def sign_up_or_in( ) uri = EnchantedLink._compose_sign_up_or_in_url() response = self._auth.do_post(uri, body, None) - return EnchantedLink._get_pending_ref_from_response(response) + return futu_apply( + response, + lambda response: response.json(), + ) - def get_session(self, pending_ref: str) -> dict: + def get_session(self, pending_ref: str) -> Union[dict, Awaitable[dict]]: uri = EndpointsV1.get_session_enchantedlink_auth_path body = EnchantedLink._compose_get_session_body(pending_ref) response = self._auth.do_post(uri, body, None) - - resp = response.json() - jwt_response = self._auth.generate_jwt_response( - resp, response.cookies.get(REFRESH_SESSION_COOKIE_NAME, None), None + return futu_apply( + response, + lambda response: self._auth.generate_jwt_response( + response.json(), + response.cookies.get(REFRESH_SESSION_COOKIE_NAME, None), + None, + ), ) - return jwt_response - def verify(self, token: str): + def verify(self, token: str) -> Union[None, Awaitable[None]]: uri = EndpointsV1.verify_enchantedlink_auth_path body = EnchantedLink._compose_verify_body(token) - self._auth.do_post(uri, body, None) + response = self._auth.do_post(uri, body, None) + return futu_apply(response, lambda response: None) def update_user_email( self, @@ -109,7 +123,7 @@ def update_user_email( template_options: dict | None = None, template_id: str | None = None, provider_id: str | None = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: if not login_id: raise AuthException( 400, ERROR_TYPE_INVALID_ARGUMENT, "Identifier cannot be empty" @@ -128,7 +142,10 @@ def update_user_email( ) uri = EndpointsV1.update_user_email_enchantedlink_path response = self._auth.do_post(uri, body, None, refresh_token) - return EnchantedLink._get_pending_ref_from_response(response) + return futu_apply( + response, + lambda response: response.json(), + ) @staticmethod def _compose_signin_url() -> str: diff --git a/descope/authmethod/magiclink.py b/descope/authmethod/magiclink.py index 4182e5484..15f2238cd 100644 --- a/descope/authmethod/magiclink.py +++ b/descope/authmethod/magiclink.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Iterable +from typing import Awaitable, Iterable, Union from descope._auth_base import AuthBase from descope.auth import Auth @@ -14,6 +14,7 @@ validate_refresh_token_provided, ) from descope.exceptions import ERROR_TYPE_INVALID_ARGUMENT, AuthException +from descope.future_utils import futu_apply class MagicLink(AuthBase): @@ -24,7 +25,7 @@ def sign_in( uri: str, login_options: LoginOptions | None = None, refresh_token: str | None = None, - ) -> str: + ) -> Union[str, Awaitable[str]]: if not login_id: raise AuthException( 400, @@ -38,7 +39,10 @@ def sign_in( uri = MagicLink._compose_signin_url(method) response = self._auth.do_post(uri, body, None, refresh_token) - return Auth.extract_masked_address(response.json(), method) + return futu_apply( + response, + lambda response: Auth.extract_masked_address(response.json(), method), + ) def sign_up( self, @@ -47,7 +51,7 @@ def sign_up( uri: str, user: dict | None = None, signup_options: SignUpOptions | None = None, - ) -> str: + ) -> Union[str, Awaitable[str]]: if not user: user = {} @@ -63,7 +67,10 @@ def sign_up( ) uri = MagicLink._compose_signup_url(method) response = self._auth.do_post(uri, body, None) - return Auth.extract_masked_address(response.json(), method) + return futu_apply( + response, + lambda response: Auth.extract_masked_address(response.json(), method), + ) def sign_up_or_in( self, @@ -71,7 +78,7 @@ def sign_up_or_in( login_id: str, uri: str, signup_options: SignUpOptions | None = None, - ) -> str: + ) -> Union[str, Awaitable[str]]: login_options: LoginOptions | None = None if signup_options is not None: login_options = LoginOptions( @@ -86,17 +93,25 @@ def sign_up_or_in( ) uri = MagicLink._compose_sign_up_or_in_url(method) response = self._auth.do_post(uri, body, None) - return Auth.extract_masked_address(response.json(), method) + return futu_apply( + response, + lambda response: Auth.extract_masked_address(response.json(), method), + ) - def verify(self, token: str, audience: str | None | Iterable[str] = None) -> dict: + def verify( + self, token: str, audience: str | None | Iterable[str] = None + ) -> Union[dict, Awaitable[dict]]: uri = EndpointsV1.verify_magiclink_auth_path body = MagicLink._compose_verify_body(token) response = self._auth.do_post(uri, body, None) - resp = response.json() - jwt_response = self._auth.generate_jwt_response( - resp, response.cookies.get(REFRESH_SESSION_COOKIE_NAME, None), audience + return futu_apply( + response, + lambda response: self._auth.generate_jwt_response( + response.json(), + response.cookies.get(REFRESH_SESSION_COOKIE_NAME, None), + audience, + ), ) - return jwt_response def update_user_email( self, @@ -108,7 +123,7 @@ def update_user_email( template_options: dict | None = None, template_id: str | None = None, provider_id: str | None = None, - ) -> str: + ) -> Union[str, Awaitable[str]]: if not login_id: raise AuthException( 400, ERROR_TYPE_INVALID_ARGUMENT, "Identifier cannot be empty" @@ -127,7 +142,12 @@ def update_user_email( ) uri = EndpointsV1.update_user_email_magiclink_path response = self._auth.do_post(uri, body, None, refresh_token) - return Auth.extract_masked_address(response.json(), DeliveryMethod.EMAIL) + return futu_apply( + response, + lambda response: Auth.extract_masked_address( + response.json(), DeliveryMethod.EMAIL + ), + ) def update_user_phone( self, @@ -140,7 +160,7 @@ def update_user_phone( template_options: dict | None = None, template_id: str | None = None, provider_id: str | None = None, - ) -> str: + ) -> Union[str, Awaitable[str]]: if not login_id: raise AuthException( 400, ERROR_TYPE_INVALID_ARGUMENT, "Identifier cannot be empty" @@ -159,7 +179,12 @@ def update_user_phone( ) uri = EndpointsV1.update_user_phone_magiclink_path response = self._auth.do_post(uri, body, None, refresh_token) - return Auth.extract_masked_address(response.json(), DeliveryMethod.SMS) + return futu_apply( + response, + lambda response: Auth.extract_masked_address( + response.json(), DeliveryMethod.SMS + ), + ) @staticmethod def _compose_signin_url(method: DeliveryMethod) -> str: diff --git a/descope/authmethod/oauth.py b/descope/authmethod/oauth.py index 55278bdfa..fb5f9f007 100644 --- a/descope/authmethod/oauth.py +++ b/descope/authmethod/oauth.py @@ -1,8 +1,14 @@ -from typing import Optional +from typing import Awaitable, Optional, Union from descope._auth_base import AuthBase -from descope.common import EndpointsV1, LoginOptions, validate_refresh_token_provided +from descope.common import ( + EndpointsV1, + LoginOptions, + REFRESH_SESSION_COOKIE_NAME, + validate_refresh_token_provided, +) from descope.exceptions import ERROR_TYPE_INVALID_ARGUMENT, AuthException +from descope.future_utils import futu_apply class OAuth(AuthBase): @@ -12,7 +18,7 @@ def start( return_url: str = "", login_options: Optional[LoginOptions] = None, refresh_token: Optional[str] = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ """ if not self._verify_provider(provider): raise AuthException( @@ -29,11 +35,29 @@ def start( uri, login_options.__dict__ if login_options else {}, params, refresh_token ) - return response.json() + return futu_apply( + response, + lambda response: response.json(), + ) + + def exchange_token(self, code: str) -> Union[dict, Awaitable[dict]]: + if not code: + raise AuthException( + 400, + ERROR_TYPE_INVALID_ARGUMENT, + "exchange code is empty", + ) - def exchange_token(self, code: str) -> dict: uri = EndpointsV1.oauth_exchange_token_path - return self._auth.exchange_token(uri, code) + response = self._auth.do_post(uri, {"code": code}, None) + return futu_apply( + response, + lambda response: self._auth.generate_jwt_response( + response.json(), + response.cookies.get(REFRESH_SESSION_COOKIE_NAME, None), + None, + ), + ) @staticmethod def _verify_provider(oauth_provider: str) -> bool: diff --git a/descope/authmethod/otp.py b/descope/authmethod/otp.py index 86cc3c10d..fafe930d7 100644 --- a/descope/authmethod/otp.py +++ b/descope/authmethod/otp.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Iterable +from typing import Awaitable, Iterable, Union from descope._auth_base import AuthBase from descope.auth import Auth @@ -14,6 +14,7 @@ validate_refresh_token_provided, ) from descope.exceptions import ERROR_TYPE_INVALID_ARGUMENT, AuthException +from descope.future_utils import futu_apply class OTP(AuthBase): @@ -23,7 +24,7 @@ def sign_in( login_id: str, login_options: LoginOptions | None = None, refresh_token: str | None = None, - ) -> str: + ) -> Union[str, Awaitable[str]]: """ Sign in (log in) an existing user with the unique login_id you provide. (See 'sign_up' function for an explanation of the login_id field.) Provide the DeliveryMethod required for this user. If the login_id value cannot be used for the @@ -36,6 +37,9 @@ def sign_in( login_options (LoginOptions): Optional advanced controls over login parameters refresh_token: Optional refresh token is needed for specific login options + Returns: + str or Awaitable[str]: Masked address for OTP delivery + Raise: AuthException: raised if sign-in operation fails """ @@ -49,7 +53,10 @@ def sign_in( uri = OTP._compose_signin_url(method) body = OTP._compose_signin_body(login_id, login_options) response = self._auth.do_post(uri, body, None, refresh_token) - return Auth.extract_masked_address(response.json(), method) + return futu_apply( + response, + lambda response: Auth.extract_masked_address(response.json(), method), + ) def sign_up( self, @@ -57,7 +64,7 @@ def sign_up( login_id: str, user: dict | None = None, signup_options: SignUpOptions | None = None, - ) -> str: + ) -> Union[str, Awaitable[str]]: """ Sign up (create) a new user using their email or phone number. Choose a delivery method for OTP verification, for example Email, SMS, Voice call, or WhatsApp. @@ -69,6 +76,9 @@ def sign_up( user (dict) optional: Preserve additional user metadata in the form of {"name": "Joe Person", "phone": "2125551212", "email": "joe@somecompany.com"} + Returns: + str or Awaitable[str]: Masked address for OTP delivery + Raise: AuthException: raised if sign-up operation fails """ @@ -86,14 +96,17 @@ def sign_up( uri = OTP._compose_signup_url(method) body = OTP._compose_signup_body(method, login_id, user, signup_options) response = self._auth.do_post(uri, body) - return Auth.extract_masked_address(response.json(), method) + return futu_apply( + response, + lambda response: Auth.extract_masked_address(response.json(), method), + ) def sign_up_or_in( self, method: DeliveryMethod, login_id: str, signup_options: SignUpOptions | None = None, - ) -> str: + ) -> Union[str, Awaitable[str]]: """ Sign_up_or_in lets you handle both sign up and sign in with a single call. Sign-up_or_in will first determine if login_id is a new or existing end user. If login_id is new, a new end user user will be created and then @@ -125,7 +138,10 @@ def sign_up_or_in( login_options, ) response = self._auth.do_post(uri, body) - return Auth.extract_masked_address(response.json(), method) + return futu_apply( + response, + lambda response: Auth.extract_masked_address(response.json(), method), + ) def verify_code( self, @@ -133,7 +149,7 @@ def verify_code( login_id: str, code: str, audience: str | None | Iterable[str] = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Verify the validity of an OTP code entered by an end user during sign_in or sign_up. (This function is not needed if you are using the sign_up_or_in function. @@ -142,11 +158,10 @@ def verify_code( method (DeliveryMethod): The method to use for delivering the OTP verification code, for example Email, SMS, Voice call, or WhatsApp login_id (str): The Login ID of the user being validated code (str): The authorization code enter by the end user during signup/signin + audience (str|Iterable[str]|None): Optional recipients that the JWT is intended for - Return value (dict): - Return dict in the format - {"jwts": [], "user": "", "firstSeen": "", "error": ""} - Includes all the jwts tokens (session token, refresh token), token claims, and user information + Returns: + dict or Awaitable[dict]: Authentication response with JWT tokens and user information Raise: AuthException: raised if the OTP code is not valid or if token verification failed @@ -159,12 +174,14 @@ def verify_code( uri = OTP._compose_verify_code_url(method) body = OTP._compose_verify_code_body(login_id, code) response = self._auth.do_post(uri, body, None) - - resp = response.json() - jwt_response = self._auth.generate_jwt_response( - resp, response.cookies.get(REFRESH_SESSION_COOKIE_NAME, None), audience + return futu_apply( + response, + lambda response: self._auth.generate_jwt_response( + response.json(), + response.cookies.get(REFRESH_SESSION_COOKIE_NAME, None), + audience, + ), ) - return jwt_response def update_user_email( self, @@ -176,7 +193,7 @@ def update_user_email( template_options: dict | None = None, template_id: str | None = None, provider_id: str | None = None, - ) -> str: + ) -> Union[str, Awaitable[str]]: """ Update the email address of an end user, after verifying the authenticity of the end user using OTP. @@ -207,7 +224,12 @@ def update_user_email( provider_id, ) response = self._auth.do_post(uri, body, None, refresh_token) - return Auth.extract_masked_address(response.json(), DeliveryMethod.EMAIL) + return futu_apply( + response, + lambda response: Auth.extract_masked_address( + response.json(), DeliveryMethod.EMAIL + ), + ) def update_user_phone( self, @@ -220,7 +242,7 @@ def update_user_phone( template_options: dict | None = None, template_id: str | None = None, provider_id: str | None = None, - ) -> str: + ) -> Union[str, Awaitable[str]]: """ Update the phone number of an existing end user, after verifying the authenticity of the end user using OTP. @@ -255,7 +277,10 @@ def update_user_phone( provider_id, ) response = self._auth.do_post(uri, body, None, refresh_token) - return Auth.extract_masked_address(response.json(), method) + return futu_apply( + response, + lambda response: Auth.extract_masked_address(response.json(), method), + ) @staticmethod def _compose_signup_url(method: DeliveryMethod) -> str: diff --git a/descope/authmethod/password.py b/descope/authmethod/password.py index 3d192ef35..d12f74697 100644 --- a/descope/authmethod/password.py +++ b/descope/authmethod/password.py @@ -1,10 +1,11 @@ from __future__ import annotations -from typing import Iterable +from typing import Awaitable, Iterable, Union from descope._auth_base import AuthBase from descope.common import REFRESH_SESSION_COOKIE_NAME, EndpointsV1 from descope.exceptions import ERROR_TYPE_INVALID_ARGUMENT, AuthException +from descope.future_utils import futu_apply class Password(AuthBase): @@ -14,7 +15,7 @@ def sign_up( password: str, user: dict | None = None, audience: str | None | Iterable[str] = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Sign up (create) a new user using a login ID and password. (optional) Include additional user metadata that you wish to save. @@ -48,18 +49,21 @@ def sign_up( body = Password._compose_signup_body(login_id, password, user) response = self._auth.do_post(uri, body) - resp = response.json() - jwt_response = self._auth.generate_jwt_response( - resp, response.cookies.get(REFRESH_SESSION_COOKIE_NAME, None), audience + return futu_apply( + response, + lambda response: self._auth.generate_jwt_response( + response.json(), + response.cookies.get(REFRESH_SESSION_COOKIE_NAME, None), + audience, + ), ) - return jwt_response def sign_in( self, login_id: str, password: str, audience: str | None | Iterable[str] = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Sign in by verifying the validity of a password entered by an end user. @@ -89,18 +93,21 @@ def sign_in( uri = EndpointsV1.sign_in_password_path response = self._auth.do_post(uri, {"loginId": login_id, "password": password}) - resp = response.json() - jwt_response = self._auth.generate_jwt_response( - resp, response.cookies.get(REFRESH_SESSION_COOKIE_NAME, None), audience + return futu_apply( + response, + lambda response: self._auth.generate_jwt_response( + response.json(), + response.cookies.get(REFRESH_SESSION_COOKIE_NAME, None), + audience, + ), ) - return jwt_response def send_reset( self, login_id: str, redirect_url: str | None = None, template_options: dict | None = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Sends a password reset prompt to the user with the given login_id according to the password settings defined in the Descope console. @@ -140,9 +147,11 @@ def send_reset( body, ) - return response.json() + return futu_apply(response, lambda response: response.json()) - def update(self, login_id: str, new_password: str, refresh_token: str) -> None: + def update( + self, login_id: str, new_password: str, refresh_token: str + ) -> Union[None, Awaitable[None]]: """ Update a password for an existing logged in user using their refresh token. @@ -171,9 +180,10 @@ def update(self, login_id: str, new_password: str, refresh_token: str) -> None: ) uri = EndpointsV1.update_password_path - self._auth.do_post( + response = self._auth.do_post( uri, {"loginId": login_id, "newPassword": new_password}, None, refresh_token ) + return futu_apply(response, lambda response: None) def replace( self, @@ -181,7 +191,7 @@ def replace( old_password: str, new_password: str, audience: str | None | Iterable[str] = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Replace a valid active password with a new one. The old_password is used to authenticate the user. If the user cannot be authenticated, this operation @@ -226,13 +236,16 @@ def replace( }, ) - resp = response.json() - jwt_response = self._auth.generate_jwt_response( - resp, response.cookies.get(REFRESH_SESSION_COOKIE_NAME, None), audience + return futu_apply( + response, + lambda response: self._auth.generate_jwt_response( + response.json(), + response.cookies.get(REFRESH_SESSION_COOKIE_NAME, None), + audience, + ), ) - return jwt_response - def get_policy(self) -> dict: + def get_policy(self) -> Union[dict, Awaitable[dict]]: """ Get a subset of the password policy defined in the Descope console and enforced by Descope. The goal is to enable client-side validations to give users a better UX @@ -251,7 +264,7 @@ def get_policy(self) -> dict: """ response = self._auth.do_get(uri=EndpointsV1.password_policy_path) - return response.json() + return futu_apply(response, lambda response: response.json()) @staticmethod def _compose_signup_body(login_id: str, password: str, user: dict | None) -> dict: diff --git a/descope/authmethod/saml.py b/descope/authmethod/saml.py index b9b8e4480..8b8b309c2 100644 --- a/descope/authmethod/saml.py +++ b/descope/authmethod/saml.py @@ -1,8 +1,9 @@ -from typing import Optional +from typing import Awaitable, Optional, Union from descope._auth_base import AuthBase from descope.common import EndpointsV1, LoginOptions, validate_refresh_token_provided from descope.exceptions import ERROR_TYPE_INVALID_ARGUMENT, AuthException +from descope.future_utils import futu_apply # This class is DEPRECATED please use SSO instead @@ -13,7 +14,7 @@ def start( return_url: Optional[str] = None, login_options: Optional[LoginOptions] = None, refresh_token: Optional[str] = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ DEPRECATED """ @@ -35,9 +36,9 @@ def start( uri, login_options.__dict__ if login_options else {}, params, refresh_token ) - return response.json() + return futu_apply(response, lambda response: response.json()) - def exchange_token(self, code: str) -> dict: + def exchange_token(self, code: str) -> Union[dict, Awaitable[dict]]: uri = EndpointsV1.saml_exchange_token_path return self._auth.exchange_token(uri, code) diff --git a/descope/authmethod/sso.py b/descope/authmethod/sso.py index 7d21f23d4..9d3e4fd21 100644 --- a/descope/authmethod/sso.py +++ b/descope/authmethod/sso.py @@ -1,8 +1,9 @@ -from typing import Optional +from typing import Awaitable, Optional, Union from descope._auth_base import AuthBase from descope.common import EndpointsV1, LoginOptions, validate_refresh_token_provided from descope.exceptions import ERROR_TYPE_INVALID_ARGUMENT, AuthException +from descope.future_utils import futu_apply class SSO(AuthBase): @@ -14,7 +15,7 @@ def start( refresh_token: Optional[str] = None, prompt: Optional[str] = None, sso_id: Optional[str] = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Start tenant sso session (saml/oidc based on tenant settings) @@ -40,9 +41,9 @@ def start( uri, login_options.__dict__ if login_options else {}, params, refresh_token ) - return response.json() + return futu_apply(response, lambda response: response.json()) - def exchange_token(self, code: str) -> dict: + def exchange_token(self, code: str) -> Union[dict, Awaitable[dict]]: uri = EndpointsV1.sso_exchange_token_path return self._auth.exchange_token(uri, code) diff --git a/descope/authmethod/totp.py b/descope/authmethod/totp.py index 11a490bd4..8ff35e616 100644 --- a/descope/authmethod/totp.py +++ b/descope/authmethod/totp.py @@ -1,4 +1,4 @@ -from typing import Iterable, Optional, Union +from typing import Iterable, Optional, Union, Awaitable from descope._auth_base import AuthBase from descope.common import ( @@ -8,10 +8,13 @@ validate_refresh_token_provided, ) from descope.exceptions import ERROR_TYPE_INVALID_ARGUMENT, AuthException +from descope.future_utils import futu_apply class TOTP(AuthBase): - def sign_up(self, login_id: str, user: Optional[dict] = None) -> dict: + def sign_up( + self, login_id: str, user: Optional[dict] = None + ) -> Union[dict, Awaitable[dict]]: """ Sign up (create) a new user using their email or phone number. (optional) Include additional user metadata that you wish to save. @@ -41,7 +44,7 @@ def sign_up(self, login_id: str, user: Optional[dict] = None) -> dict: body = TOTP._compose_signup_body(login_id, user) response = self._auth.do_post(uri, body) - return response.json() + return futu_apply(response, lambda response: response.json()) def sign_in_code( self, @@ -50,7 +53,7 @@ def sign_in_code( login_options: Optional[LoginOptions] = None, refresh_token: Optional[str] = None, audience: Union[str, None, Iterable[str]] = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Sign in by verifying the validity of a TOTP code entered by an end user. @@ -85,13 +88,18 @@ def sign_in_code( body = TOTP._compose_signin_body(login_id, code, login_options) response = self._auth.do_post(uri, body, None, refresh_token) - resp = response.json() - jwt_response = self._auth.generate_jwt_response( - resp, response.cookies.get(REFRESH_SESSION_COOKIE_NAME, None), audience + return futu_apply( + response, + lambda response: self._auth.generate_jwt_response( + response.json(), + response.cookies.get(REFRESH_SESSION_COOKIE_NAME, None), + audience, + ), ) - return jwt_response - def update_user(self, login_id: str, refresh_token: str) -> None: + def update_user( + self, login_id: str, refresh_token: str + ) -> Union[dict, Awaitable[dict]]: """ Add TOTP to an existing logged in user using their refresh token. @@ -124,7 +132,7 @@ def update_user(self, login_id: str, refresh_token: str) -> None: body = TOTP._compose_update_user_body(login_id) response = self._auth.do_post(uri, body, None, refresh_token) - return response.json() + return futu_apply(response, lambda response: response.json()) @staticmethod def _compose_signup_body(login_id: str, user: Optional[dict]) -> dict: diff --git a/descope/authmethod/webauthn.py b/descope/authmethod/webauthn.py index df72362e1..eca082826 100644 --- a/descope/authmethod/webauthn.py +++ b/descope/authmethod/webauthn.py @@ -1,4 +1,4 @@ -from typing import Iterable, Optional, Union +from typing import Awaitable, Iterable, Optional, Union from httpx import Response @@ -10,6 +10,7 @@ validate_refresh_token_provided, ) from descope.exceptions import ERROR_TYPE_INVALID_ARGUMENT, AuthException +from descope.future_utils import futu_apply class WebAuthn(AuthBase): @@ -18,7 +19,7 @@ def sign_up_start( login_id: Optional[str], origin: Optional[str], user: Optional[dict] = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Docs """ @@ -39,14 +40,14 @@ def sign_up_start( body = WebAuthn._compose_sign_up_start_body(login_id, user, origin) response = self._auth.do_post(uri, body) - return response.json() + return futu_apply(response, lambda response: response.json()) def sign_up_finish( self, transaction_id: str, response: Response, audience: Union[str, None, Iterable[str]] = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Docs """ @@ -62,13 +63,16 @@ def sign_up_finish( uri = EndpointsV1.sign_up_auth_webauthn_finish_path body = WebAuthn._compose_sign_up_in_finish_body(transaction_id, response) - response = self._auth.do_post(uri, body, None, "") - - resp = response.json() - jwt_response = self._auth.generate_jwt_response( - resp, response.cookies.get(REFRESH_SESSION_COOKIE_NAME, None), audience + res = self._auth.do_post(uri, body, None, "") + + return futu_apply( + res, + lambda res: self._auth.generate_jwt_response( + res.json(), + res.cookies.get(REFRESH_SESSION_COOKIE_NAME, None), + audience, + ), ) - return jwt_response def sign_in_start( self, @@ -76,7 +80,7 @@ def sign_in_start( origin: str, login_options: Optional[LoginOptions] = None, refresh_token: Optional[str] = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Docs """ @@ -96,14 +100,14 @@ def sign_in_start( body = WebAuthn._compose_sign_in_start_body(login_id, origin, login_options) response = self._auth.do_post(uri, body, pswd=refresh_token) - return response.json() + return futu_apply(response, lambda response: response.json()) def sign_in_finish( self, transaction_id: str, response: Response, audience: Union[str, None, Iterable[str]] = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Docs """ @@ -119,19 +123,22 @@ def sign_in_finish( uri = EndpointsV1.sign_in_auth_webauthn_finish_path body = WebAuthn._compose_sign_up_in_finish_body(transaction_id, response) - response = self._auth.do_post(uri, body, None) - - resp = response.json() - jwt_response = self._auth.generate_jwt_response( - resp, response.cookies.get(REFRESH_SESSION_COOKIE_NAME, None), audience + res = self._auth.do_post(uri, body, None) + + return futu_apply( + res, + lambda res: self._auth.generate_jwt_response( + res.json(), + res.cookies.get(REFRESH_SESSION_COOKIE_NAME, None), + audience, + ), ) - return jwt_response def sign_up_or_in_start( self, login_id: str, origin: str, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Docs """ @@ -149,9 +156,11 @@ def sign_up_or_in_start( body = WebAuthn._compose_sign_up_or_in_start_body(login_id, origin) response = self._auth.do_post(uri, body) - return response.json() + return futu_apply(response, lambda response: response.json()) - def update_start(self, login_id: str, refresh_token: str, origin: str): + def update_start( + self, login_id: str, refresh_token: str, origin: str + ) -> Union[dict, Awaitable[dict]]: """ Docs """ @@ -169,9 +178,11 @@ def update_start(self, login_id: str, refresh_token: str, origin: str): body = WebAuthn._compose_update_start_body(login_id, origin) response = self._auth.do_post(uri, body, None, refresh_token) - return response.json() + return futu_apply(response, lambda response: response.json()) - def update_finish(self, transaction_id: str, response: str) -> None: + def update_finish( + self, transaction_id: str, response: str + ) -> Union[None, Awaitable[None]]: """ Docs """ @@ -187,7 +198,9 @@ def update_finish(self, transaction_id: str, response: str) -> None: uri = EndpointsV1.update_auth_webauthn_finish_path body = WebAuthn._compose_update_finish_body(transaction_id, response) - self._auth.do_post(uri, body) + res = self._auth.do_post(uri, body) + + return futu_apply(res, lambda res: None) @staticmethod def _compose_sign_up_start_body(login_id: str, user: dict, origin: str) -> dict: diff --git a/descope/descope_client.py b/descope/descope_client.py index 2439717ac..c55e5e9a6 100644 --- a/descope/descope_client.py +++ b/descope/descope_client.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Iterable +from typing import Awaitable, Iterable, Union import httpx @@ -16,6 +16,7 @@ from descope.authmethod.webauthn import WebAuthn # noqa: F401 from descope.common import DEFAULT_TIMEOUT_SECONDS, AccessKeyLoginOptions, EndpointsV1 from descope.exceptions import ERROR_TYPE_INVALID_ARGUMENT, AuthException +from descope.future_utils import futu_apply from descope.mgmt import MGMT # noqa: F401 @@ -31,6 +32,7 @@ def __init__( timeout_seconds: float = DEFAULT_TIMEOUT_SECONDS, jwt_validation_leeway: int = 5, auth_management_key: str | None = None, + async_mode: bool = False, fga_cache_url: str | None = None, ): auth = Auth( @@ -41,6 +43,7 @@ def __init__( timeout_seconds, jwt_validation_leeway, auth_management_key, + async_mode, fga_cache_url, ) self._auth = auth @@ -296,7 +299,7 @@ def get_matched_tenant_roles( def validate_session( self, session_token: str, audience: str | Iterable[str] | None = None - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Validate a session token. Call this function for every incoming request to your private endpoints. Alternatively, use validate_and_refresh_session in order to @@ -319,7 +322,7 @@ def validate_session( def refresh_session( self, refresh_token: str, audience: str | Iterable[str] | None = None - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Refresh a session. Call this function when a session expires and needs to be refreshed. @@ -340,7 +343,7 @@ def validate_and_refresh_session( session_token: str, refresh_token: str, audience: str | Iterable[str] | None = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Validate the session token and refresh it if it has expired, the session token will automatically be refreshed. Either the session_token or the refresh_token must be provided. @@ -362,7 +365,9 @@ def validate_and_refresh_session( session_token, refresh_token, audience ) - def logout(self, refresh_token: str) -> httpx.Response: + def logout( + self, refresh_token: str + ) -> Union[httpx.Response, Awaitable[httpx.Response]]: """ Logout user from current session and revoke the refresh_token. After calling this function, you must invalidate or remove any cookies you have created. @@ -383,9 +388,12 @@ def logout(self, refresh_token: str) -> httpx.Response: ) uri = EndpointsV1.logout_path - return self._auth.do_post(uri, {}, None, refresh_token) + response = self._auth.do_post(uri, {}, None, refresh_token) + return futu_apply(response, lambda response: response) - def logout_all(self, refresh_token: str) -> httpx.Response: + def logout_all( + self, refresh_token: str + ) -> Union[httpx.Response, Awaitable[httpx.Response]]: """ Logout user from all active sessions and revoke the refresh_token. After calling this function, you must invalidate or remove any cookies you have created. @@ -406,9 +414,10 @@ def logout_all(self, refresh_token: str) -> httpx.Response: ) uri = EndpointsV1.logout_all_path - return self._auth.do_post(uri, {}, None, refresh_token) + response = self._auth.do_post(uri, {}, None, refresh_token) + return futu_apply(response, lambda response: response) - def me(self, refresh_token: str) -> dict: + def me(self, refresh_token: str) -> Union[dict, Awaitable[dict]]: """ Retrieve user details for the refresh token. The returned data includes email, name, phone, list of loginIds and boolean flags for verifiedEmail, verifiedPhone. @@ -433,14 +442,14 @@ def me(self, refresh_token: str) -> dict: response = self._auth.do_get( uri=uri, params=None, follow_redirects=None, pswd=refresh_token ) - return response.json() + return futu_apply(response, lambda response: response.json()) def my_tenants( self, refresh_token: str, dct: bool = False, ids: list[str] | None = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Retrieve tenant attributes that user belongs to, one of dct/ids must be populated . @@ -480,9 +489,9 @@ def my_tenants( uri = EndpointsV1.my_tenants_path response = self._auth.do_post(uri, body, None, refresh_token) - return response.json() + return futu_apply(response, lambda response: response.json()) - def history(self, refresh_token: str) -> list[dict]: + def history(self, refresh_token: str) -> Union[list[dict], Awaitable[list[dict]]]: """ Retrieve user authentication history for the refresh token @@ -515,14 +524,14 @@ def history(self, refresh_token: str) -> list[dict]: response = self._auth.do_get( uri=uri, params=None, follow_redirects=None, pswd=refresh_token ) - return response.json() + return futu_apply(response, lambda response: response.json()) def exchange_access_key( self, access_key: str, audience: str | Iterable[str] | None = None, login_options: AccessKeyLoginOptions | None = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Return a new session token for the given access key @@ -547,7 +556,7 @@ def select_tenant( self, tenant_id: str, refresh_token: str, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Add to JWT a selected tenant claim diff --git a/descope/future_utils.py b/descope/future_utils.py new file mode 100644 index 000000000..404629265 --- /dev/null +++ b/descope/future_utils.py @@ -0,0 +1,38 @@ +from __future__ import annotations + +import asyncio +from typing import Any, Awaitable, Callable, TypeVar, Union + +T = TypeVar("T") + + +def futu_apply( + result_or_coro: Union[T, Awaitable[T]], modifier: Callable[[T], Any] +) -> Union[Any, Awaitable[Any]]: + if asyncio.iscoroutine(result_or_coro) or asyncio.isfuture(result_or_coro): + + async def process_async(): + result = await result_or_coro + return modifier(result) + + return process_async() + else: + # we ignore arg-type due to the check above. + return modifier(result_or_coro) # type: ignore[arg-type] + + +def futu_awaitable(result: T, as_awaitable: bool) -> Union[Any, Awaitable[Any]]: + if as_awaitable: + + async def awaitable_wrapper(): + return result + + return awaitable_wrapper() + + return result + + +async def futu_await(obj: Union[Any, Awaitable[Any]]) -> Any: + if asyncio.iscoroutine(obj) or asyncio.isfuture(obj): + return await obj + return obj diff --git a/descope/management/access_key.py b/descope/management/access_key.py index e51c65463..e82edbbcf 100644 --- a/descope/management/access_key.py +++ b/descope/management/access_key.py @@ -1,6 +1,7 @@ -from typing import List, Optional +from typing import Awaitable, List, Optional, Union from descope._auth_base import AuthBase +from descope.future_utils import futu_apply from descope.management.common import ( AssociatedTenant, MgmtV1, @@ -19,7 +20,7 @@ def create( custom_claims: Optional[dict] = None, description: Optional[str] = None, permitted_ips: Optional[List[str]] = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Create a new access key. @@ -65,12 +66,12 @@ def create( ), pswd=self._auth.management_key, ) - return response.json() + return futu_apply(response, lambda response: response.json()) def load( self, id: str, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Load an existing access key. @@ -90,12 +91,12 @@ def load( params={"id": id}, pswd=self._auth.management_key, ) - return response.json() + return futu_apply(response, lambda response: response.json()) def search_all_access_keys( self, tenant_ids: Optional[List[str]] = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Search all access keys. @@ -117,14 +118,14 @@ def search_all_access_keys( {"tenantIds": tenant_ids}, pswd=self._auth.management_key, ) - return response.json() + return futu_apply(response, lambda response: response.json()) def update( self, id: str, name: str, description: Optional[str] = None, - ): + ) -> Union[None, Awaitable[None]]: """ Update an existing access key with the given various fields. IMPORTANT: id and name are mandatory fields. @@ -136,16 +137,17 @@ def update( Raise: AuthException: raised if update operation fails """ - self._auth.do_post( + response = self._auth.do_post( MgmtV1.access_key_update_path, {"id": id, "name": name, "description": description}, pswd=self._auth.management_key, ) + return futu_apply(response, lambda response: None) def deactivate( self, id: str, - ): + ) -> Union[None, Awaitable[None]]: """ Deactivate an existing access key. IMPORTANT: This deactivated key will not be usable from this stage. It will, however, persist, and can be activated again if needed. @@ -156,16 +158,17 @@ def deactivate( Raise: AuthException: raised if deactivation operation fails """ - self._auth.do_post( + response = self._auth.do_post( MgmtV1.access_key_deactivate_path, {"id": id}, pswd=self._auth.management_key, ) + return futu_apply(response, lambda response: None) def activate( self, id: str, - ): + ) -> Union[None, Awaitable[None]]: """ Activate an existing access key. IMPORTANT: Only deactivated keys can be activated again, and become usable once more. New access keys are active by default. @@ -176,16 +179,17 @@ def activate( Raise: AuthException: raised if activation operation fails """ - self._auth.do_post( + response = self._auth.do_post( MgmtV1.access_key_activate_path, {"id": id}, pswd=self._auth.management_key, ) + return futu_apply(response, lambda response: None) def delete( self, id: str, - ): + ) -> Union[None, Awaitable[None]]: """ Delete an existing access key. IMPORTANT: This action is irreversible. Use carefully. @@ -195,11 +199,12 @@ def delete( Raise: AuthException: raised if creation operation fails """ - self._auth.do_post( + response = self._auth.do_post( MgmtV1.access_key_delete_path, {"id": id}, pswd=self._auth.management_key, ) + return futu_apply(response, lambda response: None) @staticmethod def _compose_create_body( diff --git a/descope/management/audit.py b/descope/management/audit.py index 324ff16b9..3e5e2ae0a 100644 --- a/descope/management/audit.py +++ b/descope/management/audit.py @@ -1,7 +1,8 @@ from datetime import datetime -from typing import Any, List, Optional +from typing import Any, Awaitable, List, Optional, Union from descope._auth_base import AuthBase +from descope.future_utils import futu_apply from descope.management.common import MgmtV1 @@ -21,7 +22,7 @@ def search( text: Optional[str] = None, from_ts: Optional[datetime] = None, to_ts: Optional[datetime] = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Search the audit trail up to last 30 days based on given parameters @@ -96,9 +97,14 @@ def search( body=body, pswd=self._auth.management_key, ) - return { - "audits": list(map(Audit._convert_audit_record, response.json()["audits"])) - } + return futu_apply( + response, + lambda response: { + "audits": list( + map(Audit._convert_audit_record, response.json()["audits"]) + ) + }, + ) def create_event( self, @@ -108,7 +114,7 @@ def create_event( tenant_id: str, user_id: Optional[str] = None, data: Optional[dict] = None, - ): + ) -> Union[None, Awaitable[None]]: """ Create audit event based on given parameters @@ -134,11 +140,15 @@ def create_event( if data is not None: body["data"] = data - self._auth.do_post( + response = self._auth.do_post( MgmtV1.audit_create_event, body=body, pswd=self._auth.management_key, ) + return futu_apply( + response, + lambda response: None, + ) @staticmethod def _convert_audit_record(a: dict) -> dict: diff --git a/descope/management/authz.py b/descope/management/authz.py index f2d6816a2..2098f4281 100644 --- a/descope/management/authz.py +++ b/descope/management/authz.py @@ -1,12 +1,15 @@ from datetime import datetime, timezone -from typing import Any, List, Optional +from typing import Any, Awaitable, List, Optional, Union from descope._auth_base import AuthBase +from descope.future_utils import futu_apply from descope.management.common import MgmtV1 class Authz(AuthBase): - def save_schema(self, schema: dict, upgrade: bool = False): + def save_schema( + self, schema: dict, upgrade: bool = False + ) -> Union[None, Awaitable[None]]: """ Create or update the ReBAC schema. In case of update, will update only given namespaces and will not delete namespaces unless upgrade flag is true. @@ -40,25 +43,27 @@ def save_schema(self, schema: dict, upgrade: bool = False): Raise: AuthException: raised if saving fails """ - self._auth.do_post( + response = self._auth.do_post( MgmtV1.authz_schema_save, {"schema": schema, "upgrade": upgrade}, pswd=self._auth.management_key, ) + return futu_apply(response, lambda response: None) - def delete_schema(self): + def delete_schema(self) -> Union[None, Awaitable[None]]: """ Delete the schema for the project which will also delete all relations. Raise: AuthException: raised if delete schema fails """ - self._auth.do_post( + response = self._auth.do_post( MgmtV1.authz_schema_delete, None, pswd=self._auth.management_key, ) + return futu_apply(response, lambda response: None) - def load_schema(self) -> dict: + def load_schema(self) -> Union[dict, Awaitable[dict]]: """ Load the schema for the project Return value (dict): @@ -71,11 +76,11 @@ def load_schema(self) -> dict: None, pswd=self._auth.management_key, ) - return response.json()["schema"] + return futu_apply(response, lambda response: response.json()["schema"]) def save_namespace( self, namespace: dict, old_name: str = "", schema_name: str = "" - ): + ) -> Union[None, Awaitable[None]]: """ Create or update the given namespace Will not delete relation definitions not mentioned in the namespace. @@ -91,13 +96,16 @@ def save_namespace( body["oldName"] = old_name if schema_name != "": body["schemaName"] = schema_name - self._auth.do_post( + response = self._auth.do_post( MgmtV1.authz_ns_save, body, pswd=self._auth.management_key, ) + return futu_apply(response, lambda response: None) - def delete_namespace(self, name: str, schema_name: str = ""): + def delete_namespace( + self, name: str, schema_name: str = "" + ) -> Union[None, Awaitable[None]]: """ delete_namespace will also delete the relevant relations. Args: @@ -109,11 +117,12 @@ def delete_namespace(self, name: str, schema_name: str = ""): body: dict[str, Any] = {"name": name} if schema_name != "": body["schemaName"] = schema_name - self._auth.do_post( + response = self._auth.do_post( MgmtV1.authz_ns_delete, body, pswd=self._auth.management_key, ) + return futu_apply(response, lambda response: None) def save_relation_definition( self, @@ -121,7 +130,7 @@ def save_relation_definition( namespace: str, old_name: str = "", schema_name: str = "", - ): + ) -> Union[None, Awaitable[None]]: """ Create or update the given relation definition Will not delete relation definitions not mentioned in the namespace. @@ -141,15 +150,16 @@ def save_relation_definition( body["oldName"] = old_name if schema_name != "": body["schemaName"] = schema_name - self._auth.do_post( + response = self._auth.do_post( MgmtV1.authz_rd_save, body, pswd=self._auth.management_key, ) + return futu_apply(response, lambda response: None) def delete_relation_definition( self, name: str, namespace: str, schema_name: str = "" - ): + ) -> Union[None, Awaitable[None]]: """ delete_relation_definition will also delete the relevant relations. Args: @@ -162,16 +172,17 @@ def delete_relation_definition( body: dict[str, Any] = {"name": name, "namespace": namespace} if schema_name != "": body["schemaName"] = schema_name - self._auth.do_post( + response = self._auth.do_post( MgmtV1.authz_rd_delete, body, pswd=self._auth.management_key, ) + return futu_apply(response, lambda response: None) def create_relations( self, relations: List[dict], - ): + ) -> Union[None, Awaitable[None]]: """ Create the given relations based on the existing schema Args: @@ -202,18 +213,19 @@ def create_relations( Raise: AuthException: raised if create relations fails """ - self._auth.do_post( + response = self._auth.do_post( MgmtV1.authz_re_create, { "relations": relations, }, pswd=self._auth.management_key, ) + return futu_apply(response, lambda response: None) def delete_relations( self, relations: List[dict], - ): + ) -> Union[None, Awaitable[None]]: """ Delete the given relations based on the existing schema Args: @@ -221,18 +233,19 @@ def delete_relations( Raise: AuthException: raised if delete relations fails """ - self._auth.do_post( + response = self._auth.do_post( MgmtV1.authz_re_delete, { "relations": relations, }, pswd=self._auth.management_key, ) + return futu_apply(response, lambda response: None) def delete_relations_for_resources( self, resources: List[str], - ): + ) -> Union[None, Awaitable[None]]: """ Delete all relations to the given resources Args: @@ -240,18 +253,19 @@ def delete_relations_for_resources( Raise: AuthException: raised if delete relations for resources fails """ - self._auth.do_post( + response = self._auth.do_post( MgmtV1.authz_re_delete_resources, { "resources": resources, }, pswd=self._auth.management_key, ) + return futu_apply(response, lambda response: None) def has_relations( self, relation_queries: List[dict], - ) -> List[dict]: + ) -> Union[List[dict], Awaitable[List[dict]]]: """ Queries the given relations to see if they exist returning true if they do Args: @@ -284,11 +298,11 @@ def has_relations( }, pswd=self._auth.management_key, ) - return response.json()["relationQueries"] + return futu_apply(response, lambda response: response.json()["relationQueries"]) def who_can_access( self, resource: str, relation_definition: str, namespace: str - ) -> List[dict]: + ) -> Union[List[dict], Awaitable[List[dict]]]: """ Finds the list of targets (usually users) who can access the given resource with the given RD Args: @@ -309,9 +323,11 @@ def who_can_access( }, pswd=self._auth.management_key, ) - return response.json()["targets"] + return futu_apply(response, lambda response: response.json()["targets"]) - def resource_relations(self, resource: str) -> List[dict]: + def resource_relations( + self, resource: str + ) -> Union[List[dict], Awaitable[List[dict]]]: """ Returns the list of all defined relations (not recursive) on the given resource. Args: @@ -327,9 +343,11 @@ def resource_relations(self, resource: str) -> List[dict]: {"resource": resource}, pswd=self._auth.management_key, ) - return response.json()["relations"] + return futu_apply(response, lambda response: response.json()["relations"]) - def targets_relations(self, targets: List[str]) -> List[dict]: + def targets_relations( + self, targets: List[str] + ) -> Union[List[dict], Awaitable[List[dict]]]: """ Returns the list of all defined relations (not recursive) for the given targets. Args: @@ -345,9 +363,11 @@ def targets_relations(self, targets: List[str]) -> List[dict]: {"targets": targets}, pswd=self._auth.management_key, ) - return response.json()["relations"] + return futu_apply(response, lambda response: response.json()["relations"]) - def what_can_target_access(self, target: str) -> List[dict]: + def what_can_target_access( + self, target: str + ) -> Union[List[dict], Awaitable[List[dict]]]: """ Returns the list of all relations for the given target including derived relations from the schema tree. Args: @@ -363,11 +383,11 @@ def what_can_target_access(self, target: str) -> List[dict]: {"target": target}, pswd=self._auth.management_key, ) - return response.json()["relations"] + return futu_apply(response, lambda response: response.json()["relations"]) def what_can_target_access_with_relation( self, target: str, relation_definition: str, namespace: str - ) -> List[dict]: + ) -> Union[List[dict], Awaitable[List[dict]]]: """ Returns the list of all resources that the target has the given relation to including all derived relations Args: @@ -389,9 +409,11 @@ def what_can_target_access_with_relation( }, pswd=self._auth.management_key, ) - return response.json()["relations"] + return futu_apply(response, lambda response: response.json()["relations"]) - def get_modified(self, since: Optional[datetime] = None) -> dict: + def get_modified( + self, since: Optional[datetime] = None + ) -> Union[dict, Awaitable[dict]]: """ Get all targets and resources changed since the given date. Args: @@ -413,4 +435,4 @@ def get_modified(self, since: Optional[datetime] = None) -> dict: }, pswd=self._auth.management_key, ) - return response.json()["relations"] + return futu_apply(response, lambda response: response.json()["relations"]) diff --git a/descope/management/fga.py b/descope/management/fga.py index df7d5d041..efd6345f0 100644 --- a/descope/management/fga.py +++ b/descope/management/fga.py @@ -1,11 +1,12 @@ -from typing import List +from typing import Awaitable, List, Union from descope._auth_base import AuthBase +from descope.future_utils import futu_apply from descope.management.common import MgmtV1 class FGA(AuthBase): - def save_schema(self, schema: str): + def save_schema(self, schema: str) -> Union[None, Awaitable[None]]: """ Create or update an FGA schema. Args: @@ -40,17 +41,18 @@ def save_schema(self, schema: str): Raise: AuthException: raised if saving fails """ - self._auth.do_post_with_custom_base_url( + res = self._auth.do_post_with_custom_base_url( MgmtV1.fga_save_schema, {"dsl": schema}, custom_base_url=self._auth.fga_cache_url, pswd=self._auth.management_key, ) + return futu_apply(res, lambda res: None) def create_relations( self, relations: List[dict], - ): + ) -> Union[None, Awaitable[None]]: """ Create the given relations based on the existing schema Args: @@ -65,7 +67,7 @@ def create_relations( Raise: AuthException: raised if create relations fails """ - self._auth.do_post_with_custom_base_url( + res = self._auth.do_post_with_custom_base_url( MgmtV1.fga_create_relations, { "tuples": relations, @@ -73,11 +75,12 @@ def create_relations( custom_base_url=self._auth.fga_cache_url, pswd=self._auth.management_key, ) + return futu_apply(res, lambda res: None) def delete_relations( self, relations: List[dict], - ): + ) -> Union[None, Awaitable[None]]: """ Delete the given relations based on the existing schema Args: @@ -85,7 +88,7 @@ def delete_relations( Raise: AuthException: raised if delete relations fails """ - self._auth.do_post_with_custom_base_url( + res = self._auth.do_post_with_custom_base_url( MgmtV1.fga_delete_relations, { "tuples": relations, @@ -93,11 +96,12 @@ def delete_relations( custom_base_url=self._auth.fga_cache_url, pswd=self._auth.management_key, ) + return futu_apply(res, lambda res: None) def check( self, relations: List[dict], - ) -> List[dict]: + ) -> Union[List[dict], Awaitable[List[dict]]]: """ Queries the given relations to see if they exist returning true if they do Args: @@ -135,14 +139,22 @@ def check( custom_base_url=self._auth.fga_cache_url, pswd=self._auth.management_key, ) - return list( - map( - lambda tuple: {"relation": tuple["tuple"], "allowed": tuple["allowed"]}, - response.json()["tuples"], - ) + return futu_apply( + response, + lambda response: list( + map( + lambda tuple: { + "relation": tuple["tuple"], + "allowed": tuple["allowed"], + }, + response.json()["tuples"], + ) + ), ) - def load_resources_details(self, resource_identifiers: List[dict]) -> List[dict]: + def load_resources_details( + self, resource_identifiers: List[dict] + ) -> Union[List[dict], Awaitable[List[dict]]]: """ Load details for the given resource identifiers. Args: @@ -155,16 +167,21 @@ def load_resources_details(self, resource_identifiers: List[dict]) -> List[dict] {"resourceIdentifiers": resource_identifiers}, pswd=self._auth.management_key, ) - return response.json().get("resourcesDetails", []) + return futu_apply( + response, lambda response: response.json().get("resourcesDetails", []) + ) - def save_resources_details(self, resources_details: List[dict]) -> None: + def save_resources_details( + self, resources_details: List[dict] + ) -> Union[None, Awaitable[None]]: """ Save details for the given resources. Args: resources_details (List[dict]): list of dicts each containing 'resourceId' and 'resourceType' plus optionally containing metadata fields such as 'displayName'. """ - self._auth.do_post( + response = self._auth.do_post( MgmtV1.fga_resources_save, {"resourcesDetails": resources_details}, pswd=self._auth.management_key, ) + return futu_apply(response, lambda response: None) diff --git a/descope/management/flow.py b/descope/management/flow.py index 80dad519c..a318ac6ff 100644 --- a/descope/management/flow.py +++ b/descope/management/flow.py @@ -1,13 +1,14 @@ -from typing import List +from typing import Awaitable, List, Union from descope._auth_base import AuthBase +from descope.future_utils import futu_apply from descope.management.common import MgmtV1 class Flow(AuthBase): def list_flows( self, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ List all project flows @@ -23,12 +24,15 @@ def list_flows( None, pswd=self._auth.management_key, ) - return response.json() + return futu_apply( + response, + lambda response: response.json(), + ) def delete_flows( self, flow_ids: List[str], - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Delete flows by the given ids @@ -45,12 +49,15 @@ def delete_flows( }, pswd=self._auth.management_key, ) - return response.json() + return futu_apply( + response, + lambda response: response.json(), + ) def export_flow( self, flow_id: str, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Export the given flow id flow and screens. @@ -71,14 +78,17 @@ def export_flow( }, pswd=self._auth.management_key, ) - return response.json() + return futu_apply( + response, + lambda response: response.json(), + ) def import_flow( self, flow_id: str, flow: dict, screens: List[dict], - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Import the given flow and screens to the flow id. Imoprtant: This will override the current project flow by the given id, treat with caution. @@ -106,11 +116,14 @@ def import_flow( }, pswd=self._auth.management_key, ) - return response.json() + return futu_apply( + response, + lambda response: response.json(), + ) def export_theme( self, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Export the current project theme. @@ -126,12 +139,15 @@ def export_theme( {}, pswd=self._auth.management_key, ) - return response.json() + return futu_apply( + response, + lambda response: response.json(), + ) def import_theme( self, theme: dict, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Import the given theme as the current project theme. Imoprtant: This will override the current project theme, treat with caution. @@ -154,4 +170,7 @@ def import_theme( }, pswd=self._auth.management_key, ) - return response.json() + return futu_apply( + response, + lambda response: response.json(), + ) diff --git a/descope/management/group.py b/descope/management/group.py index c4dba3b54..c73c7562d 100644 --- a/descope/management/group.py +++ b/descope/management/group.py @@ -1,6 +1,7 @@ -from typing import List, Optional +from typing import Awaitable, List, Optional, Union from descope._auth_base import AuthBase +from descope.future_utils import futu_apply from descope.management.common import MgmtV1 @@ -8,7 +9,7 @@ class Group(AuthBase): def load_all_groups( self, tenant_id: str, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Load all groups for a specific tenant id. @@ -42,14 +43,17 @@ def load_all_groups( }, pswd=self._auth.management_key, ) - return response.json() + return futu_apply( + response, + lambda response: response.json(), + ) def load_all_groups_for_members( self, tenant_id: str, user_ids: Optional[List[str]] = None, login_ids: Optional[List[str]] = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Load all groups for the provided user IDs or login IDs. @@ -90,13 +94,16 @@ def load_all_groups_for_members( }, pswd=self._auth.management_key, ) - return response.json() + return futu_apply( + response, + lambda response: response.json(), + ) def load_all_group_members( self, tenant_id: str, group_id: str, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Load all members of the provided group id. @@ -132,4 +139,7 @@ def load_all_group_members( }, pswd=self._auth.management_key, ) - return response.json() + return futu_apply( + response, + lambda response: response.json(), + ) diff --git a/descope/management/jwt.py b/descope/management/jwt.py index 9adaa9338..5761c00da 100644 --- a/descope/management/jwt.py +++ b/descope/management/jwt.py @@ -1,6 +1,7 @@ -from typing import Optional +from typing import Awaitable, Optional, Union from descope._auth_base import AuthBase +from descope.future_utils import futu_apply from descope.exceptions import ERROR_TYPE_INVALID_ARGUMENT, AuthException from descope.management.common import ( MgmtLoginOptions, @@ -14,7 +15,7 @@ class JWT(AuthBase): def update_jwt( self, jwt: str, custom_claims: dict, refresh_duration: int = 0 - ) -> str: + ) -> Union[str, Awaitable[str]]: """ Given a valid JWT, update it with custom claims, and update its authz claims as well @@ -39,7 +40,10 @@ def update_jwt( }, pswd=self._auth.management_key, ) - return response.json().get("jwt", "") + return futu_apply( + response, + lambda response: response.json().get("jwt", ""), + ) def impersonate( self, @@ -49,7 +53,7 @@ def impersonate( custom_claims: Optional[dict] = None, tenant_id: Optional[str] = None, refresh_duration: Optional[int] = None, - ) -> str: + ) -> Union[str, Awaitable[str]]: """ Impersonate to another user @@ -86,7 +90,10 @@ def impersonate( }, pswd=self._auth.management_key, ) - return response.json().get("jwt", "") + return futu_apply( + response, + lambda response: response.json().get("jwt", ""), + ) def stop_impersonation( self, @@ -94,7 +101,7 @@ def stop_impersonation( custom_claims: Optional[dict] = None, tenant_id: Optional[str] = None, refresh_duration: Optional[int] = None, - ) -> str: + ) -> Union[str, Awaitable[str]]: """ Stop impersonation and return to the original user Args: @@ -121,11 +128,14 @@ def stop_impersonation( }, pswd=self._auth.management_key, ) - return response.json().get("jwt", "") + return futu_apply( + response, + lambda response: response.json().get("jwt", ""), + ) def sign_in( self, login_id: str, login_options: Optional[MgmtLoginOptions] = None - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Generate a JWT for a user, simulating a signin request. @@ -158,16 +168,19 @@ def sign_in( }, pswd=self._auth.management_key, ) - resp = response.json() - jwt_response = self._auth.generate_jwt_response(resp, None, None) - return jwt_response + return futu_apply( + response, + lambda response: self._auth.generate_jwt_response( + response.json(), None, None + ), + ) def sign_up( self, login_id: str, user: Optional[MgmtUserRequest] = None, signup_options: Optional[MgmtSignUpOptions] = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Generate a JWT for a user, simulating a signup request. @@ -186,7 +199,7 @@ def sign_up_or_in( login_id: str, user: Optional[MgmtUserRequest] = None, signup_options: Optional[MgmtSignUpOptions] = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Generate a JWT for a user, simulating a signup or in request. @@ -205,7 +218,7 @@ def _sign_up_internal( endpoint: str, user: Optional[MgmtUserRequest] = None, signup_options: Optional[MgmtSignUpOptions] = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: if user is None: user = MgmtUserRequest() @@ -230,16 +243,19 @@ def _sign_up_internal( }, pswd=self._auth.management_key, ) - resp = response.json() - jwt_response = self._auth.generate_jwt_response(resp, None, None) - return jwt_response + return futu_apply( + response, + lambda response: self._auth.generate_jwt_response( + response.json(), None, None + ), + ) def anonymous( self, custom_claims: Optional[dict] = None, tenant_id: Optional[str] = None, refresh_duration: Optional[int] = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Generate a JWT for an anonymous user. @@ -257,6 +273,13 @@ def anonymous( }, pswd=self._auth.management_key, ) + return futu_apply( + response, + lambda response: self._anonymous_jwt_response(response), + ) + + def _anonymous_jwt_response(self, response): + """Helper method to process anonymous JWT response""" resp = response.json() jwt_response = self._auth.generate_jwt_response(resp, None, None) del jwt_response["firstSeen"] diff --git a/descope/management/outbound_application.py b/descope/management/outbound_application.py index 2b9bc2deb..5f9dc6d1d 100644 --- a/descope/management/outbound_application.py +++ b/descope/management/outbound_application.py @@ -1,8 +1,9 @@ -from typing import Any, List, Optional +from typing import Any, Awaitable, List, Optional, Union from descope._auth_base import AuthBase from descope.auth import Auth from descope.exceptions import ERROR_TYPE_INVALID_ARGUMENT, AuthException # noqa: F401 +from descope.future_utils import futu_apply from descope.management.common import ( AccessType, MgmtV1, @@ -24,7 +25,7 @@ def fetch_token_by_scopes( scopes: List[str], options: Optional[dict] = None, tenant_id: Optional[str] = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """Internal implementation for fetching token by scopes.""" uri = MgmtV1.outbound_application_fetch_token_by_scopes_path response = auth_instance.do_post( @@ -38,7 +39,7 @@ def fetch_token_by_scopes( }, pswd=token, ) - return response.json() + return futu_apply(response, lambda response: response.json()) @staticmethod def fetch_token( @@ -48,7 +49,7 @@ def fetch_token( user_id: str, tenant_id: Optional[str] = None, options: Optional[dict] = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """Internal implementation for fetching token.""" uri = MgmtV1.outbound_application_fetch_token_path response = auth_instance.do_post( @@ -61,7 +62,7 @@ def fetch_token( }, pswd=token, ) - return response.json() + return futu_apply(response, lambda response: response.json()) @staticmethod def fetch_tenant_token_by_scopes( @@ -71,7 +72,7 @@ def fetch_tenant_token_by_scopes( tenant_id: str, scopes: List[str], options: Optional[dict] = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """Internal implementation for fetching tenant token by scopes.""" uri = MgmtV1.outbound_application_fetch_tenant_token_by_scopes_path response = auth_instance.do_post( @@ -84,7 +85,7 @@ def fetch_tenant_token_by_scopes( }, pswd=token, ) - return response.json() + return futu_apply(response, lambda response: response.json()) @staticmethod def fetch_tenant_token( @@ -93,7 +94,7 @@ def fetch_tenant_token( app_id: str, tenant_id: str, options: Optional[dict] = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """Internal implementation for fetching tenant token.""" uri = MgmtV1.outbound_application_fetch_tenant_token_path response = auth_instance.do_post( @@ -105,7 +106,7 @@ def fetch_tenant_token( }, pswd=token, ) - return response.json() + return futu_apply(response, lambda response: response.json()) class OutboundApplication(AuthBase): @@ -129,7 +130,7 @@ def create_application( pkce: Optional[bool] = None, access_type: Optional[AccessType] = None, prompt: Optional[List[PromptType]] = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Create a new outbound application with the given name. Outbound application IDs are provisioned automatically, but can be provided explicitly if needed. Both the name and ID must be unique per project. @@ -186,7 +187,7 @@ def create_application( ), pswd=self._auth.management_key, ) - return response.json() + return futu_apply(response, lambda response: response.json()) def update_application( self, @@ -208,7 +209,7 @@ def update_application( pkce: Optional[bool] = None, access_type: Optional[AccessType] = None, prompt: Optional[List[PromptType]] = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Update an existing outbound application with the given parameters. IMPORTANT: All parameters are used as overrides to the existing outbound application. Empty fields will override populated fields. Use carefully. @@ -267,12 +268,12 @@ def update_application( }, pswd=self._auth.management_key, ) - return response.json() + return futu_apply(response, lambda response: response.json()) def delete_application( self, id: str, - ): + ) -> Union[None, Awaitable[None]]: """ Delete an existing outbound application. IMPORTANT: This action is irreversible. Use carefully. @@ -283,12 +284,13 @@ def delete_application( AuthException: raised if deletion operation fails """ uri = MgmtV1.outbound_application_delete_path - self._auth.do_post(uri, {"id": id}, pswd=self._auth.management_key) + response = self._auth.do_post(uri, {"id": id}, pswd=self._auth.management_key) + return futu_apply(response, lambda response: None) def load_application( self, id: str, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Load outbound application by id. @@ -307,11 +309,11 @@ def load_application( uri=f"{MgmtV1.outbound_application_load_path}/{id}", pswd=self._auth.management_key, ) - return response.json() + return futu_apply(response, lambda response: response.json()) def load_all_applications( self, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Load all outbound applications. @@ -327,7 +329,7 @@ def load_all_applications( uri=MgmtV1.outbound_application_load_all_path, pswd=self._auth.management_key, ) - return response.json() + return futu_apply(response, lambda response: response.json()) def fetch_token_by_scopes( self, @@ -336,7 +338,7 @@ def fetch_token_by_scopes( scopes: List[str], options: Optional[dict] = None, tenant_id: Optional[str] = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Fetch an outbound application token for a user with specific scopes. @@ -354,15 +356,19 @@ def fetch_token_by_scopes( Raise: AuthException: raised if fetch operation fails """ - return _OutboundApplicationTokenFetcher.fetch_token_by_scopes( - self._auth, - self._auth.management_key, # type: ignore[arg-type] # will never get here with None value - app_id, - user_id, - scopes, - options, - tenant_id, + uri = MgmtV1.outbound_application_fetch_token_by_scopes_path + response = self._auth.do_post( + uri, + { + "appId": app_id, + "userId": user_id, + "scopes": scopes, + "options": options, + "tenantId": tenant_id, + }, + pswd=self._auth.management_key, ) + return futu_apply(response, lambda response: response.json()) def fetch_token( self, @@ -370,7 +376,7 @@ def fetch_token( user_id: str, tenant_id: Optional[str] = None, options: Optional[dict] = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Fetch an outbound application token for a user. @@ -387,14 +393,18 @@ def fetch_token( Raise: AuthException: raised if fetch operation fails """ - return _OutboundApplicationTokenFetcher.fetch_token( - self._auth, - self._auth.management_key, # type: ignore[arg-type] # will never get here with None value - app_id, - user_id, - tenant_id, - options, + uri = MgmtV1.outbound_application_fetch_token_path + response = self._auth.do_post( + uri, + { + "appId": app_id, + "userId": user_id, + "tenantId": tenant_id, + "options": options, + }, + pswd=self._auth.management_key, ) + return futu_apply(response, lambda response: response.json()) def fetch_tenant_token_by_scopes( self, @@ -402,7 +412,7 @@ def fetch_tenant_token_by_scopes( tenant_id: str, scopes: List[str], options: Optional[dict] = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Fetch an outbound application token for a tenant with specific scopes. @@ -419,21 +429,25 @@ def fetch_tenant_token_by_scopes( Raise: AuthException: raised if fetch operation fails """ - return _OutboundApplicationTokenFetcher.fetch_tenant_token_by_scopes( - self._auth, - self._auth.management_key, # type: ignore[arg-type] # will never get here with None value - app_id, - tenant_id, - scopes, - options, + uri = MgmtV1.outbound_application_fetch_tenant_token_by_scopes_path + response = self._auth.do_post( + uri, + { + "appId": app_id, + "tenantId": tenant_id, + "scopes": scopes, + "options": options, + }, + pswd=self._auth.management_key, ) + return futu_apply(response, lambda response: response.json()) def fetch_tenant_token( self, app_id: str, tenant_id: str, options: Optional[dict] = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Fetch an outbound application token for a tenant. @@ -449,13 +463,17 @@ def fetch_tenant_token( Raise: AuthException: raised if fetch operation fails """ - return _OutboundApplicationTokenFetcher.fetch_tenant_token( - self._auth, - self._auth.management_key, # type: ignore[arg-type] # will never get here with None value - app_id, - tenant_id, - options, + uri = MgmtV1.outbound_application_fetch_tenant_token_path + response = self._auth.do_post( + uri, + { + "appId": app_id, + "tenantId": tenant_id, + "options": options, + }, + pswd=self._auth.management_key, ) + return futu_apply(response, lambda response: response.json()) @staticmethod def _compose_create_update_body( @@ -539,7 +557,7 @@ def fetch_token_by_scopes( scopes: List[str], options: Optional[dict] = None, tenant_id: Optional[str] = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Fetch an outbound application token for a user with specific scopes. @@ -570,7 +588,7 @@ def fetch_token( user_id: str, tenant_id: Optional[str] = None, options: Optional[dict] = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Fetch an outbound application token for a user. @@ -600,7 +618,7 @@ def fetch_tenant_token_by_scopes( tenant_id: str, scopes: List[str], options: Optional[dict] = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Fetch an outbound application token for a tenant with specific scopes. @@ -625,7 +643,7 @@ def fetch_tenant_token_by_scopes( def fetch_tenant_token( self, token: str, app_id: str, tenant_id: str, options: Optional[dict] = None - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Fetch an outbound application token for a tenant. diff --git a/descope/management/permission.py b/descope/management/permission.py index 273d96ed7..0cb006d68 100644 --- a/descope/management/permission.py +++ b/descope/management/permission.py @@ -1,6 +1,7 @@ -from typing import Optional +from typing import Awaitable, Optional, Union from descope._auth_base import AuthBase +from descope.future_utils import futu_apply from descope.management.common import MgmtV1 @@ -9,7 +10,7 @@ def create( self, name: str, description: Optional[str] = None, - ): + ) -> Union[None, Awaitable[None]]: """ Create a new permission. @@ -20,18 +21,19 @@ def create( Raise: AuthException: raised if creation operation fails """ - self._auth.do_post( + response = self._auth.do_post( MgmtV1.permission_create_path, {"name": name, "description": description}, pswd=self._auth.management_key, ) + return futu_apply(response, lambda response: None) def update( self, name: str, new_name: str, description: Optional[str] = None, - ): + ) -> Union[None, Awaitable[None]]: """ Update an existing permission with the given various fields. IMPORTANT: All parameters are used as overrides to the existing permission. Empty fields will override populated fields. Use carefully. @@ -44,16 +46,17 @@ def update( Raise: AuthException: raised if update operation fails """ - self._auth.do_post( + response = self._auth.do_post( MgmtV1.permission_update_path, {"name": name, "newName": new_name, "description": description}, pswd=self._auth.management_key, ) + return futu_apply(response, lambda response: None) def delete( self, name: str, - ): + ) -> Union[None, Awaitable[None]]: """ Delete an existing permission. IMPORTANT: This action is irreversible. Use carefully. @@ -63,15 +66,16 @@ def delete( Raise: AuthException: raised if creation operation fails """ - self._auth.do_post( + response = self._auth.do_post( MgmtV1.permission_delete_path, {"name": name}, pswd=self._auth.management_key, ) + return futu_apply(response, lambda response: None) def load_all( self, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Load all permissions. @@ -87,4 +91,4 @@ def load_all( uri=MgmtV1.permission_load_all_path, pswd=self._auth.management_key, ) - return response.json() + return futu_apply(response, lambda response: response.json()) diff --git a/descope/management/project.py b/descope/management/project.py index ba9fa2b76..cdc3ddd68 100644 --- a/descope/management/project.py +++ b/descope/management/project.py @@ -1,6 +1,7 @@ -from typing import List, Optional +from typing import Awaitable, List, Optional, Union from descope._auth_base import AuthBase +from descope.future_utils import futu_apply from descope.management.common import MgmtV1 @@ -8,7 +9,7 @@ class Project(AuthBase): def update_name( self, name: str, - ): + ) -> Union[None, Awaitable[None]]: """ Update the current project name. @@ -17,18 +18,22 @@ def update_name( Raise: AuthException: raised if operation fails """ - self._auth.do_post( + response = self._auth.do_post( MgmtV1.project_update_name, { "name": name, }, pswd=self._auth.management_key, ) + return futu_apply( + response, + lambda response: None, + ) def update_tags( self, tags: List[str], - ): + ) -> Union[None, Awaitable[None]]: """ Update the current project tags. @@ -37,17 +42,21 @@ def update_tags( Raise: AuthException: raised if operation fails """ - self._auth.do_post( + response = self._auth.do_post( MgmtV1.project_update_tags, { "tags": tags, }, pswd=self._auth.management_key, ) + return futu_apply( + response, + lambda response: None, + ) def list_projects( self, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ List of all the projects in the company. @@ -64,12 +73,17 @@ def list_projects( {}, pswd=self._auth.management_key, ) - resp = response.json() + return futu_apply( + response, + lambda response: self._process_list_projects_response(response), + ) + def _process_list_projects_response(self, response): + """Helper method to process list_projects response""" + resp = response.json() projects = resp["projects"] # Apply the function to the projects list formatted_projects = self.remove_tag_field(projects) - # Return the same structure with 'tag' removed result = {"projects": formatted_projects} return result @@ -79,7 +93,7 @@ def clone( name: str, environment: Optional[str] = None, tags: Optional[List[str]] = None, - ): + ) -> Union[dict, Awaitable[dict]]: """ Clone the current project, including its settings and configurations. - This action is supported only with a pro license or above. @@ -105,11 +119,14 @@ def clone( }, pswd=self._auth.management_key, ) - return response.json() + return futu_apply( + response, + lambda response: response.json(), + ) def export_project( self, - ): + ) -> Union[dict, Awaitable[dict]]: """ Exports all settings and configurations for a project and returns the raw JSON files response as a dictionary. @@ -128,12 +145,15 @@ def export_project( {}, pswd=self._auth.management_key, ) - return response.json()["files"] + return futu_apply( + response, + lambda response: response.json()["files"], + ) def import_project( self, files: dict, - ): + ) -> Union[None, Awaitable[None]]: """ Imports all settings and configurations for a project overriding any current configuration. @@ -147,14 +167,17 @@ def import_project( Raise: AuthException: raised if import operation fails """ - self._auth.do_post( + response = self._auth.do_post( MgmtV1.project_import, { "files": files, }, pswd=self._auth.management_key, ) - return + return futu_apply( + response, + lambda response: None, + ) # Function to remove 'tag' field from each project def remove_tag_field(self, projects): diff --git a/descope/management/role.py b/descope/management/role.py index 2c11f2865..956778008 100644 --- a/descope/management/role.py +++ b/descope/management/role.py @@ -1,6 +1,7 @@ -from typing import List, Optional +from typing import Awaitable, List, Optional, Union from descope._auth_base import AuthBase +from descope.future_utils import futu_apply from descope.management.common import MgmtV1 @@ -12,7 +13,7 @@ def create( permission_names: Optional[List[str]] = None, tenant_id: Optional[str] = None, default: Optional[bool] = None, - ): + ) -> Union[None, Awaitable[None]]: """ Create a new role. @@ -28,7 +29,7 @@ def create( """ permission_names = [] if permission_names is None else permission_names - self._auth.do_post( + response = self._auth.do_post( MgmtV1.role_create_path, { "name": name, @@ -39,6 +40,7 @@ def create( }, pswd=self._auth.management_key, ) + return futu_apply(response, lambda response: None) def update( self, @@ -48,7 +50,7 @@ def update( permission_names: Optional[List[str]] = None, tenant_id: Optional[str] = None, default: Optional[bool] = None, - ): + ) -> Union[None, Awaitable[None]]: """ Update an existing role with the given various fields. IMPORTANT: All parameters are used as overrides to the existing role. Empty fields will override populated fields. Use carefully. @@ -65,7 +67,7 @@ def update( AuthException: raised if update operation fails """ permission_names = [] if permission_names is None else permission_names - self._auth.do_post( + response = self._auth.do_post( MgmtV1.role_update_path, { "name": name, @@ -77,12 +79,13 @@ def update( }, pswd=self._auth.management_key, ) + return futu_apply(response, lambda response: None) def delete( self, name: str, tenant_id: Optional[str] = None, - ): + ) -> Union[None, Awaitable[None]]: """ Delete an existing role. IMPORTANT: This action is irreversible. Use carefully. @@ -92,15 +95,16 @@ def delete( Raise: AuthException: raised if creation operation fails """ - self._auth.do_post( + response = self._auth.do_post( MgmtV1.role_delete_path, {"name": name, "tenantId": tenant_id}, pswd=self._auth.management_key, ) + return futu_apply(response, lambda response: None) def load_all( self, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Load all roles. @@ -116,7 +120,7 @@ def load_all( uri=MgmtV1.role_load_all_path, pswd=self._auth.management_key, ) - return response.json() + return futu_apply(response, lambda response: response.json()) def search( self, @@ -125,7 +129,7 @@ def search( role_name_like: Optional[str] = None, permission_names: Optional[List[str]] = None, include_project_roles: Optional[bool] = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Search roles based on the given filters. @@ -160,4 +164,4 @@ def search( body, pswd=self._auth.management_key, ) - return response.json() + return futu_apply(response, lambda response: response.json()) diff --git a/descope/management/sso_application.py b/descope/management/sso_application.py index c3173ee2d..3867b892d 100644 --- a/descope/management/sso_application.py +++ b/descope/management/sso_application.py @@ -1,6 +1,7 @@ -from typing import Any, List, Optional +from typing import Any, Awaitable, List, Optional, Union from descope._auth_base import AuthBase +from descope.future_utils import futu_apply from descope.management.common import ( MgmtV1, SAMLIDPAttributeMappingInfo, @@ -20,7 +21,7 @@ def create_oidc_application( logo: Optional[str] = None, enabled: Optional[bool] = True, force_authentication: Optional[bool] = False, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Create a new OIDC sso application with the given name. SSO application IDs are provisioned automatically, but can be provided explicitly if needed. Both the name and ID must be unique per project. @@ -55,7 +56,7 @@ def create_oidc_application( ), pswd=self._auth.management_key, ) - return response.json() + return futu_apply(response, lambda response: response.json()) def create_saml_application( self, @@ -78,7 +79,7 @@ def create_saml_application( default_relay_state: Optional[str] = None, force_authentication: Optional[bool] = False, logout_redirect_url: Optional[str] = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Create a new SAML sso application with the given name. SSO application IDs are provisioned automatically, but can be provided explicitly if needed. Both the name and ID must be unique per project. @@ -153,7 +154,7 @@ def create_saml_application( ), pswd=self._auth.management_key, ) - return response.json() + return futu_apply(response, lambda response: response.json()) def update_oidc_application( self, @@ -164,7 +165,7 @@ def update_oidc_application( logo: Optional[str] = None, enabled: Optional[bool] = True, force_authentication: Optional[bool] = False, - ): + ) -> Union[None, Awaitable[None]]: """ Update an existing OIDC sso application with the given parameters. IMPORTANT: All parameters are used as overrides to the existing sso application. Empty fields will override populated fields. Use carefully. @@ -183,7 +184,7 @@ def update_oidc_application( """ uri = MgmtV1.sso_application_oidc_update_path - self._auth.do_post( + response = self._auth.do_post( uri, SSOApplication._compose_create_update_oidc_body( name, @@ -196,6 +197,7 @@ def update_oidc_application( ), pswd=self._auth.management_key, ) + return futu_apply(response, lambda response: None) def update_saml_application( self, @@ -218,7 +220,7 @@ def update_saml_application( default_relay_state: Optional[str] = None, force_authentication: Optional[bool] = False, logout_redirect_url: Optional[str] = None, - ): + ) -> Union[None, Awaitable[None]]: """ Update an existing SAML sso application with the given parameters. IMPORTANT: All parameters are used as overrides to the existing sso application. Empty fields will override populated fields. Use carefully. @@ -264,7 +266,7 @@ def update_saml_application( ) uri = MgmtV1.sso_application_saml_update_path - self._auth.do_post( + response = self._auth.do_post( uri, SSOApplication._compose_create_update_saml_body( name, @@ -289,11 +291,12 @@ def update_saml_application( ), pswd=self._auth.management_key, ) + return futu_apply(response, lambda response: None) def delete( self, id: str, - ): + ) -> Union[None, Awaitable[None]]: """ Delete an existing sso application. IMPORTANT: This action is irreversible. Use carefully. @@ -304,12 +307,13 @@ def delete( AuthException: raised if deletion operation fails """ uri = MgmtV1.sso_application_delete_path - self._auth.do_post(uri, {"id": id}, pswd=self._auth.management_key) + response = self._auth.do_post(uri, {"id": id}, pswd=self._auth.management_key) + return futu_apply(response, lambda response: None) def load( self, id: str, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Load sso application by id. @@ -329,11 +333,11 @@ def load( params={"id": id}, pswd=self._auth.management_key, ) - return response.json() + return futu_apply(response, lambda response: response.json()) def load_all( self, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Load all sso applications. @@ -354,7 +358,7 @@ def load_all( uri=MgmtV1.sso_application_load_all_path, pswd=self._auth.management_key, ) - return response.json() + return futu_apply(response, lambda response: response.json()) @staticmethod def _compose_create_update_oidc_body( diff --git a/descope/management/sso_settings.py b/descope/management/sso_settings.py index 01d0fc46f..82c75de1d 100644 --- a/descope/management/sso_settings.py +++ b/descope/management/sso_settings.py @@ -1,6 +1,7 @@ -from typing import List, Optional +from typing import Awaitable, List, Optional, Union from descope._auth_base import AuthBase +from descope.future_utils import futu_apply from descope.management.common import MgmtV1 @@ -164,7 +165,7 @@ class SSOSettings(AuthBase): def load_settings( self, tenant_id: str, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Load SSO setting for the provided tenant_id. @@ -184,12 +185,12 @@ def load_settings( params={"tenantId": tenant_id}, pswd=self._auth.management_key, ) - return response.json() + return futu_apply(response, lambda response: response.json()) def delete_settings( self, tenant_id: str, - ): + ) -> Union[None, Awaitable[None]]: """ Delete SSO setting for the provided tenant_id. @@ -199,18 +200,19 @@ def delete_settings( Raise: AuthException: raised if delete operation fails """ - self._auth.do_delete( + response = self._auth.do_delete( MgmtV1.sso_settings_path, {"tenantId": tenant_id}, pswd=self._auth.management_key, ) + return futu_apply(response, lambda response: None) def configure_oidc_settings( self, tenant_id: str, settings: SSOOIDCSettings, domains: Optional[List[str]] = None, - ): + ) -> Union[None, Awaitable[None]]: """ Configure SSO OIDC settings for a tenant. @@ -223,13 +225,14 @@ def configure_oidc_settings( AuthException: raised if configuration operation fails """ - self._auth.do_post( + response = self._auth.do_post( MgmtV1.sso_configure_oidc_settings, SSOSettings._compose_configure_oidc_settings_body( tenant_id, settings, domains ), pswd=self._auth.management_key, ) + return futu_apply(response, lambda response: None) def configure_saml_settings( self, @@ -237,7 +240,7 @@ def configure_saml_settings( settings: SSOSAMLSettings, redirect_url: Optional[str] = None, domains: Optional[List[str]] = None, - ): + ) -> Union[None, Awaitable[None]]: """ Configure SSO SAML settings for a tenant. @@ -251,13 +254,14 @@ def configure_saml_settings( AuthException: raised if configuration operation fails """ - self._auth.do_post( + response = self._auth.do_post( MgmtV1.sso_configure_saml_settings, SSOSettings._compose_configure_saml_settings_body( tenant_id, settings, redirect_url, domains ), pswd=self._auth.management_key, ) + return futu_apply(response, lambda response: None) def configure_saml_settings_by_metadata( self, @@ -265,7 +269,7 @@ def configure_saml_settings_by_metadata( settings: SSOSAMLSettingsByMetadata, redirect_url: Optional[str] = None, domains: Optional[List[str]] = None, - ): + ) -> Union[None, Awaitable[None]]: """ Configure SSO SAML settings for a tenant by fetching them from an IDP metadata URL. @@ -279,19 +283,20 @@ def configure_saml_settings_by_metadata( AuthException: raised if configuration operation fails """ - self._auth.do_post( + response = self._auth.do_post( MgmtV1.sso_configure_saml_by_metadata_settings, SSOSettings._compose_configure_saml_settings_by_metadata_body( tenant_id, settings, redirect_url, domains ), pswd=self._auth.management_key, ) + return futu_apply(response, lambda response: None) # DEPRECATED def get_settings( self, tenant_id: str, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ DEPRECATED (use load_settings(..) function instead) @@ -311,7 +316,7 @@ def get_settings( params={"tenantId": tenant_id}, pswd=self._auth.management_key, ) - return response.json() + return futu_apply(response, lambda response: response.json()) # DEPRECATED def configure( @@ -322,7 +327,7 @@ def configure( idp_cert: str, redirect_url: str, domains: Optional[List[str]] = None, - ) -> None: + ) -> Union[None, Awaitable[None]]: """ DEPRECATED (use configure_saml_settings(..) function instead) @@ -339,13 +344,14 @@ def configure( Raise: AuthException: raised if configuration operation fails """ - self._auth.do_post( + response = self._auth.do_post( MgmtV1.sso_settings_path, SSOSettings._compose_configure_body( tenant_id, idp_url, entity_id, idp_cert, redirect_url, domains ), pswd=self._auth.management_key, ) + return futu_apply(response, lambda response: None) # DEPRECATED def configure_via_metadata( @@ -354,7 +360,7 @@ def configure_via_metadata( idp_metadata_url: str, redirect_url: Optional[str] = None, domains: Optional[List[str]] = None, - ): + ) -> Union[None, Awaitable[None]]: """ DEPRECATED (use configure_saml_settings_by_metadata(..) function instead) @@ -369,13 +375,14 @@ def configure_via_metadata( Raise: AuthException: raised if configuration operation fails """ - self._auth.do_post( + response = self._auth.do_post( MgmtV1.sso_metadata_path, SSOSettings._compose_metadata_body( tenant_id, idp_metadata_url, redirect_url, domains ), pswd=self._auth.management_key, ) + return futu_apply(response, lambda response: None) # DEPRECATED def mapping( @@ -383,7 +390,7 @@ def mapping( tenant_id: str, role_mappings: Optional[List[RoleMapping]] = None, attribute_mapping: Optional[AttributeMapping] = None, - ): + ) -> Union[None, Awaitable[None]]: """ DEPRECATED (use configure_saml_settings(..) or configure_saml_settings_by_metadata(..) functions instead) @@ -397,13 +404,14 @@ def mapping( Raise: AuthException: raised if configuration operation fails """ - self._auth.do_post( + response = self._auth.do_post( MgmtV1.sso_mapping_path, SSOSettings._compose_mapping_body( tenant_id, role_mappings, attribute_mapping ), pswd=self._auth.management_key, ) + return futu_apply(response, lambda response: None) @staticmethod def _compose_configure_body( diff --git a/descope/management/tenant.py b/descope/management/tenant.py index ccfa1a578..99f91fc22 100644 --- a/descope/management/tenant.py +++ b/descope/management/tenant.py @@ -1,6 +1,7 @@ -from typing import Any, List, Optional +from typing import Any, Awaitable, List, Optional, Union from descope._auth_base import AuthBase +from descope.future_utils import futu_apply from descope.management.common import MgmtV1 @@ -13,7 +14,7 @@ def create( custom_attributes: Optional[dict] = None, enforce_sso: Optional[bool] = False, disabled: Optional[bool] = False, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Create a new tenant with the given name. Tenant IDs are provisioned automatically, but can be provided explicitly if needed. Both the name and ID must be unique per project. @@ -51,7 +52,7 @@ def create( ), pswd=self._auth.management_key, ) - return response.json() + return futu_apply(response, lambda response: response.json()) def update( self, @@ -61,7 +62,7 @@ def update( custom_attributes: Optional[dict] = None, enforce_sso: Optional[bool] = False, disabled: Optional[bool] = False, - ): + ) -> Union[None, Awaitable[None]]: """ Update an existing tenant with the given name and domains. IMPORTANT: All parameters are used as overrides to the existing tenant. Empty fields will override populated fields. Use carefully. @@ -83,7 +84,7 @@ def update( ) uri = MgmtV1.tenant_update_path - self._auth.do_post( + response = self._auth.do_post( uri, Tenant._compose_create_update_body( name, @@ -95,12 +96,13 @@ def update( ), pswd=self._auth.management_key, ) + return futu_apply(response, lambda response: None) def delete( self, id: str, cascade: bool = False, - ): + ) -> Union[None, Awaitable[None]]: """ Delete an existing tenant. IMPORTANT: This action is irreversible. Use carefully. @@ -111,14 +113,15 @@ def delete( AuthException: raised if creation operation fails """ uri = MgmtV1.tenant_delete_path - self._auth.do_post( + response = self._auth.do_post( uri, {"id": id, "cascade": cascade}, pswd=self._auth.management_key ) + return futu_apply(response, lambda response: None) def load( self, id: str, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Load tenant by id. @@ -138,11 +141,11 @@ def load( params={"id": id}, pswd=self._auth.management_key, ) - return response.json() + return futu_apply(response, lambda response: response.json()) def load_all( self, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Load all tenants. @@ -158,7 +161,7 @@ def load_all( uri=MgmtV1.tenant_load_all_path, pswd=self._auth.management_key, ) - return response.json() + return futu_apply(response, lambda response: response.json()) def search_all( self, @@ -166,7 +169,7 @@ def search_all( names: Optional[List[str]] = None, self_provisioning_domains: Optional[List[str]] = None, custom_attributes: Optional[dict] = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Search all tenants. @@ -194,7 +197,7 @@ def search_all( }, pswd=self._auth.management_key, ) - return response.json() + return futu_apply(response, lambda response: response.json()) @staticmethod def _compose_create_update_body( diff --git a/descope/management/user.py b/descope/management/user.py index 8bc01aefd..52ec79e11 100644 --- a/descope/management/user.py +++ b/descope/management/user.py @@ -1,9 +1,10 @@ -from typing import Any, List, Optional, Union +from typing import Any, Awaitable, List, Optional, Union from descope._auth_base import AuthBase from descope.auth import Auth from descope.common import DeliveryMethod, LoginOptions from descope.exceptions import ERROR_TYPE_INVALID_ARGUMENT, AuthException +from descope.future_utils import futu_apply from descope.management.common import ( AssociatedTenant, MgmtV1, @@ -93,7 +94,7 @@ def create( invite_url: Optional[str] = None, additional_login_ids: Optional[List[str]] = None, sso_app_ids: Optional[List[str]] = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Create a new user. Users can have any number of optional fields, including email, phone number and authorization. @@ -147,7 +148,7 @@ def create( ), pswd=self._auth.management_key, ) - return response.json() + return futu_apply(response, lambda response: response.json()) def create_test_user( self, @@ -167,7 +168,7 @@ def create_test_user( invite_url: Optional[str] = None, additional_login_ids: Optional[List[str]] = None, sso_app_ids: Optional[List[str]] = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Create a new test user. The login_id is required and will determine what the user will use to sign in. @@ -223,7 +224,7 @@ def create_test_user( ), pswd=self._auth.management_key, ) - return response.json() + return futu_apply(response, lambda response: response.json()) def invite( self, @@ -251,7 +252,7 @@ def invite( sso_app_ids: Optional[List[str]] = None, template_id: str = "", test: bool = False, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Create a new user and invite them via an email / text message. @@ -293,7 +294,7 @@ def invite( ), pswd=self._auth.management_key, ) - return response.json() + return futu_apply(response, lambda response: response.json()) def invite_batch( self, @@ -305,7 +306,7 @@ def invite_batch( send_sms: Optional[ bool ] = None, # send invite via text message, default is according to project settings - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Create users in batch and invite them via an email / text message. @@ -328,7 +329,7 @@ def invite_batch( ), pswd=self._auth.management_key, ) - return response.json() + return futu_apply(response, lambda response: response.json()) def update( self, @@ -348,7 +349,7 @@ def update( additional_login_ids: Optional[List[str]] = None, sso_app_ids: Optional[List[str]] = None, test: bool = False, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Update an existing user with the given various fields. IMPORTANT: All parameters are used as overrides to the existing user. Empty fields will override populated fields. Use carefully. @@ -405,7 +406,7 @@ def update( ), pswd=self._auth.management_key, ) - return response.json() + return futu_apply(response, lambda response: response.json()) def patch( self, @@ -425,7 +426,7 @@ def patch( sso_app_ids: Optional[List[str]] = None, status: Optional[str] = None, test: bool = False, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Patches an existing user with the given various fields. Only the given fields will be used to update the user. @@ -483,13 +484,13 @@ def patch( ), pswd=self._auth.management_key, ) - return response.json() + return futu_apply(response, lambda response: response.json()) def patch_batch( self, users: List[UserObj], test: bool = False, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Patch users in batch. Only the provided fields will be updated for each user. @@ -525,12 +526,12 @@ def patch_batch( User._compose_patch_batch_body(users, test), pswd=self._auth.management_key, ) - return response.json() + return futu_apply(response, lambda response: response.json()) def delete( self, login_id: str, - ): + ) -> Union[None, Awaitable[None]]: """ Delete an existing user. IMPORTANT: This action is irreversible. Use carefully. @@ -540,16 +541,17 @@ def delete( Raise: AuthException: raised if delete operation fails """ - self._auth.do_post( + response = self._auth.do_post( MgmtV1.user_delete_path, {"loginId": login_id}, pswd=self._auth.management_key, ) + return futu_apply(response, lambda response: None) def delete_by_user_id( self, user_id: str, - ): + ) -> Union[None, Awaitable[None]]: """ Delete an existing user by user ID. IMPORTANT: This action is irreversible. Use carefully. @@ -559,30 +561,32 @@ def delete_by_user_id( Raise: AuthException: raised if delete operation fails """ - self._auth.do_post( + response = self._auth.do_post( MgmtV1.user_delete_path, {"userId": user_id}, pswd=self._auth.management_key, ) + return futu_apply(response, lambda response: None) def delete_all_test_users( self, - ): + ) -> Union[None, Awaitable[None]]: """ Delete all test users in the project. IMPORTANT: This action is irreversible. Use carefully. Raise: AuthException: raised if delete operation fails """ - self._auth.do_delete( + response = self._auth.do_delete( MgmtV1.user_delete_all_test_users_path, pswd=self._auth.management_key, ) + return futu_apply(response, lambda response: None) def load( self, login_id: str, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Load an existing user. @@ -602,12 +606,12 @@ def load( params={"loginId": login_id}, pswd=self._auth.management_key, ) - return response.json() + return futu_apply(response, lambda response: response.json()) def load_by_user_id( self, user_id: str, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Load an existing user by user ID. The user ID can be found on the user's JWT. @@ -628,12 +632,12 @@ def load_by_user_id( params={"userId": user_id}, pswd=self._auth.management_key, ) - return response.json() + return futu_apply(response, lambda response: response.json()) def logout_user( self, login_id: str, - ): + ) -> Union[None, Awaitable[None]]: """ Logout a user from all devices. @@ -643,16 +647,17 @@ def logout_user( Raise: AuthException: raised if logout operation fails """ - self._auth.do_post( + response = self._auth.do_post( MgmtV1.user_logout_path, {"loginId": login_id}, pswd=self._auth.management_key, ) + return futu_apply(response, lambda response: None) def logout_user_by_user_id( self, user_id: str, - ): + ) -> Union[None, Awaitable[None]]: """ Logout a user from all devices. @@ -662,11 +667,12 @@ def logout_user_by_user_id( Raise: AuthException: raised if logout operation fails """ - self._auth.do_post( + response = self._auth.do_post( MgmtV1.user_logout_path, {"userId": user_id}, pswd=self._auth.management_key, ) + return futu_apply(response, lambda response: None) def search_all( self, @@ -691,7 +697,7 @@ def search_all( user_ids: Optional[List[str]] = None, tenant_role_ids: Optional[dict] = None, tenant_role_names: Optional[dict] = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Search all users. @@ -794,7 +800,7 @@ def search_all( body=body, pswd=self._auth.management_key, ) - return response.json() + return futu_apply(response, lambda response: response.json()) def search_all_test_users( self, @@ -816,7 +822,7 @@ def search_all_test_users( to_modified_time: Optional[int] = None, tenant_role_ids: Optional[dict] = None, tenant_role_names: Optional[dict] = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Search all test users. @@ -913,7 +919,7 @@ def search_all_test_users( body=body, pswd=self._auth.management_key, ) - return response.json() + return futu_apply(response, lambda response: response.json()) def get_provider_token( self, @@ -921,7 +927,7 @@ def get_provider_token( provider: str, withRefreshToken: Optional[bool] = False, forceRefresh: Optional[bool] = False, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Get the provider token for the given login ID. Only users that sign-in using social providers will have token. @@ -951,12 +957,12 @@ def get_provider_token( }, pswd=self._auth.management_key, ) - return response.json() + return futu_apply(response, lambda response: response.json()) def activate( self, login_id: str, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Activate an existing user. @@ -976,12 +982,12 @@ def activate( {"loginId": login_id, "status": "enabled"}, pswd=self._auth.management_key, ) - return response.json() + return futu_apply(response, lambda response: response.json()) def deactivate( self, login_id: str, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Deactivate an existing user. @@ -1001,13 +1007,13 @@ def deactivate( {"loginId": login_id, "status": "disabled"}, pswd=self._auth.management_key, ) - return response.json() + return futu_apply(response, lambda response: response.json()) def update_login_id( self, login_id: str, new_login_id: Optional[str] = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Update login id of user, leave new login empty to remove the ID. A user must have at least one login ID. Trying to remove the last one will fail. @@ -1029,14 +1035,14 @@ def update_login_id( {"loginId": login_id, "newLoginId": new_login_id}, pswd=self._auth.management_key, ) - return response.json() + return futu_apply(response, lambda response: response.json()) def update_email( self, login_id: str, email: Optional[str] = None, verified: Optional[bool] = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Update the email address for an existing user. @@ -1058,14 +1064,14 @@ def update_email( {"loginId": login_id, "email": email, "verified": verified}, pswd=self._auth.management_key, ) - return response.json() + return futu_apply(response, lambda response: response.json()) def update_phone( self, login_id: str, phone: Optional[str] = None, verified: Optional[bool] = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Update the phone number for an existing user. @@ -1087,7 +1093,7 @@ def update_phone( {"loginId": login_id, "phone": phone, "verified": verified}, pswd=self._auth.management_key, ) - return response.json() + return futu_apply(response, lambda response: response.json()) def update_display_name( self, @@ -1096,7 +1102,7 @@ def update_display_name( given_name: Optional[str] = None, middle_name: Optional[str] = None, family_name: Optional[str] = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Update the display name for an existing user. @@ -1126,13 +1132,13 @@ def update_display_name( bdy, pswd=self._auth.management_key, ) - return response.json() + return futu_apply(response, lambda response: response.json()) def update_picture( self, login_id: str, picture: Optional[str] = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Update the picture for an existing user. @@ -1153,11 +1159,11 @@ def update_picture( {"loginId": login_id, "picture": picture}, pswd=self._auth.management_key, ) - return response.json() + return futu_apply(response, lambda response: response.json()) def update_custom_attribute( self, login_id: str, attribute_key: str, attribute_val: Union[str, int, bool] - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Update a custom attribute of an existing user. @@ -1183,13 +1189,13 @@ def update_custom_attribute( }, pswd=self._auth.management_key, ) - return response.json() + return futu_apply(response, lambda response: response.json()) def set_roles( self, login_id: str, role_names: List[str], - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Set roles to a user without tenant association. Use set_tenant_roles for users that are part of a multi-tenant project. @@ -1211,13 +1217,13 @@ def set_roles( {"loginId": login_id, "roleNames": role_names}, pswd=self._auth.management_key, ) - return response.json() + return futu_apply(response, lambda response: response.json()) def add_roles( self, login_id: str, role_names: List[str], - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Add roles to a user without tenant association. Use add_tenant_roles for users that are part of a multi-tenant project. @@ -1239,13 +1245,13 @@ def add_roles( {"loginId": login_id, "roleNames": role_names}, pswd=self._auth.management_key, ) - return response.json() + return futu_apply(response, lambda response: response.json()) def remove_roles( self, login_id: str, role_names: List[str], - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Remove roles from a user without tenant association. Use remove_tenant_roles for users that are part of a multi-tenant project. @@ -1267,13 +1273,13 @@ def remove_roles( {"loginId": login_id, "roleNames": role_names}, pswd=self._auth.management_key, ) - return response.json() + return futu_apply(response, lambda response: response.json()) def set_sso_apps( self, login_id: str, sso_app_ids: List[str], - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Set SSO applications association to a user. @@ -1294,13 +1300,13 @@ def set_sso_apps( {"loginId": login_id, "ssoAppIds": sso_app_ids}, pswd=self._auth.management_key, ) - return response.json() + return futu_apply(response, lambda response: response.json()) def add_sso_apps( self, login_id: str, sso_app_ids: List[str], - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Add SSO applications association to a user. @@ -1321,13 +1327,13 @@ def add_sso_apps( {"loginId": login_id, "ssoAppIds": sso_app_ids}, pswd=self._auth.management_key, ) - return response.json() + return futu_apply(response, lambda response: response.json()) def remove_sso_apps( self, login_id: str, sso_app_ids: List[str], - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Remove SSO applications association from a user. @@ -1348,13 +1354,13 @@ def remove_sso_apps( {"loginId": login_id, "ssoAppIds": sso_app_ids}, pswd=self._auth.management_key, ) - return response.json() + return futu_apply(response, lambda response: response.json()) def add_tenant( self, login_id: str, tenant_id: str, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Add a tenant association to an existing user. @@ -1375,13 +1381,13 @@ def add_tenant( {"loginId": login_id, "tenantId": tenant_id}, pswd=self._auth.management_key, ) - return response.json() + return futu_apply(response, lambda response: response.json()) def remove_tenant( self, login_id: str, tenant_id: str, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Remove a tenant association from an existing user. @@ -1402,14 +1408,14 @@ def remove_tenant( {"loginId": login_id, "tenantId": tenant_id}, pswd=self._auth.management_key, ) - return response.json() + return futu_apply(response, lambda response: response.json()) def set_tenant_roles( self, login_id: str, tenant_id: str, role_names: List[str], - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Set roles to a user in a specific tenant. @@ -1431,14 +1437,14 @@ def set_tenant_roles( {"loginId": login_id, "tenantId": tenant_id, "roleNames": role_names}, pswd=self._auth.management_key, ) - return response.json() + return futu_apply(response, lambda response: response.json()) def add_tenant_roles( self, login_id: str, tenant_id: str, role_names: List[str], - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Add roles to a user in a specific tenant. @@ -1460,14 +1466,14 @@ def add_tenant_roles( {"loginId": login_id, "tenantId": tenant_id, "roleNames": role_names}, pswd=self._auth.management_key, ) - return response.json() + return futu_apply(response, lambda response: response.json()) def remove_tenant_roles( self, login_id: str, tenant_id: str, role_names: List[str], - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Remove roles from a user in a specific tenant. @@ -1489,13 +1495,13 @@ def remove_tenant_roles( {"loginId": login_id, "tenantId": tenant_id, "roleNames": role_names}, pswd=self._auth.management_key, ) - return response.json() + return futu_apply(response, lambda response: response.json()) def set_temporary_password( self, login_id: str, password: str, - ) -> None: + ) -> Union[None, Awaitable[None]]: """ Set the temporary password for the given login ID. Note: The password will automatically be set as expired. @@ -1509,7 +1515,7 @@ def set_temporary_password( Raise: AuthException: raised if the operation fails """ - self._auth.do_post( + response = self._auth.do_post( MgmtV1.user_set_temporary_password_path, { "loginId": login_id, @@ -1518,13 +1524,13 @@ def set_temporary_password( }, pswd=self._auth.management_key, ) - return + return futu_apply(response, lambda response: None) def set_active_password( self, login_id: str, password: str, - ) -> None: + ) -> Union[None, Awaitable[None]]: """ Set the password for the given login ID. @@ -1535,7 +1541,7 @@ def set_active_password( Raise: AuthException: raised if the operation fails """ - self._auth.do_post( + response = self._auth.do_post( MgmtV1.user_set_active_password_path, { "loginId": login_id, @@ -1544,7 +1550,7 @@ def set_active_password( }, pswd=self._auth.management_key, ) - return + return futu_apply(response, lambda response: None) # Deprecated (use set_temporary_password instead) def set_password( @@ -1552,7 +1558,7 @@ def set_password( login_id: str, password: str, set_active: Optional[bool] = False, - ) -> None: + ) -> Union[None, Awaitable[None]]: """ Set the password for the given login ID. Note: The password will automatically be set as expired unless the set_active flag will be set to True, @@ -1567,7 +1573,7 @@ def set_password( Raise: AuthException: raised if the operation fails """ - self._auth.do_post( + response = self._auth.do_post( MgmtV1.user_set_password_path, { "loginId": login_id, @@ -1576,12 +1582,12 @@ def set_password( }, pswd=self._auth.management_key, ) - return + return futu_apply(response, lambda response: None) def expire_password( self, login_id: str, - ) -> None: + ) -> Union[None, Awaitable[None]]: """ Expires the password for the given login ID. Note: user sign-in with an expired password, the user will get an error with code. @@ -1593,17 +1599,17 @@ def expire_password( Raise: AuthException: raised if the operation fails """ - self._auth.do_post( + response = self._auth.do_post( MgmtV1.user_expire_password_path, {"loginId": login_id}, pswd=self._auth.management_key, ) - return + return futu_apply(response, lambda response: None) def remove_all_passkeys( self, login_id: str, - ) -> None: + ) -> Union[None, Awaitable[None]]: """ Removes all registered passkeys (WebAuthn devices) for the user with the given login ID. Note: The user might not be able to login anymore if they have no other authentication @@ -1615,17 +1621,17 @@ def remove_all_passkeys( Raise: AuthException: raised if the operation fails """ - self._auth.do_post( + response = self._auth.do_post( MgmtV1.user_remove_all_passkeys_path, {"loginId": login_id}, pswd=self._auth.management_key, ) - return + return futu_apply(response, lambda response: None) def remove_totp_seed( self, login_id: str, - ) -> None: + ) -> Union[None, Awaitable[None]]: """ Removes TOTP seed for the user with the given login ID. Note: The user might not be able to login anymore if they have no other authentication @@ -1637,19 +1643,19 @@ def remove_totp_seed( Raise: AuthException: raised if the operation fails """ - self._auth.do_post( + response = self._auth.do_post( MgmtV1.user_remove_totp_seed_path, {"loginId": login_id}, pswd=self._auth.management_key, ) - return + return futu_apply(response, lambda response: None) def generate_otp_for_test_user( self, method: DeliveryMethod, login_id: str, login_options: Optional[LoginOptions] = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Generate OTP for the given login ID of a test user. This is useful when running tests and don't want to use 3rd party messaging services. @@ -1677,7 +1683,7 @@ def generate_otp_for_test_user( }, pswd=self._auth.management_key, ) - return response.json() + return futu_apply(response, lambda response: response.json()) def generate_magic_link_for_test_user( self, @@ -1685,7 +1691,7 @@ def generate_magic_link_for_test_user( login_id: str, uri: str, login_options: Optional[LoginOptions] = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Generate Magic Link for the given login ID of a test user. This is useful when running tests and don't want to use 3rd party messaging services. @@ -1715,14 +1721,14 @@ def generate_magic_link_for_test_user( }, pswd=self._auth.management_key, ) - return response.json() + return futu_apply(response, lambda response: response.json()) def generate_enchanted_link_for_test_user( self, login_id: str, uri: str, login_options: Optional[LoginOptions] = None, - ) -> dict: + ) -> Union[dict, Awaitable[dict]]: """ Generate Enchanted Link for the given login ID of a test user. This is useful when running tests and don't want to use 3rd party messaging services. @@ -1749,11 +1755,11 @@ def generate_enchanted_link_for_test_user( }, pswd=self._auth.management_key, ) - return response.json() + return futu_apply(response, lambda response: response.json()) def generate_embedded_link( self, login_id: str, custom_claims: Optional[dict] = None, timeout: int = 0 - ) -> str: + ) -> Union[str, Awaitable[str]]: """ Generate Embedded Link for the given user login ID. The return value is a token that can be verified via magic link, or using flows @@ -1773,7 +1779,7 @@ def generate_embedded_link( {"loginId": login_id, "customClaims": custom_claims, "timeout": timeout}, pswd=self._auth.management_key, ) - return response.json()["token"] + return futu_apply(response, lambda response: response.json()["token"]) def generate_sign_up_embedded_link( self, @@ -1783,7 +1789,7 @@ def generate_sign_up_embedded_link( phone_verified: bool = False, login_options: Optional[LoginOptions] = None, timeout: int = 0, - ) -> str: + ) -> Union[str, Awaitable[str]]: """ Generate sign up Embedded Link for the given user login ID. The return value is a token that can be verified via magic link, or using flows @@ -1814,9 +1820,9 @@ def generate_sign_up_embedded_link( }, pswd=self._auth.management_key, ) - return response.json()["token"] + return futu_apply(response, lambda response: response.json()["token"]) - def history(self, user_ids: List[str]) -> List[dict]: + def history(self, user_ids: List[str]) -> Union[List[dict], Awaitable[List[dict]]]: """ Retrieve users' authentication history, by the given user's ids. @@ -1843,7 +1849,7 @@ def history(self, user_ids: List[str]) -> List[dict]: user_ids, pswd=self._auth.management_key, ) - return response.json() + return futu_apply(response, lambda response: response.json()) @staticmethod def _compose_create_body( diff --git a/tests/test_auth.py b/tests/test_auth.py index 2ed8e5239..f569c25b4 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -115,18 +115,18 @@ def test_fetch_public_key(self): # Test failed flows with patch("httpx.get") as mock_get: mock_get.return_value.is_success = False - self.assertRaises(AuthException, auth._fetch_public_keys) + self.assertRaises(AuthException, auth._fetch_public_keys_sync) with patch("httpx.get") as mock_get: mock_get.return_value.is_success = True mock_get.return_value.text = "invalid json" - self.assertRaises(AuthException, auth._fetch_public_keys) + self.assertRaises(AuthException, auth._fetch_public_keys_sync) # test success flow with patch("httpx.get") as mock_get: mock_get.return_value.is_success = True mock_get.return_value.text = valid_keys_response - self.assertIsNone(auth._fetch_public_keys()) + self.assertIsNone(auth._fetch_public_keys_sync()) def test_project_id_from_env(self): os.environ["DESCOPE_PROJECT_ID"] = self.dummy_project_id @@ -656,7 +656,7 @@ def test_api_rate_limit_exception(self): timeout=DEFAULT_TIMEOUT_SECONDS, ) - # Test _fetch_public_keys rate limit + # Test _fetch_public_keys_sync rate limit with patch("httpx.get") as mock_request: mock_request.return_value.is_success = False mock_request.return_value.status_code = 429 @@ -669,7 +669,7 @@ def test_api_rate_limit_exception(self): API_RATE_LIMIT_RETRY_AFTER_HEADER: "10" } with self.assertRaises(RateLimitException) as cm: - auth._fetch_public_keys() + auth._fetch_public_keys_sync() the_exception = cm.exception self.assertEqual(the_exception.status_code, "E130429") self.assertEqual(the_exception.error_type, ERROR_TYPE_API_RATE_LIMIT) From 11d953205976a98ec306bb7fedf2b8ee6ee7bbf6 Mon Sep 17 00:00:00 2001 From: mellowCaribou Date: Sun, 5 Oct 2025 12:44:09 +0300 Subject: [PATCH 2/4] reduce temporarily and add test --- pyproject.toml | 2 +- tests/test_future_utils.py | 206 +++++++++++++++++++++++++++++++++++++ 2 files changed, 207 insertions(+), 1 deletion(-) create mode 100644 tests/test_future_utils.py diff --git a/pyproject.toml b/pyproject.toml index f7dd694b9..9bee08464 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -78,7 +78,7 @@ omit = ["descope/flask/*"] [tool.coverage.report] -fail_under = 98 +fail_under = 97 skip_covered = true skip_empty = true diff --git a/tests/test_future_utils.py b/tests/test_future_utils.py new file mode 100644 index 000000000..801384812 --- /dev/null +++ b/tests/test_future_utils.py @@ -0,0 +1,206 @@ +import asyncio +import unittest +from unittest.mock import Mock, patch + +from descope.future_utils import futu_apply, futu_await, futu_awaitable + + +class TestFutureUtils(unittest.TestCase): + def test_futu_apply_with_sync_result(self): + """Test futu_apply with synchronous result""" + result = "test_result" + modifier = lambda x: f"modified_{x}" + + result = futu_apply(result, modifier) + self.assertEqual(result, "modified_test_result") + + def test_futu_apply_with_coroutine(self): + """Test futu_apply with coroutine""" + + async def async_func(): + return "async_result" + + modifier = lambda x: f"modified_{x}" + + result = futu_apply(async_func(), modifier) + self.assertTrue(asyncio.iscoroutine(result)) + + # Test the actual result + async def run_test(): + actual_result = await result + self.assertEqual(actual_result, "modified_async_result") + + asyncio.run(run_test()) + + def test_futu_apply_with_future(self): + """Test futu_apply with future""" + + async def run_test(): + future = asyncio.Future() + future.set_result("future_result") + + modifier = lambda x: f"modified_{x}" + + result = futu_apply(future, modifier) + self.assertTrue(asyncio.iscoroutine(result)) + + # Test the actual result + actual_result = await result + self.assertEqual(actual_result, "modified_future_result") + + asyncio.run(run_test()) + + def test_futu_awaitable_with_false(self): + """Test futu_awaitable with as_awaitable=False""" + result = "test_result" + awaitable_result = futu_awaitable(result, False) + + self.assertEqual(awaitable_result, "test_result") + self.assertFalse(asyncio.iscoroutine(awaitable_result)) + + def test_futu_awaitable_with_true(self): + """Test futu_awaitable with as_awaitable=True""" + result = "test_result" + awaitable_result = futu_awaitable(result, True) + + self.assertTrue(asyncio.iscoroutine(awaitable_result)) + + # Test the actual result + async def run_test(): + actual_result = await awaitable_result + self.assertEqual(actual_result, "test_result") + + asyncio.run(run_test()) + + def test_futu_await_with_sync_object(self): + """Test futu_await with synchronous object""" + obj = "sync_object" + + async def run_test(): + result = await futu_await(obj) + self.assertEqual(result, "sync_object") + + asyncio.run(run_test()) + + def test_futu_await_with_coroutine(self): + """Test futu_await with coroutine""" + + async def async_func(): + return "coroutine_result" + + async def run_test(): + result = await futu_await(async_func()) + self.assertEqual(result, "coroutine_result") + + asyncio.run(run_test()) + + def test_futu_await_with_future(self): + """Test futu_await with future""" + + async def run_test(): + future = asyncio.Future() + future.set_result("future_result") + + result = await futu_await(future) + self.assertEqual(result, "future_result") + + asyncio.run(run_test()) + + def test_futu_apply_with_complex_modifier(self): + """Test futu_apply with complex modifier function""" + result = {"key": "value"} + modifier = lambda x: {**x, "modified": True} + + result = futu_apply(result, modifier) + expected = {"key": "value", "modified": True} + self.assertEqual(result, expected) + + def test_futu_apply_with_async_modifier(self): + """Test futu_apply with async modifier""" + + async def async_func(): + return "async_result" + + def modifier(x): + return f"modified_{x}" + + result = futu_apply(async_func(), modifier) + + async def run_test(): + actual_result = await result + self.assertEqual(actual_result, "modified_async_result") + + asyncio.run(run_test()) + + def test_futu_awaitable_with_none_result(self): + """Test futu_awaitable with None result""" + result = None + awaitable_result = futu_awaitable(result, True) + + self.assertTrue(asyncio.iscoroutine(awaitable_result)) + + async def run_test(): + actual_result = await awaitable_result + self.assertIsNone(actual_result) + + asyncio.run(run_test()) + + def test_futu_await_with_none_object(self): + """Test futu_await with None object""" + + async def run_test(): + result = await futu_await(None) + self.assertIsNone(result) + + asyncio.run(run_test()) + + def test_futu_apply_with_exception_in_modifier(self): + """Test futu_apply when modifier raises exception""" + result = "test_result" + + def modifier(x): + raise ValueError("Test exception") + + with self.assertRaises(ValueError): + futu_apply(result, modifier) + + def test_futu_apply_with_exception_in_async_modifier(self): + """Test futu_apply when async modifier raises exception""" + + async def async_func(): + return "async_result" + + def modifier(x): + raise ValueError("Test exception") + + result = futu_apply(async_func(), modifier) + + async def run_test(): + with self.assertRaises(ValueError): + await result + + asyncio.run(run_test()) + + def test_futu_await_with_exception_in_coroutine(self): + """Test futu_await when coroutine raises exception""" + + async def async_func(): + raise ValueError("Test exception") + + async def run_test(): + with self.assertRaises(ValueError): + await futu_await(async_func()) + + asyncio.run(run_test()) + + def test_futu_awaitable_with_false_and_none(self): + """Test futu_awaitable with as_awaitable=False and None result""" + result = None + awaitable_result = futu_awaitable(result, False) + + self.assertIsNone(awaitable_result) + self.assertFalse(asyncio.iscoroutine(awaitable_result)) + + +if __name__ == "__main__": + unittest.main() From f7e1f27084e6ec459975af76c63b760fc7c43a3b Mon Sep 17 00:00:00 2001 From: mellowCaribou Date: Thu, 9 Oct 2025 09:37:54 +0300 Subject: [PATCH 3/4] fix types in docstring return --- descope/authmethod/password.py | 10 ++-- descope/authmethod/sso.py | 2 +- descope/authmethod/totp.py | 6 +- descope/descope_client.py | 26 ++++---- descope/management/access_key.py | 6 +- descope/management/audit.py | 2 +- descope/management/authz.py | 16 ++--- descope/management/fga.py | 2 +- descope/management/flow.py | 10 ++-- descope/management/group.py | 6 +- descope/management/jwt.py | 6 +- descope/management/outbound_application.py | 24 ++++---- descope/management/permission.py | 2 +- descope/management/project.py | 6 +- descope/management/role.py | 4 +- descope/management/sso_application.py | 8 +-- descope/management/sso_settings.py | 4 +- descope/management/tenant.py | 8 +-- descope/management/user.py | 70 +++++++++++----------- 19 files changed, 109 insertions(+), 109 deletions(-) diff --git a/descope/authmethod/password.py b/descope/authmethod/password.py index d12f74697..3a5451bd2 100644 --- a/descope/authmethod/password.py +++ b/descope/authmethod/password.py @@ -26,7 +26,7 @@ def sign_up( user (dict) optional: Preserve additional user metadata in the form of {"name": "Desmond Copeland", "phone": "2125551212", "email": "des@cope.com"} - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"jwts": [], "user": "", "firstSeen": "", "error": ""} Includes all the jwts tokens (session token, refresh token), token claims, and user information @@ -71,7 +71,7 @@ def sign_in( login_id (str): The login ID of the user being validated password (str): The password to be validated - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"jwts": [], "user": "", "firstSeen": "", "error": ""} Includes all the jwts tokens (session token, refresh token), token claims, and user information @@ -119,7 +119,7 @@ def send_reset( if those are the chosen reset methods. See the Magic Link and Enchanted Link sections for more details. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"resetMethod": "", "pendingRef": "", "linkId": "", "maskedEmail": ""} The contents will differ according to the chosen reset method. 'pendingRef' @@ -202,7 +202,7 @@ def replace( old_password (str): The user's current active password new_password (str): The new password to use - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"jwts": [], "user": "", "firstSeen": false, "error": ""} Includes all the jwts tokens (session token, refresh token), token claims, and user information @@ -250,7 +250,7 @@ def get_policy(self) -> Union[dict, Awaitable[dict]]: Get a subset of the password policy defined in the Descope console and enforced by Descope. The goal is to enable client-side validations to give users a better UX - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"minLength": 8, "lowercase": true, "uppercase": true, "number": true, "nonAlphanumeric": true} minLength - the minimum length of a password diff --git a/descope/authmethod/sso.py b/descope/authmethod/sso.py index 9d3e4fd21..354aa9ac2 100644 --- a/descope/authmethod/sso.py +++ b/descope/authmethod/sso.py @@ -19,7 +19,7 @@ def start( """ Start tenant sso session (saml/oidc based on tenant settings) - Return value (dict): the redirect url for the login page + Return value (Union[dict, Awaitable[dict]]): the redirect url for the login page Return dict in the format {'url': 'http://dummy.com/login..'} """ diff --git a/descope/authmethod/totp.py b/descope/authmethod/totp.py index 8ff35e616..ed8170fd3 100644 --- a/descope/authmethod/totp.py +++ b/descope/authmethod/totp.py @@ -24,7 +24,7 @@ def sign_up( user (dict) optional: Preserve additional user metadata in the form of, {"name": "Desmond Copeland", "phone": "2125551212", "email": "des@cope.com"} - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"provisioningURL": "", "image": "", "key": ""} Includes 3 different ways to allow the user to save their credentials in @@ -63,7 +63,7 @@ def sign_in_code( login_options (LoginOptions): Optional advanced controls over login parameters refresh_token: Optional refresh token is needed for specific login options - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"jwts": [], "user": "", "firstSeen": "", "error": ""} Includes all the jwts tokens (session token, refresh token), token claims, and user information @@ -107,7 +107,7 @@ def update_user( login_id (str): The login ID of the user whose information is being updated refresh_token (str): The session's refresh token (used for verification) - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"provisioningURL": "", "image": "", "key": ""} Includes 3 different ways to allow the user to save their credentials in diff --git a/descope/descope_client.py b/descope/descope_client.py index c55e5e9a6..9e0153089 100644 --- a/descope/descope_client.py +++ b/descope/descope_client.py @@ -125,7 +125,7 @@ def get_matched_permissions( jwt_response (dict): The jwt_response object which includes all JWT claims information permissions (List[str]): List of permissions to validate for this jwt_response - Return value (List[str]): returns the list of permissions that are granted + Return value (list[str]): returns the list of permissions that are granted """ return self.get_matched_tenant_permissions(jwt_response, "", permissions) @@ -177,7 +177,7 @@ def get_matched_tenant_permissions( tenant (str): TenantId permissions (List[str]): List of permissions to validate for this jwt_response - Return value (List[str]): returns the list of permissions that are granted + Return value (list[str]): returns the list of permissions that are granted """ if not jwt_response: return [] @@ -224,7 +224,7 @@ def get_matched_roles(self, jwt_response: dict, roles: list[str]) -> list[str]: jwt_response (dict): The jwt_response object which includes all JWT claims information roles (List[str]): List of roles to validate for this jwt_response - Return value (List[str]): returns the list of roles that are granted + Return value (list[str]): returns the list of roles that are granted """ return self.get_matched_tenant_roles(jwt_response, "", roles) @@ -274,7 +274,7 @@ def get_matched_tenant_roles( tenant (str): TenantId roles (List[str]): List of roles to validate for this jwt_response - Return value (List[str]): returns the list of roles that are granted + Return value (list[str]): returns the list of roles that are granted """ if not jwt_response: return [] @@ -312,7 +312,7 @@ def validate_session( session_token (str): The session token to be validated audience (str|Iterable[str]|None): Optional recipients that the JWT is intended for (must be equal to the 'aud' claim on the provided token) - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict includes the session token and all JWT claims Raise: @@ -330,7 +330,7 @@ def refresh_session( refresh_token (str): The refresh token that will be used to refresh the session audience (str|Iterable[str]|None): Optional recipients that the JWT is intended for (must be equal to the 'aud' claim on the provided token) - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict includes the session token, refresh token, and all JWT claims Raise: @@ -355,7 +355,7 @@ def validate_and_refresh_session( refresh_token (str): The refresh token that will be used to refresh the session token, if needed audience (str|Iterable[str]|None): Optional recipients that the JWT is intended for (must be equal to the 'aud' claim on the provided token) - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict includes the session token, refresh token, and all JWT claims Raise: @@ -375,7 +375,7 @@ def logout( Args: refresh_token (str): The refresh token - Return value (httpx.Response): returns the response from the Descope server + Return value (Union[httpx.Response, Awaitable[httpx.Response]]): returns the response from the Descope server Raise: AuthException: Exception is raised if session is not authorized or another error occurs @@ -401,7 +401,7 @@ def logout_all( Args: refresh_token (str): The refresh token - Return value (httpx.Response): returns the response from the Descope server + Return value (Union[httpx.Response, Awaitable[httpx.Response]]): returns the response from the Descope server Raise: AuthException: Exception is raised if session is not authorized or another error occurs @@ -425,7 +425,7 @@ def me(self, refresh_token: str) -> Union[dict, Awaitable[dict]]: Args: refresh_token (str): The refresh token - Return value (dict): returns the user details from the server + Return value (Union[dict, Awaitable[dict]]): returns the user details from the server (email:str, name:str, phone:str, loginIds[str], verifiedEmail:bool, verifiedPhone:bool) Raise: @@ -458,7 +458,7 @@ def my_tenants( ids (List[str]): Get the list of tenants refresh_token (str): The refresh token - Return value (dict): returns the tenant requested from the server + Return value (Union[dict, Awaitable[dict]]): returns the tenant requested from the server (id:str, name:str, customAttributes:dict) Raise: @@ -498,7 +498,7 @@ def history(self, refresh_token: str) -> Union[list[dict], Awaitable[list[dict]] Args: refresh_token (str): The refresh token - Return value (List[dict]): + Return value (Union[list[dict], Awaitable[list[dict]]]): Return List in the format [ { @@ -564,7 +564,7 @@ def select_tenant( refresh_token (str): The refresh token that will be used to refresh the session token, if needed tenant_id (str): The tenant id to place on JWT - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict includes the session token, refresh token, with the tenant id on the jwt Raise: diff --git a/descope/management/access_key.py b/descope/management/access_key.py index e82edbbcf..e5cdba7bc 100644 --- a/descope/management/access_key.py +++ b/descope/management/access_key.py @@ -37,7 +37,7 @@ def create( description (str): an optional text the access key can hold. permitted_ips: (List[str]): An optional list of IP addresses or CIDR ranges that are allowed to use the access key. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format { "key": {}, @@ -78,7 +78,7 @@ def load( Args: id (str): The id of the access key to be loaded. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"key": {}} Containing the loaded access key information. @@ -103,7 +103,7 @@ def search_all_access_keys( Args: tenant_ids (List[str]): Optional list of tenant IDs to filter by - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"keys": []} "keys" contains a list of all of the found users and their information diff --git a/descope/management/audit.py b/descope/management/audit.py index 3e5e2ae0a..daabfad8c 100644 --- a/descope/management/audit.py +++ b/descope/management/audit.py @@ -41,7 +41,7 @@ def search( from_ts (datetime): Retrieve records newer than given time but not older than 30 days to_ts (datetime): Retrieve records older than given time - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format { "audits": [ diff --git a/descope/management/authz.py b/descope/management/authz.py index 2098f4281..0ef703781 100644 --- a/descope/management/authz.py +++ b/descope/management/authz.py @@ -66,7 +66,7 @@ def delete_schema(self) -> Union[None, Awaitable[None]]: def load_schema(self) -> Union[dict, Awaitable[dict]]: """ Load the schema for the project - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format of schema as above (see save_schema) Raise: AuthException: raised if load schema fails @@ -277,7 +277,7 @@ def has_relations( "target": "the target that has the relation - usually users or other resources" } - Return value (List[dict]): + Return value (Union[List[dict], Awaitable[List[dict]]]): Return List in the format [ { @@ -310,7 +310,7 @@ def who_can_access( relation_definition (str): the RD we are checking namespace (str): the namespace for the RD - Return value (List[str]): list of targets (user IDs usually that have the access) + Return value (Union[List[dict], Awaitable[List[dict]]]): list of targets (user IDs usually that have the access) Raise: AuthException: raised if query fails """ @@ -333,7 +333,7 @@ def resource_relations( Args: resource (str): the resource we are listing relations for - Return value (List[dict]): + Return value (Union[List[dict], Awaitable[List[dict]]]): Return List of relations each in the format of a relation as documented in create_relations Raise: AuthException: raised if query fails @@ -353,7 +353,7 @@ def targets_relations( Args: targets (List[str]): the list of targets we are returning the relations for - Return value (List[dict]): + Return value (Union[List[dict], Awaitable[List[dict]]]): Return List of relations each in the format of a relation as documented in create_relations Raise: AuthException: raised if query fails @@ -373,7 +373,7 @@ def what_can_target_access( Args: target (str): the target we are returning the relations for - Return value (List[dict]): + Return value (Union[List[dict], Awaitable[List[dict]]]): Return List of relations each in the format of a relation as documented in create_relations Raise: AuthException: raised if query fails @@ -395,7 +395,7 @@ def what_can_target_access_with_relation( relation_definition (str): the RD we are checking namespace (str): the namespace for the RD - Return value (List[dict]): + Return value (Union[List[dict], Awaitable[List[dict]]]): Return List of relations each in the format of a relation as documented in create_relations Raise: AuthException: raised if query fails @@ -419,7 +419,7 @@ def get_modified( Args: since (datetime): only return changes from this given datetime - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Dict including "resources" list of strings, "targets" list of strings and "schemaChanged" bool Raise: AuthException: raised if query fails diff --git a/descope/management/fga.py b/descope/management/fga.py index efd6345f0..1b9f40a1e 100644 --- a/descope/management/fga.py +++ b/descope/management/fga.py @@ -114,7 +114,7 @@ def check( "targetType": "the type of the target (namespace)" } - Return value (List[dict]): + Return value (Union[List[dict], Awaitable[List[dict]]]): Return List in the format [ { diff --git a/descope/management/flow.py b/descope/management/flow.py index a318ac6ff..9ef32540c 100644 --- a/descope/management/flow.py +++ b/descope/management/flow.py @@ -12,7 +12,7 @@ def list_flows( """ List all project flows - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format { "flows": [{"id": "", "name": "", "description": "", "disabled": False}], total: number} @@ -64,7 +64,7 @@ def export_flow( Args: flow_id (str): the flow id to export. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format { "flow": {"id": "", "name": "", "description": "", "disabled": False, "etag": "", "dsl": {}}, screens: [{ "id": "", "inputs": [], "interactions": [] }] } @@ -100,7 +100,7 @@ def import_flow( screens (List[dict]): the flow screens to import. list of dictss in the format: { "id": "", "inputs": [], "interactions": [] } - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format { "flow": {"id": "", "name": "", "description": "", "disabled": False, "etag": "", "dsl": {}}, screens: [{ "id": "", "inputs": [], "interactions": [] }] } @@ -127,7 +127,7 @@ def export_theme( """ Export the current project theme. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"id": "", "cssTemplate": {} } @@ -156,7 +156,7 @@ def import_theme( theme (Theme): the theme to import. dict in the format {"id": "", "cssTemplate": {} } - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"id": "", "cssTemplate": {} } diff --git a/descope/management/group.py b/descope/management/group.py index c73c7562d..d86e9005f 100644 --- a/descope/management/group.py +++ b/descope/management/group.py @@ -16,7 +16,7 @@ def load_all_groups( Args: tenant_id (str): Tenant ID to load groups from. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format [ { @@ -62,7 +62,7 @@ def load_all_groups_for_members( user_ids (List[str]): Optional List of user IDs, with the format of "U2J5ES9S8TkvCgOvcrkpzUgVTEBM" (example), which can be found on the user's JWT. login_ids (List[str]): Optional List of login IDs, how the users identify when logging in. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format [ { @@ -111,7 +111,7 @@ def load_all_group_members( tenant_id (str): Tenant ID to load groups from. group_id (str): Group ID to load members for. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format [ { diff --git a/descope/management/jwt.py b/descope/management/jwt.py index 5761c00da..4fd21e731 100644 --- a/descope/management/jwt.py +++ b/descope/management/jwt.py @@ -24,7 +24,7 @@ def update_jwt( custom_claims (dict): Custom claims to add to JWT, system claims will be filtered out refresh_duration (int): duration in seconds for which the new JWT will be valid - Return value (str): the newly updated JWT + Return value (Union[str, Awaitable[str]]): the newly updated JWT Raise: AuthException: raised if update failed @@ -65,7 +65,7 @@ def impersonate( tenant_id (str): tenant id to set on DCT claim. refresh_duration (int): duration in seconds for which the new JWT will be valid - Return value (str): A JWT of the impersonated user + Return value (Union[str, Awaitable[str]]): A JWT of the impersonated user Raise: AuthException: raised if update failed @@ -110,7 +110,7 @@ def stop_impersonation( tenant_id (str): tenant id to set on DCT claim. refresh_duration (int): duration in seconds for which the new JWT will be valid - Return value (str): A JWT of the actor + Return value (Union[str, Awaitable[str]]): A JWT of the actor Raise: AuthException: raised if update failed diff --git a/descope/management/outbound_application.py b/descope/management/outbound_application.py index 5f9dc6d1d..71fbf4a6b 100644 --- a/descope/management/outbound_application.py +++ b/descope/management/outbound_application.py @@ -155,7 +155,7 @@ def create_application( access_type (AccessType): Optional OAuth access type. prompt (List[PromptType]): Optional OAuth prompt parameters. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"app": {"id": , "name": , "description": , "logo": }} @@ -234,7 +234,7 @@ def update_application( access_type (AccessType): Optional OAuth access type. prompt (List[PromptType]): Optional OAuth prompt parameters. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"app": {"id": , "name": , "description": , "logo": }} @@ -297,7 +297,7 @@ def load_application( Args: id (str): The ID of the outbound application to load. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"app": {"id": , "name": , "description": , "logo": }} Containing the loaded outbound application information. @@ -317,7 +317,7 @@ def load_all_applications( """ Load all outbound applications. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"apps": [{"id": , "name": , "description": , "logo": }, ...]} Containing the loaded outbound applications information. @@ -349,7 +349,7 @@ def fetch_token_by_scopes( options (dict): Optional token options. tenant_id (str): Optional tenant ID. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"token": {"token": , "refreshToken": , "expiresIn": , "tokenType": , "scopes": }} @@ -386,7 +386,7 @@ def fetch_token( tenant_id (str): Optional tenant ID. options (dict): Optional token options. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"token": {"token": , "refreshToken": , "expiresIn": , "tokenType": , "scopes": }} @@ -422,7 +422,7 @@ def fetch_tenant_token_by_scopes( scopes (List[str]): List of scopes to include in the token. options (dict): Optional token options. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"token": {"token": , "refreshToken": , "expiresIn": , "tokenType": , "scopes": }} @@ -456,7 +456,7 @@ def fetch_tenant_token( tenant_id (str): The ID of the tenant. options (dict): Optional token options. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"token": {"token": , "refreshToken": , "expiresIn": , "tokenType": , "scopes": }} @@ -569,7 +569,7 @@ def fetch_token_by_scopes( options (dict): Optional token options. tenant_id (str): Optional tenant ID. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"token": {"token": , "refreshToken": , "expiresIn": , "tokenType": , "scopes": }} @@ -599,7 +599,7 @@ def fetch_token( tenant_id (str): Optional tenant ID. options (dict): Optional token options. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"token": {"token": , "refreshToken": , "expiresIn": , "tokenType": , "scopes": }} @@ -629,7 +629,7 @@ def fetch_tenant_token_by_scopes( scopes (List[str]): List of scopes to include in the token. options (dict): Optional token options. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"token": {"token": , "refreshToken": , "expiresIn": , "tokenType": , "scopes": }} @@ -653,7 +653,7 @@ def fetch_tenant_token( tenant_id (str): The ID of the tenant. options (dict): Optional token options. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"token": {"token": , "refreshToken": , "expiresIn": , "tokenType": , "scopes": }} diff --git a/descope/management/permission.py b/descope/management/permission.py index 0cb006d68..269a3020f 100644 --- a/descope/management/permission.py +++ b/descope/management/permission.py @@ -79,7 +79,7 @@ def load_all( """ Load all permissions. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"permissions": [{"name": , "description": , "systemDefault":}]} Containing the loaded permission information. diff --git a/descope/management/project.py b/descope/management/project.py index cdc3ddd68..2ca608e91 100644 --- a/descope/management/project.py +++ b/descope/management/project.py @@ -60,7 +60,7 @@ def list_projects( """ List of all the projects in the company. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"projects": []} "projects" contains a list of all of the projects and their information @@ -104,7 +104,7 @@ def clone( environment (str): Optional state for the project. Currently, only the "production" tag is supported. tags(list[str]): Optional free text tags. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict Containing the new project details (name, id, environment and tag). Raise: @@ -134,7 +134,7 @@ def export_project( - Users, tenants and access keys are not cloned. - Secrets, keys and tokens are not stripped from the exported data. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict Containing the exported JSON files payload. Raise: diff --git a/descope/management/role.py b/descope/management/role.py index 956778008..017d78265 100644 --- a/descope/management/role.py +++ b/descope/management/role.py @@ -108,7 +108,7 @@ def load_all( """ Load all roles. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"roles": [{"name": , "description": , "permissionNames":[]}] } Containing the loaded role information. @@ -139,7 +139,7 @@ def search( role_name_like (str): Return roles that contain the given string ignoring case permission_names (List[str]): Only return roles that have the given permissions - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"roles": [{"name": , "description": , "permissionNames":[]}] } Containing the loaded role information. diff --git a/descope/management/sso_application.py b/descope/management/sso_application.py index 3867b892d..1253963e2 100644 --- a/descope/management/sso_application.py +++ b/descope/management/sso_application.py @@ -35,7 +35,7 @@ def create_oidc_application( enabled (bool): Optional (default True) does the sso application will be enabled or disabled. force_authentication (bool): Optional determine if the IdP should force the user to re-authenticate. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"id": } @@ -105,7 +105,7 @@ def create_saml_application( force_authentication (bool): Optional determine if the IdP should force the user to re-authenticate. logout_redirect_url (str): Optional Target URL to which the user will be redirected upon logout completion. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"id": } @@ -320,7 +320,7 @@ def load( Args: id (str): The ID of the sso application to load. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"id":"","name":"","description":"","enabled":true,"logo":"","appType":"saml","samlSettings":{"loginPageUrl":"","idpCert":"","useMetadataInfo":true,"metadataUrl":"","entityId":"","acsUrl":"","certificate":"","attributeMapping":[{"name":"email","type":"","value":"attrVal1"}],"groupsMapping":[{"name":"grp1","type":"","filterType":"roles","value":"","roles":[{"id":"myRoleId","name":"myRole"}]}],"idpMetadataUrl":"","idpEntityId":"","idpSsoUrl":"","acsAllowedCallbacks":[],"subjectNameIdType":"","subjectNameIdFormat":"", "defaultRelayState":"", "forceAuthentication": false, "idpLogoutUrl": "", "logoutRedirectUrl": ""},"oidcSettings":{"loginPageUrl":"","issuer":"","discoveryUrl":"", "forceAuthentication":false}} Containing the loaded sso application information. @@ -341,7 +341,7 @@ def load_all( """ Load all sso applications. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format { "apps": [ diff --git a/descope/management/sso_settings.py b/descope/management/sso_settings.py index 82c75de1d..12f4b0d7f 100644 --- a/descope/management/sso_settings.py +++ b/descope/management/sso_settings.py @@ -172,7 +172,7 @@ def load_settings( Args: tenant_id (str): The tenant ID of the desired SSO Settings - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Containing the loaded SSO settings information. Return dict in the format: {"tenant": {"id": "T2AAAA", "name": "myTenantName", "selfProvisioningDomains": [], "customAttributes": {}, "authType": "saml", "domains": ["lulu", "kuku"]}, "saml": {"idpEntityId": "", "idpSSOUrl": "", "idpCertificate": "", "idpMetadataUrl": "https://dummy.com/metadata", "spEntityId": "", "spACSUrl": "", "spCertificate": "", "attributeMapping": {"name": "name", "email": "email", "username": "", "phoneNumber": "phone", "group": "", "givenName": "", "middleName": "", "familyName": "", "picture": "", "customAttributes": {}}, "groupsMapping": [], "redirectUrl": ""}, "oidc": {"name": "", "clientId": "", "clientSecret": "", "redirectUrl": "", "authUrl": "", "tokenUrl": "", "userDataUrl": "", "scope": [], "JWKsUrl": "", "userAttrMapping": {"loginId": "sub", "username": "", "name": "name", "email": "email", "phoneNumber": "phone_number", "verifiedEmail": "email_verified", "verifiedPhone": "phone_number_verified", "picture": "picture", "givenName": "given_name", "middleName": "middle_name", "familyName": "family_name"}, "manageProviderTokens": False, "callbackDomain": "", "prompt": [], "grantType": "authorization_code", "issuer": ""}} @@ -305,7 +305,7 @@ def get_settings( Args: tenant_id (str): The tenant ID of the desired SSO Settings - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Containing the loaded SSO settings information. Raise: diff --git a/descope/management/tenant.py b/descope/management/tenant.py index 99f91fc22..38432b7f9 100644 --- a/descope/management/tenant.py +++ b/descope/management/tenant.py @@ -28,7 +28,7 @@ def create( enforce_sso (bool): Optional, login to the tenant is possible only using the configured sso disabled (bool): Optional, login to the tenant will be disabled - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"id": } @@ -128,7 +128,7 @@ def load( Args: id (str): The ID of the tenant to load. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"id": , "name": , "selfProvisioningDomains": [], "customAttributes: {}, "createdTime": } Containing the loaded tenant information. @@ -149,7 +149,7 @@ def load_all( """ Load all tenants. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"tenants": [{"id": , "name": , "selfProvisioningDomains": [], customAttributes: {}, "createdTime": }]} Containing the loaded tenant information. @@ -179,7 +179,7 @@ def search_all( self_provisioning_domains (List[str]): Optional list of self provisioning domains to filter by custom_attributes (dict): Optional search for a attribute with a given value - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"tenants": [{"id": , "name": , "selfProvisioningDomains": [], customAttributes:{}}]} Containing the loaded tenant information. diff --git a/descope/management/user.py b/descope/management/user.py index 52ec79e11..bfb20b350 100644 --- a/descope/management/user.py +++ b/descope/management/user.py @@ -111,7 +111,7 @@ def create( custom_attributes (dict): Optional, set the different custom attributes values of the keys that were previously configured in Descope console app sso_app_ids (List[str]): Optional, list of SSO applications IDs to be associated with the user. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"user": {}} Containing the created user information. @@ -187,7 +187,7 @@ def create_test_user( custom_attributes (dict): Optional, set the different custom attributes values of the keys that were previously configured in Descope console app sso_app_ids (List[str]): Optional, list of SSO applications IDs to be associated with the user. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"user": {}} Containing the created test user information. @@ -372,7 +372,7 @@ def update( sso_app_ids (List[str]): Optional, list of SSO applications IDs to be associated with the user. test (bool, optional): Set to True to update a test user. Defaults to False. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"user": {}} Containing the updated user information. @@ -448,7 +448,7 @@ def patch( status (str): Optional status field. Can be one of: "enabled", "disabled", "invited". test (bool, optional): Set to True to update a test user. Defaults to False. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"user": {}} Containing the patched user information. @@ -499,7 +499,7 @@ def patch_batch( Each UserObj should have a login_id and the fields to be updated. test (bool, optional): Set to True to patch test users. Defaults to False. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"patchedUsers": [...], "failedUsers": [...]} "patchedUsers" contains successfully patched users, @@ -593,7 +593,7 @@ def load( Args: login_id (str): The login ID of the user to be loaded. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"user": {}} Containing the loaded user information. @@ -619,7 +619,7 @@ def load_by_user_id( Args: user_id (str): The user ID from the user's JWT. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"user": {}} Containing the loaded user information. @@ -726,7 +726,7 @@ def search_all( tenant_role_names (dict): Optional mapping of tenant ID to list of role names. Dict value is in the form of {"tenant_id": {"values":["role_name1", "role_name2"], "and": True}} if you want to match all roles (AND) or any role (OR). - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"users": []} "users" contains a list of all of the found users and their information @@ -848,7 +848,7 @@ def search_all_test_users( tenant_role_names (dict): Optional mapping of tenant ID to list of role names. Dict value is in the form of {"tenant_id": {"values":["role_name1", "role_name2"], "and": True}} if you want to match all roles (AND) or any role (OR). - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"users": []} "users" contains a list of all of the found users and their information @@ -939,7 +939,7 @@ def get_provider_token( withRefreshToken (bool): Optional, set to true to get also the refresh token. forceRefresh (bool): Optional, set to true to force refresh the token. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"provider": "", "providerUserId": "", "accessToken": "", "expiration": "", "scopes": "[]"} Containing the provider token of the given user and provider. @@ -969,7 +969,7 @@ def activate( Args: login_id (str): The login ID of the user to be activated. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"user": {}} Containing the updated user information. @@ -994,7 +994,7 @@ def deactivate( Args: login_id (str): The login ID of the user to be deactivated. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"user": {}} Containing the updated user information. @@ -1022,7 +1022,7 @@ def update_login_id( login_id (str): The login ID of the user to update. new_login_id (str): New login ID to set for the user. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"user": {}} Containing the updated user information. @@ -1051,7 +1051,7 @@ def update_email( email (str): The user email address. Leave empty to remove. verified (bool): Set to true for the user to be able to login with the email address. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"user": {}} Containing the updated user information. @@ -1080,7 +1080,7 @@ def update_phone( phone (str): The user phone number. Leave empty to remove. verified (bool): Set to true for the user to be able to login with the phone number. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"user": {}} Containing the updated user information. @@ -1110,7 +1110,7 @@ def update_display_name( login_id (str): The login ID of the user to update. display_name (str): Optional user display name. Leave empty to remove. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"user": {}} Containing the updated user information. @@ -1146,7 +1146,7 @@ def update_picture( login_id (str): The login ID of the user to update. picture (str): Optional url to user avatar. Leave empty to remove. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"user": {}} Containing the updated user information. @@ -1172,7 +1172,7 @@ def update_custom_attribute( attribute_key (str): The custom attribute that needs to be updated, this attribute needs to exists in Descope console app attribute_val: The value to be updated - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"user": {}} Containing the updated user information. @@ -1204,7 +1204,7 @@ def set_roles( login_id (str): The login ID of the user to update. role_names (List[str]): A list of roles to set to a user without tenant association. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"user": {}} Containing the updated user information. @@ -1232,7 +1232,7 @@ def add_roles( login_id (str): The login ID of the user to update. role_names (List[str]): A list of roles to add to a user without tenant association. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"user": {}} Containing the updated user information. @@ -1260,7 +1260,7 @@ def remove_roles( login_id (str): The login ID of the user to update. role_names (List[str]): A list of roles to remove from a user without tenant association. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"user": {}} Containing the updated user information. @@ -1287,7 +1287,7 @@ def set_sso_apps( login_id (str): The login ID of the user to update. sso_app_ids (List[str]): A list of sso applications ids for associate with a user. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"user": {}} Containing the updated user information. @@ -1314,7 +1314,7 @@ def add_sso_apps( login_id (str): The login ID of the user to update. sso_app_ids (List[str]): A list of sso applications ids for associate with a user. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"user": {}} Containing the updated user information. @@ -1341,7 +1341,7 @@ def remove_sso_apps( login_id (str): The login ID of the user to update. sso_app_ids (List[str]): A list of sso applications ids to remove association from a user. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"user": {}} Containing the updated user information. @@ -1368,7 +1368,7 @@ def add_tenant( login_id (str): The login ID of the user to update. tenant_id (str): The ID of the tenant to add to the user. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"user": {}} Containing the updated user information. @@ -1395,7 +1395,7 @@ def remove_tenant( login_id (str): The login ID of the user to update. tenant_id (str): The ID of the tenant to add to the user. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"user": {}} Containing the updated user information. @@ -1424,7 +1424,7 @@ def set_tenant_roles( tenant_id (str): The ID of the user's tenant. role_names (List[str]): A list of roles to set on the user. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"user": {}} Containing the updated user information. @@ -1453,7 +1453,7 @@ def add_tenant_roles( tenant_id (str): The ID of the user's tenant. role_names (List[str]): A list of roles to add to the user. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"user": {}} Containing the updated user information. @@ -1482,7 +1482,7 @@ def remove_tenant_roles( tenant_id (str): The ID of the user's tenant. role_names (List[str]): A list of roles to remove from the user. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"user": {}} Containing the updated user information. @@ -1666,7 +1666,7 @@ def generate_otp_for_test_user( login_id (str): The login ID of the test user being validated. login_options (LoginOptions): optional, can be provided to set custom claims to the generated jwt. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"code": "", "loginId": ""} Containing the code for the login (exactly as it sent via Email or Phone messaging). @@ -1703,7 +1703,7 @@ def generate_magic_link_for_test_user( uri (str): Optional redirect uri which will be used instead of any global configuration. login_options (LoginOptions): optional, can be provided to set custom claims to the generated jwt. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"link": "", "loginId": ""} Containing the magic link for the login (exactly as it sent via Email or Phone messaging). @@ -1738,7 +1738,7 @@ def generate_enchanted_link_for_test_user( uri (str): Optional redirect uri which will be used instead of any global configuration. login_options (LoginOptions): optional, can be provided to set custom claims to the generated jwt. - Return value (dict): + Return value (Union[dict, Awaitable[dict]]): Return dict in the format {"link": "", "loginId": "", "pendingRef": ""} Containing the enchanted link for the login (exactly as it sent via Email or Phone messaging) and pendingRef. @@ -1768,7 +1768,7 @@ def generate_embedded_link( login_id (str): The login ID of the user to authenticate with. custom_claims (dict): Additional claims to place on the jwt after verification - Return value (str): + Return value (Union[str, Awaitable[str]]): Return the token to be used in verification process Raise: @@ -1802,7 +1802,7 @@ def generate_sign_up_embedded_link( login_options (LoginOptions): Optional login options to customize the link timeout (int): Optional, the timeout in seconds for the link to be valid - Return value (str): + Return value (Union[str, Awaitable[str]]): Return the token to be used in verification process Raise: @@ -1829,7 +1829,7 @@ def history(self, user_ids: List[str]) -> Union[List[dict], Awaitable[List[dict] Args: login_ids (List[str]): List of Users' IDs. - Return value (List[dict]): + Return value (Union[List[dict], Awaitable[List[dict]]]): Return List in the format [ { From 2b77b65660651923242833dc03c5f39dd933d8f8 Mon Sep 17 00:00:00 2001 From: mellowCaribou Date: Thu, 9 Oct 2025 11:11:51 +0300 Subject: [PATCH 4/4] fixs --- descope/authmethod/enchantedlink.py | 4 ---- descope/authmethod/oauth.py | 18 ++---------------- 2 files changed, 2 insertions(+), 20 deletions(-) diff --git a/descope/authmethod/enchantedlink.py b/descope/authmethod/enchantedlink.py index ccc87b049..a2b91f719 100644 --- a/descope/authmethod/enchantedlink.py +++ b/descope/authmethod/enchantedlink.py @@ -228,7 +228,3 @@ def _compose_update_user_email_body( @staticmethod def _compose_get_session_body(pending_ref: str) -> dict: return {"pendingRef": pending_ref} - - @staticmethod - def _get_pending_ref_from_response(response: httpx.Response) -> dict: - return response.json() diff --git a/descope/authmethod/oauth.py b/descope/authmethod/oauth.py index fb5f9f007..caeb5f12b 100644 --- a/descope/authmethod/oauth.py +++ b/descope/authmethod/oauth.py @@ -41,22 +41,8 @@ def start( ) def exchange_token(self, code: str) -> Union[dict, Awaitable[dict]]: - if not code: - raise AuthException( - 400, - ERROR_TYPE_INVALID_ARGUMENT, - "exchange code is empty", - ) - - uri = EndpointsV1.oauth_exchange_token_path - response = self._auth.do_post(uri, {"code": code}, None) - return futu_apply( - response, - lambda response: self._auth.generate_jwt_response( - response.json(), - response.cookies.get(REFRESH_SESSION_COOKIE_NAME, None), - None, - ), + return self._auth.exchange_token( + EndpointsV1.oauth_exchange_token_path, code, None ) @staticmethod