Skip to content

feat: add async client base, AsyncHTTPClient, AsyncDescopeClient, and async TOTP#1562

Draft
LioriE wants to merge 18 commits into
mainfrom
feat/poc-async-base-and-totp
Draft

feat: add async client base, AsyncHTTPClient, AsyncDescopeClient, and async TOTP#1562
LioriE wants to merge 18 commits into
mainfrom
feat/poc-async-base-and-totp

Conversation

@LioriE

@LioriE LioriE commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Introduced _ClientBase and _AuthBase to extract shared sync/async logic
  • Added AsyncHTTPClient with httpx.AsyncClient for non-blocking HTTP
  • Added AsyncDescopeClient as the async counterpart to DescopeClient
  • Added async TOTP support via AsyncTOTP backed by a shared _TOTPBase
  • Refactored DescopeClient and TOTP to reuse the new base classes

Status

Draft / POC — async infrastructure is in place; remaining auth methods need to be ported to async.

Test plan

  • tests/test_async_http_client.py — AsyncHTTPClient unit tests
  • tests/test_descope_client_parity.py — sync/async client behaviour parity
  • tests/test_totp_parity.py — sync/async TOTP parity

Comment thread tests/test_descope_client_unified.py Fixed
Comment thread tests/test_descope_client_parity.py Outdated
Comment thread tests/test_descope_client_parity.py Outdated
@LioriE LioriE force-pushed the feat/poc-async-base-and-totp branch from 46a82d0 to 99839cc Compare June 7, 2026 13:03
@github-actions

github-actions Bot commented Jun 7, 2026

Copy link
Copy Markdown

Coverage report

The coverage rate went from 98.31% to 97.67% ⬇️

97.58% of new lines are covered.

Diff Coverage details (click to unfold)

descope/management/access_key.py

100% of new lines are covered (100% of the complete file).

descope/_client_base.py

94.11% of new lines are covered (94.11% of the complete file).
Missing lines: 54, 124, 125, 126, 175, 225

descope/_auth_base.py

87.5% of new lines are covered (92.3% of the complete file).
Missing lines: 9

descope/authmethod/magiclink_async.py

96.15% of new lines are covered (96.15% of the complete file).
Missing lines: 50, 74

descope/authmethod/password.py

100% of new lines are covered (97.95% of the complete file).

descope/management/user_async.py

98.35% of new lines are covered (98.35% of the complete file).
Missing lines: 732, 1079, 1081, 1083

descope/management/_outbound_application_base.py

100% of new lines are covered (100% of the complete file).

descope/authmethod/sso.py

100% of new lines are covered (100% of the complete file).

descope/http_client_async.py

100% of new lines are covered (100% of the complete file).

descope/authmethod/saml_async.py

100% of new lines are covered (100% of the complete file).

descope/management/_audit_base.py

100% of new lines are covered (100% of the complete file).

descope/authmethod/webauthn.py

100% of new lines are covered (100% of the complete file).

descope/management/audit_async.py

73.8% of new lines are covered (73.8% of the complete file).
Missing lines: 77, 79, 81, 83, 85, 87, 89, 91, 93, 95, 97

descope/_http_client_base.py

100% of new lines are covered (100% of the complete file).

descope/authmethod/_totp_base.py

100% of new lines are covered (100% of the complete file).

descope/management/sso_settings_async.py

100% of new lines are covered (100% of the complete file).

descope/management/jwt_async.py

100% of new lines are covered (100% of the complete file).

descope/authmethod/_oauth_base.py

100% of new lines are covered (100% of the complete file).

descope/management/tenant_async.py

100% of new lines are covered (100% of the complete file).

descope/authmethod/otp.py

100% of new lines are covered (98.18% of the complete file).

descope/authmethod/webauthn_async.py

100% of new lines are covered (100% of the complete file).

descope/init.py

100% of new lines are covered (100% of the complete file).

descope/authmethod/oauth.py

100% of new lines are covered (100% of the complete file).

descope/authmethod/sso_async.py

100% of new lines are covered (100% of the complete file).

descope/management/_user_base.py

92.41% of new lines are covered (92.41% of the complete file).
Missing lines: 65, 66, 67, 68, 69, 70, 123, 178, 223, 225, 227

