|
| 1 | +# Async Rollout Plan — python-sdk |
| 2 | + |
| 3 | +## Current state |
| 4 | +- `future_utils.py` exists with `then`, `wrap`, `resolve` helpers |
| 5 | +- `HTTPClient` stores `async_mode_experimental` but doesn't act on it yet |
| 6 | +- `DescopeClient.__init__` accepts and forwards the flag |
| 7 | + |
| 8 | +--- |
| 9 | + |
| 10 | +## Stage 0 — Foundation: async HTTP transport (1 PR + 1 test PR) |
| 11 | + |
| 12 | +**PR 0a — Implementation:** |
| 13 | +- Add `httpx.AsyncClient` (persistent, per-instance) alongside the existing synchronous path |
| 14 | +- Add `async def _async_execute_with_retry(request_fn)` mirroring the sync retry loop |
| 15 | +- Each public method accepts an explicit `async_mode: bool = False` parameter; passing `True` delegates to the async path and returns a coroutine; the class-level `async_mode_experimental` flag is stored but inert until the final global-rollout stage |
| 16 | +- No callers change yet — this PR is purely internal to `HTTPClient` |
| 17 | + |
| 18 | +**PR 0b — Tests:** |
| 19 | +- Unit tests asserting async mode methods return coroutines (`asyncio.iscoroutine`) |
| 20 | +- Verify sync mode is completely unaffected (all existing tests continue to pass unchanged) |
| 21 | +- Test async retry logic (mock 503s, assert delays and retry count) |
| 22 | + |
| 23 | +--- |
| 24 | + |
| 25 | +## Stage 1–9 — Auth methods (one file per PR pair) |
| 26 | + |
| 27 | +**Pattern for every auth method file:** |
| 28 | + |
| 29 | +```python |
| 30 | +# Before |
| 31 | +response = self._http.post(uri, body=body) |
| 32 | +return Auth.extract_masked_address(response.json(), method) |
| 33 | + |
| 34 | +# After (using then from future_utils) |
| 35 | +from descope.future_utils import then |
| 36 | +response = self._http.post(uri, body=body) |
| 37 | +return then(response, lambda r: Auth.extract_masked_address(r.json(), method)) |
| 38 | +``` |
| 39 | + |
| 40 | +When the HTTP client returns a plain `httpx.Response` (sync mode), `then` applies the lambda immediately and returns the final value — zero behaviour change. When it returns a coroutine (async mode), `then` returns a new coroutine that awaits it and applies the lambda. |
| 41 | + |
| 42 | +Rollout order (each is one implementation PR + one test PR): |
| 43 | + |
| 44 | +| Stage | File | Methods | |
| 45 | +|-------|------|---------| |
| 46 | +| 1 | `authmethod/otp.py` | sign\_in, sign\_up, sign\_up\_or\_in, verify\_code, update\_user\_email, update\_user\_phone | |
| 47 | +| 2 | `authmethod/magiclink.py` | sign\_in, sign\_up, sign\_up\_or\_in, verify, update\_user\_email, update\_user\_phone | |
| 48 | +| 3 | `authmethod/enchantedlink.py` | sign\_in, sign\_up, sign\_up\_or\_in, verify, get\_session, update\_user\_email, update\_user\_phone | |
| 49 | +| 4 | `authmethod/oauth.py` | start, exchange\_token, update\_user | |
| 50 | +| 5 | `authmethod/password.py` | sign\_in, sign\_up, send\_reset, update, replace, get\_policy | |
| 51 | +| 6 | `authmethod/totp.py` | sign\_in, sign\_up, sign\_up\_or\_in, update\_user, verify | |
| 52 | +| 7 | `authmethod/webauthn.py` | sign\_in\_start/finish, sign\_up\_start/finish, update\_user\_start/finish | |
| 53 | +| 8 | `authmethod/saml.py` + `sso.py` | start methods | |
| 54 | +| 9 | `auth.py` | validate\_session, refresh\_session, exchange\_access\_key (I/O-bound JWKS fetch) | |
| 55 | + |
| 56 | +--- |
| 57 | + |
| 58 | +## Stage 10–N — Management files (one file per PR pair) |
| 59 | + |
| 60 | +Same `then()` wrapping pattern. Suggested order by impact: |
| 61 | + |
| 62 | +| Stage | File | |
| 63 | +|-------|------| |
| 64 | +| 10 | `management/user.py` | |
| 65 | +| 11 | `management/access_key.py` | |
| 66 | +| 12 | `management/tenant.py` | |
| 67 | +| 13 | `management/role.py` + `permission.py` | |
| 68 | +| 14 | `management/audit.py` | |
| 69 | +| 15 | `management/authz.py` + `management/fga.py` | |
| 70 | +| 16 | `management/sso_settings.py` + `management/sso_application.py` | |
| 71 | +| 17 | `management/flow.py` + `management/jwt.py` | |
| 72 | +| 18 | `management/group.py` + `management/project.py` + remaining files | |
| 73 | + |
| 74 | +--- |
| 75 | + |
| 76 | +## Final stage — Global setting (future, after all stages done) |
| 77 | + |
| 78 | +Once every file is converted, add a class-level `async_mode` property to `DescopeClient` that applies to all methods at once, and graduate the feature out of experimental. The per-file opt-in PRs make this final step trivial since all callers already use `then()`. |
| 79 | + |
| 80 | +--- |
| 81 | + |
| 82 | +## Invariants throughout |
| 83 | +- Sync callers are **never broken** at any stage — `then(sync_result, fn)` is identical to `fn(sync_result)` |
| 84 | +- No new public API surface until the global-setting stage |
| 85 | +- Each implementation PR is independently reviewable and rollback-safe |
| 86 | +- Test PRs always cover both sync (regression) and async (new) paths for the converted file |
0 commit comments