fix: update Python docs to match SDK - #763
Conversation
WalkthroughThe Python SDK documentation now reflects v2+ architecture, including new exception modules, synchronous ManagementClient construction, revised Management API examples, updated permission checks, and FastAPI handling for Management API exceptions. ChangesPython SDK v2 guidance
Estimated code review effort: 3 (Moderate) | ~20 minutes Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Deploying kinde-docs-preview with
|
| Latest commit: |
d49c202
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://4dad3a4a.kinde-docs-preview.pages.dev |
| Branch Preview URL: | https://fix-python-sdk-management-do.kinde-docs-preview.pages.dev |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/content/docs/developer-tools/sdks/backend/python-sdk.mdx`:
- Around line 1505-1520: Update both FastAPI route examples, including
list_organizations and the corresponding route around the second
ManagementClient example, so synchronous management API calls do not run
directly inside async handlers. Prefer changing each route declaration from
async def to def, preserving the existing synchronous management calls and
response behavior.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 072a4a3c-ae2c-4718-a893-c800c25b4507
📒 Files selected for processing (1)
src/content/docs/developer-tools/sdks/backend/python-sdk.mdx
| ```python | ||
| from kinde_sdk.auth.async_oauth import AsyncOAuth | ||
| from fastapi import FastAPI | ||
| from kinde_sdk.management import ManagementClient | ||
|
|
||
| oauth = AsyncOAuth() | ||
| app = FastAPI() | ||
| management = ManagementClient( | ||
| domain="your-domain.kinde.com", | ||
| client_id="your-m2m-client-id", | ||
| client_secret="your-m2m-client-secret", | ||
| ) | ||
|
|
||
| async def manage_organizations(): | ||
| # Get management client | ||
| management = await oauth.get_management() | ||
|
|
||
| # List organizations | ||
| orgs = await management.get_organizations() | ||
|
|
||
| # Get a specific organization | ||
| org = await management.get_organization(org_id="org_123") | ||
|
|
||
| # Create a new organization | ||
| new_org = await management.create_organization( | ||
| name="My Organization" | ||
| ) | ||
|
|
||
| # Update an organization | ||
| updated_org = await management.update_organization( | ||
| org_id="org_123", | ||
| name="Updated Name" | ||
| ) | ||
|
|
||
| # Delete an organization | ||
| await management.delete_organization(org_id="org_123") | ||
|
|
||
| return orgs | ||
| @app.get("/organizations") | ||
| async def list_organizations(): | ||
| # The management call itself is synchronous | ||
| return management.organizations_api.get_organizations() | ||
| ``` |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Synchronous ManagementClient calls in async FastAPI routes block the event loop.
Both examples define async def routes but call synchronous management.* methods directly. This blocks the event loop for the duration of each HTTP call to Kinde's API, starving concurrent requests. The comment on line 1518 acknowledges the call is synchronous but doesn't address the blocking consequence.
Either define the route as a regular def (FastAPI automatically runs sync routes in a threadpool) or offload the call with asyncio.to_thread():
⚡ Proposed fix — use `def` instead of `async def` (simplest)
`@app.get`("/organizations")
-async def list_organizations():
+def list_organizations():
# The management call itself is synchronous
return management.organizations_api.get_organizations() `@app.get`("/users/{user_id}")
-async def get_user(user_id: str):
+def get_user(user_id: str):
try:
return management.users_api.get_user_data(id=user_id)
except NotFoundException:
raise HTTPException(status_code=404, detail="User not found")
except ApiException as e:
raise HTTPException(status_code=e.status, detail=e.reason)Alternatively, if the route must stay async:
⚡ Alternative fix — offload with `asyncio.to_thread`
`@app.get`("/organizations")
async def list_organizations():
- # The management call itself is synchronous
- return management.organizations_api.get_organizations()
+ return await asyncio.to_thread(management.organizations_api.get_organizations)Also applies to: 1538-1546
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/content/docs/developer-tools/sdks/backend/python-sdk.mdx` around lines
1505 - 1520, Update both FastAPI route examples, including list_organizations
and the corresponding route around the second ManagementClient example, so
synchronous management API calls do not run directly inside async handlers.
Prefer changing each route declaration from async def to def, preserving the
existing synchronous management calls and response behavior.
There was a problem hiding this comment.
Valid, needs a fix.
There was a problem hiding this comment.
@dtoxvanilla1991, acknowledged. The examples need a fix.
- Change the affected FastAPI Management API route handlers from
async deftodef. - Keep the existing synchronous
ManagementClientcalls unchanged. - Validate both Management API code examples after the edit.
🐇 ✅
- 📌 Commit the changes to this branch
You are interacting with an AI system.
dtoxvanilla1991
left a comment
There was a problem hiding this comment.
Good foundation. 4 comments to review. 📔
| from fastapi import HTTPException | ||
| from kinde_sdk.auth.oauth import OAuth | ||
| from kinde_sdk.exceptions import KindeAPIException | ||
|
|
||
| oauth = OAuth(framework="fastapi", app=app) | ||
| from kinde_sdk.auth import permissions | ||
|
|
||
| async def check_permission_with_error_handling(request: Request, permission: str): | ||
| try: | ||
| perm_result = await oauth.get_permission(permission, request) | ||
| if not perm_result.get("isGranted"): | ||
| raise HTTPException( | ||
| status_code=403, | ||
| detail=f"Permission '{permission}' not granted" | ||
| ) | ||
| return perm_result | ||
| except KindeAPIException as e: | ||
| async def require_permission(permission_key: str): | ||
| result = await permissions.get_permission(permission_key) | ||
| if not result.get("isGranted"): | ||
| raise HTTPException( | ||
| status_code=e.status_code, | ||
| detail=f"Error checking permission: {str(e)}" | ||
| status_code=403, | ||
| detail=f"Permission '{permission_key}' not granted", | ||
| ) | ||
| return result |
There was a problem hiding this comment.
This introduces a brand new API shape for permission checks - from kinde_sdk.auth import permissions then await permissions.get_permission(permission_key), no oauth instance, no request. Every other permission check in this file (the Authorization section, plus the untouched examples further down) uses oauth.get_permission(permission, request). Two contradicting ways to do the same thing on the same page is going to confuse whoever reads this next.
Since this PR's whole premise is "match the docs to the real SDK," worth double-checking whether kinde_sdk.auth.permissions is actually the current interface or a mix-up with the oauth.get_permission() pattern used everywhere else on this page. Pick one and make the rest of the file consistent with it.
| @app.get("/protected") | ||
| async def protected_route(request: Request): | ||
| try: | ||
| # Check authentication | ||
| if not await oauth.is_authenticated(request): | ||
| raise HTTPException(status_code=401, detail="Not authenticated") | ||
|
|
||
| # Get user info | ||
| user_info = await oauth.get_user_info(request) | ||
| return user_info | ||
|
|
||
| except KindeAuthenticationException as e: | ||
| # Handle authentication errors | ||
| raise HTTPException(status_code=401, detail=f"Authentication failed: {str(e)}") | ||
|
|
||
| except KindeAuthorizationException as e: | ||
| # Handle authorization errors | ||
| raise HTTPException(status_code=403, detail=f"Authorization failed: {str(e)}") | ||
|
|
||
| except KindeAPIException as e: | ||
| # Handle API errors | ||
| raise HTTPException(status_code=e.status_code, detail=f"API error: {str(e)}") | ||
|
|
||
| except Exception as e: | ||
| # Handle unexpected errors | ||
| raise HTTPException(status_code=500, detail=f"Internal error: {str(e)}") | ||
| async def protected_route(): | ||
| if not oauth.is_authenticated(): | ||
| raise HTTPException(status_code=401, detail="Not authenticated") | ||
| return oauth.get_user_info() |
There was a problem hiding this comment.
This one's calling oauth.is_authenticated() and oauth.get_user_info() with no await and no request arg. Every other FastAPI + OAuth example still left in this same doc (lines ~510, 571, 669, 714, 1663, 1826 on main) awaits these calls and passes request. That's not a small stylistic gap - it's two different call signatures for the same method on the same client in the same file.
If the sync/no-request form is actually correct for the current SDK, the older examples elsewhere in this file need updating too, otherwise readers won't know which one to trust.
| @app.get("/users/{user_id}") | ||
| async def get_user(user_id: str): | ||
| management = oauth.get_management() | ||
| try: | ||
| user = await management.get_user(user_id=user_id) | ||
| return user | ||
| except KindeAPIException as e: | ||
| raise HTTPException(status_code=e.status_code, detail=str(e)) | ||
| except Exception as e: | ||
| raise HTTPException(status_code=500, detail=f"Internal error: {str(e)}") | ||
| ``` | ||
|
|
||
| **Example with AsyncOAuth client:** | ||
|
|
||
| ```python | ||
| from kinde_sdk.auth.async_oauth import AsyncOAuth | ||
| from kinde_sdk.exceptions import KindeAPIException | ||
|
|
||
| oauth = AsyncOAuth() | ||
|
|
||
| async def get_user_safely(user_id: str): | ||
| management = await oauth.get_management() | ||
| try: | ||
| user = await management.get_user(user_id=user_id) | ||
| return user | ||
| except KindeAPIException as e: | ||
| print(f"API Error {e.status_code}: {e.message}") | ||
| return None | ||
| except Exception as e: | ||
| print(f"Unexpected error: {str(e)}") | ||
| return None | ||
| return management.users_api.get_user_data(id=user_id) | ||
| except NotFoundException: | ||
| raise HTTPException(status_code=404, detail="User not found") | ||
| except ApiException as e: | ||
| raise HTTPException(status_code=e.status, detail=e.reason) | ||
| ``` |
There was a problem hiding this comment.
Same blocking issue as the list_organizations example above - async def get_user calling management.users_api.get_user_data() directly. Two occurrences of the same anti-pattern in one section means a reader who fixes one and misses the other still ships a half-broken example. Worth fixing both in the same commit.
| ```python | ||
| from kinde_sdk.auth.async_oauth import AsyncOAuth | ||
| from fastapi import FastAPI | ||
| from kinde_sdk.management import ManagementClient | ||
|
|
||
| oauth = AsyncOAuth() | ||
| app = FastAPI() | ||
| management = ManagementClient( | ||
| domain="your-domain.kinde.com", | ||
| client_id="your-m2m-client-id", | ||
| client_secret="your-m2m-client-secret", | ||
| ) | ||
|
|
||
| async def manage_organizations(): | ||
| # Get management client | ||
| management = await oauth.get_management() | ||
|
|
||
| # List organizations | ||
| orgs = await management.get_organizations() | ||
|
|
||
| # Get a specific organization | ||
| org = await management.get_organization(org_id="org_123") | ||
|
|
||
| # Create a new organization | ||
| new_org = await management.create_organization( | ||
| name="My Organization" | ||
| ) | ||
|
|
||
| # Update an organization | ||
| updated_org = await management.update_organization( | ||
| org_id="org_123", | ||
| name="Updated Name" | ||
| ) | ||
|
|
||
| # Delete an organization | ||
| await management.delete_organization(org_id="org_123") | ||
|
|
||
| return orgs | ||
| @app.get("/organizations") | ||
| async def list_organizations(): | ||
| # The management call itself is synchronous | ||
| return management.organizations_api.get_organizations() | ||
| ``` |
There was a problem hiding this comment.
Valid, needs a fix.
Description (required)
Fixes the Python SDK docs to match the SDK.
All code samples checked against the current SDK.
Related issues & labels (optional)
Summary by CodeRabbit