descope/authmethod/_saml_base.py

100% of new lines are covered (100% of the complete file).

descope/management/access_key_async.py

100% of new lines are covered (100% of the complete file).

descope/management/role_async.py

97.77% of new lines are covered (97.77% of the complete file).
Missing lines: 315

descope/authmethod/_otp_base.py

88.33% of new lines are covered (88.33% of the complete file).
Missing lines: 60, 96, 98, 100, 120, 122, 124

descope/management/sso_application_async.py

100% of new lines are covered (100% of the complete file).

descope/management/permission_async.py

100% of new lines are covered (100% of the complete file).

descope/management/license_async.py

100% of new lines are covered (100% of the complete file).

descope/authmethod/saml.py

100% of new lines are covered (100% of the complete file).

descope/management/authz_async.py

100% of new lines are covered (100% of the complete file).

descope/management/fga_async.py

100% of new lines are covered (100% of the complete file).

descope/authmethod/magiclink.py

100% of new lines are covered (96.22% of the complete file).

descope/management/_access_key_base.py

100% of new lines are covered (100% of the complete file).

descope/authmethod/_enchantedlink_base.py

92.15% of new lines are covered (92.15% of the complete file).
Missing lines: 69, 98, 100, 102

descope/management/sso_application.py

100% of new lines are covered (100% of the complete file).

descope/management/group_async.py

100% of new lines are covered (100% of the complete file).

descope/descope_client.py

100% of new lines are covered (100% of the complete file).

descope/management/outbound_application.py

100% of new lines are covered (100% of the complete file).

descope/management/sso_settings.py

100% of new lines are covered (100% of the complete file).

descope/management/user.py

100% of new lines are covered (98.34% of the complete file).

descope/management/_jwt_base.py

100% of new lines are covered (100% of the complete file).

descope/authmethod/totp_async.py

100% of new lines are covered (100% of the complete file).

descope/management/management_key_async.py

100% of new lines are covered (100% of the complete file).

descope/http_client.py

100% of new lines are covered (100% of the complete file).

descope/management/_sso_application_base.py

100% of new lines are covered (100% of the complete file).

descope/authmethod/enchantedlink.py

100% of new lines are covered (95.83% of the complete file).

descope/authmethod/_webauthn_base.py

100% of new lines are covered (100% of the complete file).

descope/management/tenant.py

100% of new lines are covered (100% of the complete file).

descope/authmethod/_password_base.py

100% of new lines are covered (100% of the complete file).

descope/_http_base.py

83.33% of new lines are covered (90.9% of the complete file).
Missing lines: 8

descope/management/audit.py

100% of new lines are covered (73.8% of the complete file).

descope/management/descoper_async.py

100% of new lines are covered (100% of the complete file).

descope/mgmt_async.py

100% of new lines are covered (100% of the complete file).

descope/authmethod/otp_async.py

96.29% of new lines are covered (96.29% of the complete file).
Missing lines: 48, 74

descope/management/_tenant_base.py

100% of new lines are covered (100% of the complete file).

descope/authmethod/totp.py

100% of new lines are covered (100% of the complete file).

descope/management/project_async.py

100% of new lines are covered (100% of the complete file).

descope/authmethod/oauth_async.py

100% of new lines are covered (100% of the complete file).

descope/authmethod/password_async.py

97.82% of new lines are covered (97.82% of the complete file).
Missing lines: 62

descope/authmethod/_sso_base.py

100% of new lines are covered (100% of the complete file).

descope/management/flow_async.py

100% of new lines are covered (100% of the complete file).

descope/management/_sso_settings_base.py

98.24% of new lines are covered (98.24% of the complete file).
Missing lines: 7

descope/authmethod/_magiclink_base.py

88.52% of new lines are covered (88.52% of the complete file).
Missing lines: 74, 103, 105, 107, 127, 129, 131

descope/descope_client_async.py

98.36% of new lines are covered (98.36% of the complete file).
Missing lines: 160, 163

descope/authmethod/enchantedlink_async.py

95.83% of new lines are covered (95.83% of the complete file).
Missing lines: 46, 64

descope/management/outbound_application_async.py

