Skip to content

Commit 83afa6a

Browse files
committed
fix: address PR review comments on async transport foundation
- Use inspect.isawaitable() in future_utils for correct awaitable detection - Guard async_mode=True with a clear AuthException when async client is not initialized - Use contextvars.ContextVar for last_response in async methods (thread-safe per task) - Add aclose() and __aenter__/__aexit__ to DescopeClient for async resource cleanup
1 parent d751b99 commit 83afa6a

3 files changed

Lines changed: 34 additions & 7 deletions

File tree

descope/descope_client.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,16 @@ def __init__(
115115
self._auth_http_client = auth_http_client
116116
self._mgmt_http_client = mgmt_http_client
117117

118+
async def aclose(self) -> None:
119+
await self._auth_http_client.aclose()
120+
await self._mgmt_http_client.aclose()
121+
122+
async def __aenter__(self):
123+
return self
124+
125+
async def __aexit__(self, *_):
126+
await self.aclose()
127+
118128
@property
119129
def mgmt(self):
120130
return self._mgmt

descope/future_utils.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from __future__ import annotations
22

3-
import asyncio
3+
import inspect
44
from typing import Any, Awaitable, Callable, TypeVar, Union
55

66
T = TypeVar("T")
@@ -9,7 +9,7 @@
99
def then(
1010
result_or_coro: Union[T, Awaitable[T]], modifier: Callable[[T], Any]
1111
) -> Union[Any, Awaitable[Any]]:
12-
if asyncio.iscoroutine(result_or_coro) or asyncio.isfuture(result_or_coro):
12+
if inspect.isawaitable(result_or_coro):
1313

1414
async def process_async():
1515
result = await result_or_coro
@@ -32,6 +32,6 @@ async def awaitable_wrapper():
3232

3333

3434
async def resolve(obj: Union[Any, Awaitable[Any]]) -> Any:
35-
if asyncio.iscoroutine(obj) or asyncio.isfuture(obj):
35+
if inspect.isawaitable(obj):
3636
return await obj
3737
return obj

descope/http_client.py

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
import asyncio
4+
import contextvars
45
import os
56
import platform
67
import ssl
@@ -178,6 +179,9 @@ def __init__(
178179
# Reserved for the future global async rollout (see big-plan.md "Final stage")
179180
self.async_mode_experimental = async_mode_experimental
180181
self._thread_local = threading.local()
182+
self._async_last_response: contextvars.ContextVar[DescopeResponse | None] = contextvars.ContextVar(
183+
"last_response", default=None
184+
)
181185

182186
# Setup SSL verification for httpx (backwards compatibility with requests)
183187
self.client_verify: bool | ssl.SSLContext = False
@@ -208,6 +212,8 @@ def get(
208212
async_mode: bool = False,
209213
) -> httpx.Response | Awaitable[httpx.Response]:
210214
if async_mode:
215+
if self._async_client is None:
216+
raise AuthException(400, ERROR_TYPE_INVALID_ARGUMENT, "async_mode requires async_mode_experimental=True at client construction")
211217
return self._async_get(uri, params=params, allow_redirects=allow_redirects, pswd=pswd)
212218
response = self._execute_with_retry(
213219
lambda: httpx.get(
@@ -235,6 +241,8 @@ def post(
235241
async_mode: bool = False,
236242
) -> httpx.Response | Awaitable[httpx.Response]:
237243
if async_mode:
244+
if self._async_client is None:
245+
raise AuthException(400, ERROR_TYPE_INVALID_ARGUMENT, "async_mode requires async_mode_experimental=True at client construction")
238246
return self._async_post(uri, body=body, params=params, pswd=pswd, base_url=base_url)
239247
response = self._execute_with_retry(
240248
lambda: httpx.post(
@@ -262,6 +270,8 @@ def put(
262270
async_mode: bool = False,
263271
) -> httpx.Response | Awaitable[httpx.Response]:
264272
if async_mode:
273+
if self._async_client is None:
274+
raise AuthException(400, ERROR_TYPE_INVALID_ARGUMENT, "async_mode requires async_mode_experimental=True at client construction")
265275
return self._async_put(uri, body=body, params=params, pswd=pswd)
266276
response = self._execute_with_retry(
267277
lambda: httpx.put(
@@ -287,6 +297,8 @@ def patch(
287297
async_mode: bool = False,
288298
) -> httpx.Response | Awaitable[httpx.Response]:
289299
if async_mode:
300+
if self._async_client is None:
301+
raise AuthException(400, ERROR_TYPE_INVALID_ARGUMENT, "async_mode requires async_mode_experimental=True at client construction")
290302
return self._async_patch(uri, body=body, params=params, pswd=pswd)
291303
response = self._execute_with_retry(
292304
lambda: httpx.patch(
@@ -313,6 +325,8 @@ def delete(
313325
async_mode: bool = False,
314326
) -> httpx.Response | Awaitable[httpx.Response]:
315327
if async_mode:
328+
if self._async_client is None:
329+
raise AuthException(400, ERROR_TYPE_INVALID_ARGUMENT, "async_mode requires async_mode_experimental=True at client construction")
316330
return self._async_delete(uri, params=params, pswd=pswd)
317331
response = self._execute_with_retry(
318332
lambda: httpx.delete(
@@ -351,6 +365,9 @@ def get_last_response(self) -> DescopeResponse | None:
351365
if resp:
352366
logger.error(f"cf-ray: {resp.headers.get('cf-ray')}")
353367
"""
368+
async_resp = self._async_last_response.get(None)
369+
if async_resp is not None:
370+
return async_resp
354371
return getattr(self._thread_local, "last_response", None)
355372

356373
def get_default_headers(self, pswd: str | None = None) -> dict:
@@ -460,7 +477,7 @@ async def _async_get(
460477
)
461478
)
462479
if self.verbose:
463-
self._thread_local.last_response = DescopeResponse(response)
480+
self._async_last_response.set(DescopeResponse(response))
464481
self._raise_from_response(response)
465482
return response
466483

@@ -483,7 +500,7 @@ async def _async_post(
483500
)
484501
)
485502
if self.verbose:
486-
self._thread_local.last_response = DescopeResponse(response)
503+
self._async_last_response.set(DescopeResponse(response))
487504
self._raise_from_response(response)
488505
return response
489506

@@ -525,7 +542,7 @@ async def _async_patch(
525542
)
526543
)
527544
if self.verbose:
528-
self._thread_local.last_response = DescopeResponse(response)
545+
self._async_last_response.set(DescopeResponse(response))
529546
self._raise_from_response(response)
530547
return response
531548

@@ -545,6 +562,6 @@ async def _async_delete(
545562
)
546563
)
547564
if self.verbose:
548-
self._thread_local.last_response = DescopeResponse(response)
565+
self._async_last_response.set(DescopeResponse(response))
549566
self._raise_from_response(response)
550567
return response

0 commit comments

Comments
 (0)