11"""
2- Monkey-patch async support for Descope SDK.
2+ Async support for Descope SDK.
33
4- This mod async_wrapper = _asyncify(original_method)
5- async_wrapper.__name__ = f"{method_name}_async"
6-
7- # Try to set __qualname__ safely (may fail with mocks)
8- try:
9- if hasattr(original_method, "__qualname__"):
10- async_wrapper.__qualname__ = f"{original_method.__qualname__}_async"
11- except (AttributeError, TypeError):
12- # Skip if __qualname__ is not accessible (e.g., with mocks)
13- pass
14-
15- # Try to set docstring safely
16- try:
17- if hasattr(original_method, "__doc__") and original_method.__doc__:
18- async_wrapper.__doc__ = (
19- f"Async version of {method_name}.\n \n {original_method.__doc__}"
20- )
21- else:
22- async_wrapper.__doc__ = f"Async version of {method_name}."
23- except (AttributeError, TypeError):
24- # Skip if __doc__ is not accessible
25- async_wrapper.__doc__ = f"Async version of {method_name}."
26-
27- # Try to preserve the signature safely (skip for now due to asyncer limitations)
28- # try:
29- # sig = inspect.signature(original_method)
30- # if hasattr(async_wrapper, "__signature__"):
31- # async_wrapper.__signature__ = sig
32- # except (ValueError, TypeError, AttributeError):
33- # passcky but elegant way to add async methods to existing
34- sync classes without code duplication. It dynamically discovers all public methods
35- and creates async variants using asyncer.
4+ Provides a native AsyncDescopeClient class that mirrors DescopeClient structure
5+ but with async methods throughout.
366"""
377
388from __future__ import annotations
399
40- from typing import Any , Callable , Type , TypeVar
10+ from typing import Any , Callable , TypeVar
11+
4112from asyncer import asyncify as _asyncify
4213
14+ from descope .common import DEFAULT_TIMEOUT_SECONDS
15+
4316T = TypeVar ("T" )
4417
4518
46- def add_async_methods (cls : Type [T ], method_suffix : str = "_async" ) -> Type [T ]:
19+ def _add_async_methods (cls : type [T ], method_suffix : str = "_async" ) -> type [T ]:
4720 """
4821 Monkey patch a class to add async versions of all public methods.
4922
@@ -106,18 +79,10 @@ async def async_wrapper(self, *args, **kwargs):
10679 # Skip if __doc__ is not accessible
10780 async_wrapper .__doc__ = f"Async version of { method_name } ."
10881
109- # Try to preserve the signature (skip for now due to asyncer limitations)
110- # try:
111- # sig = inspect.signature(original_method)
112- # if hasattr(async_wrapper, "__signature__"):
113- # async_wrapper.__signature__ = sig
114- # except (ValueError, TypeError, AttributeError):
115- # pass
116-
11782 return async_wrapper
11883
11984
120- def asyncify_client (client_instance : Any ) -> Any :
85+ def _asyncify_client (client_instance : Any ) -> Any :
12186 """
12287 Add async methods to an existing client instance.
12388
@@ -128,7 +93,7 @@ def asyncify_client(client_instance: Any) -> Any:
12893 The same instance with async methods added
12994 """
13095 # Patch the instance's class
131- add_async_methods (client_instance .__class__ )
96+ _add_async_methods (client_instance .__class__ )
13297
13398 # Recursively patch all authentication method attributes
13499 # Use a safe list of known auth method attributes to avoid triggering properties
@@ -176,87 +141,62 @@ def asyncify_client(client_instance: Any) -> Any:
176141 attr = getattr (client_instance , attr_name )
177142 if hasattr (attr , "__class__" ) and hasattr (attr , "_auth" ):
178143 # This looks like an auth method class
179- add_async_methods (attr .__class__ )
144+ _add_async_methods (attr .__class__ )
180145 except Exception :
181146 # Skip attributes that can't be accessed safely
182147 continue
183148
184- # Add specific async methods for client-level operations
185- _add_client_async_methods (client_instance )
186-
187149 return client_instance
188150
189151
190- def _add_client_async_methods (client_instance : Any ) -> None :
191- """Add client-specific async methods like close_async."""
192-
193- async def close_async (self ):
194- """Async method to close the client and clean up resources."""
152+ class AsyncDescopeClient :
153+ ALGORITHM_KEY = "alg"
154+
155+ def __init__ (
156+ self ,
157+ project_id : str ,
158+ public_key : dict | None = None ,
159+ skip_verify : bool = False ,
160+ management_key : str | None = None ,
161+ timeout_seconds : float = DEFAULT_TIMEOUT_SECONDS ,
162+ jwt_validation_leeway : int = 5 ,
163+ ):
164+ # Import here to avoid circular import
165+ from .descope_client import DescopeClient
166+
167+ # Create a sync client instance
168+ self ._sync_client = DescopeClient (
169+ project_id ,
170+ public_key ,
171+ skip_verify ,
172+ management_key ,
173+ timeout_seconds ,
174+ jwt_validation_leeway ,
175+ )
176+
177+ # Patch it with async methods
178+ _asyncify_client (self ._sync_client )
179+
180+ def __getattr__ (self , name ):
181+ """Dynamically delegate all attribute access to the sync client."""
182+ attr = getattr (self ._sync_client , name )
183+
184+ # If it's a method and we have an async version, prefer the async version
185+ if callable (attr ) and hasattr (self ._sync_client , f"{ name } _async" ):
186+ return getattr (self ._sync_client , f"{ name } _async" )
187+
188+ return attr
189+
190+ async def close (self ):
191+ """Close the client and clean up resources."""
195192 # For now, this is just a placeholder since the sync client doesn't have cleanup
196193 # In the future, this could close async HTTP connections
197194 pass
198195
199- # Add context manager support
200- async def __aenter__ (self ): # noqa: N807
196+ async def __aenter__ (self ):
201197 """Async context manager entry."""
202198 return self
203199
204- async def __aexit__ (self , exc_type , exc_val , exc_tb ): # noqa: N807
200+ async def __aexit__ (self , exc_type , exc_val , exc_tb ):
205201 """Async context manager exit."""
206- await self .close_async ()
207-
208- # Add methods to the class rather than instance for proper protocol support
209- cls = client_instance .__class__
210-
211- # Only add if not already present
212- if not hasattr (cls , "close_async" ):
213- cls .close_async = close_async
214- if not hasattr (cls , "__aenter__" ):
215- cls .__aenter__ = __aenter__
216- if not hasattr (cls , "__aexit__" ):
217- cls .__aexit__ = __aexit__
218-
219-
220- def create_async_client (client_class : Type [T ], * args , ** kwargs ) -> T :
221- """
222- Create a client instance with async methods automatically added.
223-
224- Args:
225- client_class: The client class to instantiate
226- *args: Arguments for client constructor
227- **kwargs: Keyword arguments for client constructor
228-
229- Returns:
230- Client instance with async methods
231- """
232- # Create instance
233- instance = client_class (* args , ** kwargs )
234-
235- # Add async methods
236- return asyncify_client (instance )
237-
238-
239- # Convenience function for DescopeClient
240- def create_async_descope_client (* args , ** kwargs ): # noqa: N802
241- """
242- Create a DescopeClient with async methods added.
243-
244- Same signature as DescopeClient but with async methods available.
245- All methods get an _async suffix (e.g., sign_up_async, verify_async).
246-
247- Example:
248- client = create_async_descope_client(project_id="P123")
249-
250- # Use sync methods normally
251- result = client.otp.sign_up(DeliveryMethod.EMAIL, "[email protected] ") 252-
253- # Use async methods with _async suffix
254- result = await client.otp.sign_up_async(DeliveryMethod.EMAIL, "[email protected] ") 255- """
256- from .descope_client import DescopeClient
257-
258- return create_async_client (DescopeClient , * args , ** kwargs )
259-
260-
261- # Alias for convenience (ignore naming convention for this public API)
262- AsyncDescopeClient = create_async_descope_client # noqa: N816
202+ await self .close ()
0 commit comments