100% of new lines are covered (100% of the complete file).

descope/management/jwt.py

100% of new lines are covered (100% of the complete file).

Comment thread descope/_http_client_base.py Dismissed
Comment thread descope/_http_client_base.py
LioriE and others added 4 commits June 8, 2026 18:21
Move PUBLIC_KEY_DICT, VALID_REFRESH_TOKEN, VALID_SESSION_TOKEN, and
EXPIRED_SESSION_TOKEN from individual test files into testutils.py.
Move assert_http_called into conftest.py. Replace old test files with
the unified sync/async parity versions.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace ~45 three-line dash-banner comment blocks across 8 files with
nothing — class names, method names, and docstrings already describe
what those banners labelled. The only banner-style block kept is the
SOCKS-proxy workaround doc in conftest.py, which is real documentation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Comment thread descope/_auth_base.py
Comment thread descope/_http_base.py
@LioriE LioriE requested a review from omercnet June 9, 2026 10:35
@omercnet

omercnet commented Jun 9, 2026

Copy link
Copy Markdown
Member

@shuni review this async implementation will it scale to all other methods?

@shuni-bot

shuni-bot Bot commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

🐕 Review complete — View session on Shuni Portal 🐾

@shuni-bot shuni-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🐕 Shuni's Review

POC that extracts shared sync/async base classes and adds AsyncHTTPClient, AsyncDescopeClient, and async TOTP. Clean refactor — the base-class extraction preserves behaviour and sync/async parity is faithful (verified against Auth methods).

Sniffed out 3 issues:

  • 1 🟡 MEDIUM: async validate/refresh path runs blocking sync I/O on the event loop
  • 2 🟢 LOW: committed sandbox workaround in conftest; verbose put parity gap

No bugs that break — good bones for a POC! Woof!

