Skip to content

Commit 1a7defa

Browse files
committed
fix: make it a class
1 parent dfefec9 commit 1a7defa

File tree

6 files changed

+169
-360
lines changed

6 files changed

+169
-360
lines changed

.pre-commit-config.yaml

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ repos:
1919
- id: isort
2020
args: ["--profile", "black"]
2121
- repo: https://github.com/psf/black
22-
rev: 25.1.0
22+
rev: 24.10.0
2323
hooks:
2424
- id: black
2525
language_version: python3
@@ -29,19 +29,22 @@ repos:
2929
- id: pyupgrade
3030
args: [--py38-plus]
3131
- repo: https://github.com/pycqa/flake8
32-
rev: 7.2.0
32+
rev: 7.1.2
3333
hooks:
3434
- id: flake8
3535
additional_dependencies: [Flake8-pyproject]
3636
- repo: https://github.com/python-poetry/poetry
3737
rev: 2.1.3
3838
hooks:
39-
- id: poetry-export
40-
files: pyproject.toml
4139
- id: poetry-lock
4240
files: pyproject.toml
4341
- id: poetry-check
4442
files: pyproject.toml
43+
- repo: https://github.com/python-poetry/poetry-plugin-export
44+
rev: 1.9.0
45+
hooks:
46+
- id: poetry-export
47+
files: pyproject.toml
4548
- repo: https://github.com/pre-commit/pre-commit
4649
rev: v4.2.0
4750
hooks:
@@ -62,11 +65,3 @@ repos:
6265
types: [python]
6366
entry: "print"
6467
language: pygrep
65-
- id: liccheck
66-
name: Run Python License Checker
67-
description: Check license compliance of python requirements
68-
entry: poetry
69-
args: [run, liccheck, --level, paranoid]
70-
language: system
71-
files: ^(.*requirements.*\.txt|setup\.cfg|setup\.py|pyproject\.toml|liccheck\.ini)$
72-
pass_filenames: false

ASYNC_IMPLEMENTATION.md

Lines changed: 0 additions & 43 deletions
This file was deleted.

descope/descope_client_async.py

Lines changed: 54 additions & 114 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,22 @@
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

388
from __future__ import annotations
399

40-
from typing import Any, Callable, Type, TypeVar
10+
from typing import Any, Callable, TypeVar
11+
4112
from asyncer import asyncify as _asyncify
4213

14+
from descope.common import DEFAULT_TIMEOUT_SECONDS
15+
4316
T = 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()

poetry.lock

Lines changed: 1 addition & 45 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)