Comment thread descope/descope_client_async.py
Comment thread tests/conftest.py
params=params,
)
)
self._raise_from_response(response)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 LOW: put skips the if self.verbose: self._last_response_var.set(...) capture that get/post/patch/delete all do, so get_last_response() returns a stale response after a PUT. (The sync HTTPClient.put has the same gap, so this preserves parity — but since this is new code it's a cheap spot to fix both.)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@omercnet is this intentional?

@LioriE

LioriE commented Jun 9, 2026

Copy link
Copy Markdown
Contributor Author

@shuni follow the example set in this PR
implement the changes on the other auth methods and the management methods.
including testing

follow conventions and patterns set in this PR.

perform this in another branch and open a pull request to this branch.

@shuni-bot

shuni-bot Bot commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

🐕 Uh oh! I ran into an error.

View failed run

Please check the logs for details.

LioriE added 4 commits June 10, 2026 11:27
For each method (OAuth, SSO, SAML, OTP, MagicLink, EnchantedLink,
Password, WebAuthn): extract an I/O-free _<name>_base.py with shared
static helpers, refactor the sync class to inherit it, add a parallel
async class using AsyncAuthBase, and rewrite the test file to the
parametrized client_factory harness so every test runs under both sync
and async. Wires all new async classes into DescopeClientAsync.
Comment thread tests/management/test_descoper.py Fixed
Comment thread descope/management/_jwt_base.py Fixed
Comment thread descope/management/_user_base.py Fixed
Comment thread descope/management/_user_base.py Fixed
Comment thread descope/management/fga_async.py Fixed
Comment thread descope/management/user_async.py Fixed
Comment thread descope/management/user_async.py Fixed
- Add missing `from datetime import datetime` to audit.py / audit_async.py
- Rename camelCase params in tenant_async.py (JITDisabled→jit_disabled) and
  user_async.py (withRefreshToken/forceRefresh→snake_case); `from __future__
  import annotations` caused ruff N803 to fire only on the async files
- Fix N806 variable names in _user_base.py (usersBody→users_body, uBody→u_body)
- Wire JWTBase into jwt.py / jwt_async.py; UserBase._validate_search_pagination
  into user.py / user_async.py; complete sso_settings_async.py dedup
- Remove unused imports (Any from fga_async, url_params_to_dict from
  outbound_application)
mock_delete.assert_called_with(
f"{common.DEFAULT_BASE_URL}{MgmtV1.descoper_delete_path}",
with client.mock_mgmt_delete(make_response()) as mock_delete:
assert await client.invoke(client.mgmt.descoper.delete("U2111111111111111111111111")) is None
@LioriE

LioriE commented Jun 10, 2026

Copy link
Copy Markdown
Contributor Author

@shuni review this full implementation. does it follow the example of totp? are there any issues with the management implementation?

@shuni-bot

shuni-bot Bot commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

🐕 Done! No changes were needed.

View workflow run

📝 Analysis

Here's a summary of the deep-dive review:

Does it follow the TOTP pattern?

Yes, consistently across the entire codebase. The TOTP pattern is:

  • Extract shared validation/body-composition into a _*_base.py file (TOTPBase) with @staticmethod methods
  • Sync class: class TOTP(TOTPBase, AuthBase) — calls self._http.post(...)
  • Async class: class TOTPAsync(TOTPBase, AsyncAuthBase) — calls await self._http.post(...)

All 9 auth methods and all 18 management modules follow this dual-inheritance pattern. The DescopeClientBase, HTTPClientBase, and MGMTAsync extractions all mirror their sync counterparts properly.

Issues with the management implementation

1 real bug found (HIGH): The test_mgmt test at tests/test_descope_client.py:93 skips for async mode with "mgmt not available on DescopeClientAsync" — but mgmt IS fully wired up (MGMTAsync instantiated at descope_client_async.py:94-98, exposed via mgmt property at line 151). The management-key guard is a sync property check that should work identically for both modes. The skip means async management is never tested.

1 design concern (MEDIUM): _fetch_rate_limit_tier uses synchronous httpx.get() and blocks the event loop when called from DescopeClientAsync.__init__. Same issue with Auth._validate_token_fetch_public_keys doing sync I/O during async token validation. Suggested a lazy-init or factory-method approach.

📋 Actions Taken

No tasks recorded


View session on Shuni Portal 🐾

@shuni-bot shuni-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🐕 Shuni's Review — Deep Dive

Thorough review of async infrastructure, TOTP pattern compliance, and management wiring.

Pattern compliance: solid. All 9 auth methods and all 18 management modules follow the TOTP dual-inheritance pattern (XBase + AuthBase/AsyncAuthBase for auth, XBase + HTTPBase/AsyncHTTPBase for mgmt). DescopeClientBase, HTTPClientBase, MGMTAsync — all properly mirror their sync counterparts.

Sniffed out 4 issues:

  • 1 🟠 HIGH: mgmt test incorrectly skipped for async (stale skip)
  • 1 🟡 MEDIUM: sync blocking I/O in async constructor
  • 2 🟢 LOW: conftest scaffold; verbose put gap

See inline comments. Good bones on this refactor — the base-class extraction is clean! Woof!

Comment thread tests/test_descope_client.py Outdated
Comment on lines +92 to +94
async def test_mgmt(self, descope_client):
if descope_client.mode != "sync":
pytest.skip("mgmt not available on DescopeClientAsync")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 HIGH: This skip is stale — DescopeClientAsync does wire up MGMTAsync now (line 94–98 of descope_client_async.py). The management-key guard test (descope_client.mgmt.tenant raising AuthException without a key) is a sync property access that works identically for both sync and async clients. This skip means the async management guard is never tested.

Remove the skip so the test runs for both modes. The descope_client fixture creates both variants without a management key, so every mgmt.* property access should raise AuthException regardless of mode.

Suggested change
async def test_mgmt(self, descope_client):
if descope_client.mode != "sync":
pytest.skip("mgmt not available on DescopeClientAsync")
async def test_mgmt(self, descope_client):

Comment thread descope/descope_client_async.py
Comment thread tests/conftest.py
Comment thread descope/http_client_async.py
- Remove stale pytest.skip from test_mgmt (MGMTAsync raises AuthException without key, same as sync)
- Add per-method one-liner docstrings to all 8 async auth classes to match TOTPAsync pattern
- Fix response param shadowing in webauthn sign_up_finish/sign_in_finish (sync + async)
- Add gitleaks fingerprints for test-fixture JWTs in test_descope_client_parity.py
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants