-
Notifications
You must be signed in to change notification settings - Fork 24
feat: More dry approach to API handling #149
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
WalkthroughThe ManagementClient is refactored to dynamically discover and instantiate API classes from a dedicated module, replacing hard-coded endpoint mappings. A token injection wrapper is added to automatically insert Bearer tokens into API calls, and backward-compatibility methods are provided as deprecated proxies to the new API namespaces. Changes
Sequence DiagramsequenceDiagram
participant Client as Application Client
participant MC as ManagementClient
participant Wrapper as Token Injection Wrapper
participant API as Dynamic API Instance<br/>(UsersApi, etc.)
participant Token as ManagementTokenManager
participant AC as ApiClient
Client->>MC: Call api method<br/>(e.g., users.get_users())
MC->>API: Route to dynamic API instance
API->>Wrapper: make_request()
Wrapper->>Token: get_token()
Token-->>Wrapper: Bearer token
Wrapper->>AC: call_api() with<br/>Authorization header
AC->>AC: Execute API call
AC-->>Wrapper: Response
Wrapper-->>API: Return response
API-->>MC: Return result
MC-->>Client: Return result
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes 🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 4
🤖 Fix all issues with AI agents
In @kinde_sdk/management/management_client.py:
- Around line 100-123: The token-injection wrapper in _setup_token_handling must
handle failures from token_manager.get_access_token() and avoid silently
overwriting existing Authorization headers: update call_api_with_token to
try/except around self.token_manager.get_access_token(), raise or return a clear
AuthenticationError (or raise ValueError) if the call raises or returns a falsy
token instead of injecting "Bearer None", and before setting
kwargs['header_params']['Authorization'] check for an existing Authorization
value and either preserve it or log/raise a warning so you don't overwrite it
silently; ensure the error you raise is descriptive (e.g., "Failed to acquire
access token") and replace api_client.call_api only after this robust wrapper is
implemented.
- Line 281: The parameter type for get_organization should explicitly allow
None; update the signature of get_organization to use an explicit Optional type
(e.g., Optional[str] or Python 3.10+ union syntax str | None) instead of relying
on a default None, and add the corresponding import (from typing import
Optional) if using Optional to satisfy PEP 484.
- Around line 185-495: Add explicit Optional[type] annotations for parameters
that default to None: import typing.Optional and the corresponding request
models (e.g., CreateUserRequest, CreateOrganizationRequest, CreateRoleRequest,
CreateFeatureFlagRequest, UpdateUserRequest, UpdateRoleRequest,
UpdateFeatureFlagRequest) and update signatures such as
create_user(create_user_request: Optional[CreateUserRequest] = None, **kwargs),
create_organization(create_organization_request:
Optional[CreateOrganizationRequest] = None, **kwargs),
create_role(create_role_request: Optional[CreateRoleRequest] = None, **kwargs),
create_feature_flag(create_feature_flag_request:
Optional[CreateFeatureFlagRequest] = None, **kwargs), update_user(id: str,
update_user_request: Optional[UpdateUserRequest] = None, **kwargs),
update_role(role_id: str, update_role_request: Optional[UpdateRoleRequest] =
None, **kwargs), and update_feature_flag(feature_flag_key: str,
update_feature_flag_request: Optional[UpdateFeatureFlagRequest] = None,
**kwargs); keep the existing **kwargs forwarding behavior unchanged.
🧹 Nitpick comments (1)
kinde_sdk/management/management_client.py (1)
154-178: Consider handling consecutive uppercase letters in snake_case conversion.The current regex
r'(?<!^)(?=[A-Z])'will convert consecutive uppercase letters individually, which may create non-intuitive attribute names for acronym-heavy classes:
APIsApi→a_p_i_s(might preferapis)MFAApi→m_f_a(might prefermfa)This works correctly but could be refined if more natural names are preferred.
♻️ Optional enhancement for acronym handling
@staticmethod def _class_name_to_snake_case(class_name: str) -> str: """ Convert a class name to snake_case attribute name. Examples: - UsersApi -> users - FeatureFlagsApi -> feature_flags - APIsApi -> apis - MFAApi -> mfa Args: class_name: The class name (e.g., 'UsersApi') Returns: Snake case attribute name (e.g., 'users') """ # Remove the 'Api' suffix name = class_name[:-3] if class_name.endswith('Api') else class_name - # Insert underscores before uppercase letters (except at the start) - # and convert to lowercase - snake_case = re.sub(r'(?<!^)(?=[A-Z])', '_', name).lower() + # Handle sequences of uppercase letters followed by lowercase + # (e.g., "HTTPSConnection" -> "HTTPS_Connection") + s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name) + # Handle transitions from lowercase/digit to uppercase + # (e.g., "getHTTPResponseCode" -> "get_HTTPResponseCode") + snake_case = re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower() return snake_caseThis would produce:
APIsApi→apisMFAApi→mfaHTTPSConnectionApi→https_connection
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
kinde_sdk/management/management_client.py
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-08-21T04:46:50.368Z
Learnt from: KomanRudden
Repo: kinde-oss/kinde-python-sdk PR: 101
File: kinde_sdk/auth/permissions.py:6-7
Timestamp: 2025-08-21T04:46:50.368Z
Learning: The kinde_sdk has both frontend and management API modules. The frontend permissions_api.py exists at kinde_sdk/frontend/api/permissions_api.py and contains PermissionsApi class. Using the management API instead of the frontend API for user permissions would be incorrect.
Applied to files:
kinde_sdk/management/management_client.py
🪛 GitHub Actions: CI
kinde_sdk/management/management_client.py
[error] 231-231: pydantic.ValidationError: create_user received unexpected keyword arguments (email, given_name, family_name) when calling UsersApi.create_user.
🪛 Ruff (0.14.10)
kinde_sdk/management/management_client.py
281-281: PEP 484 prohibits implicit Optional
Convert to T | None
(RUF013)
🔇 Additional comments (1)
kinde_sdk/management/management_client.py (1)
19-20: Theapimodule atkinde_sdk/management/apidoes not exist, causing aModuleNotFoundErrorwhenmanagement_client.pyis imported.Lines 19-20 attempt to import
from kinde_sdk.management import api, but neither theapidirectory nor anapi.pyfile exists in the repository. Additionally, the__init__.pyfile imports classes from this non-existent module (e.g.,from kinde_sdk.management.api.users_api import UsersApi), so the management module cannot be imported at all. Either the api module needs to be created with the expected structure, or the dynamic loading approach needs to be revised to work with the actual module layout.⛔ Skipped due to learnings
Learnt from: KomanRudden Repo: kinde-oss/kinde-python-sdk PR: 101 File: kinde_sdk/auth/permissions.py:6-7 Timestamp: 2025-08-21T04:46:50.368Z Learning: The kinde_sdk has both frontend and management API modules. The frontend permissions_api.py exists at kinde_sdk/frontend/api/permissions_api.py and contains PermissionsApi class. Using the management API instead of the frontend API for user permissions would be incorrect.
| def _setup_token_handling(self): | ||
| """Set up automatic token refresh for API calls.""" | ||
| # Token will be added directly in the API method since we're calling REST client directly | ||
| pass | ||
|
|
||
| def _generate_methods(self): | ||
| """Generate dynamic methods for each API endpoint.""" | ||
| for resource, endpoints in self.API_ENDPOINTS.items(): | ||
| # Handle special cases for singularization | ||
| if resource == 'business': | ||
| resource_singular = 'business' # Don't remove 's' from 'business' | ||
| else: | ||
| resource_singular = resource[:-1] if resource.endswith('s') else resource | ||
|
|
||
| for action, endpoint in endpoints.items(): | ||
| if len(endpoint) == 3: | ||
| method, path, query_keys = endpoint | ||
| else: | ||
| method, path = endpoint | ||
| query_keys = () | ||
|
|
||
| # Create method name based on action and resource | ||
| if action == 'list': | ||
| method_name = f"get_{resource}" | ||
| elif action == 'get': | ||
| method_name = f"get_{resource_singular}" | ||
| else: | ||
| method_name = f"{action}_{resource_singular}" | ||
|
|
||
| # Create the method | ||
| api_method = self._create_api_method(method, path, resource, action, query_keys) | ||
|
|
||
| # Set the method on the class | ||
| setattr(self, method_name, api_method) | ||
|
|
||
| def _create_api_method(self, http_method: str, path: str, resource: str, action: str, query_keys=()) -> Callable: | ||
| """ | ||
| Create a dynamic method for an API endpoint. | ||
| Set up automatic token injection for all API calls. | ||
| Args: | ||
| http_method: HTTP method (GET, POST, etc.) | ||
| path: API endpoint path | ||
| resource: API resource name (users, organizations, etc.) | ||
| action: API action (list, get, create, etc.) | ||
| Returns: | ||
| A callable method that makes the API request | ||
| This modifies the API client to automatically add the Bearer token | ||
| to all outgoing requests. | ||
| """ | ||
| # Handle special cases for singularization | ||
| if resource == 'business': | ||
| resource_singular = 'business' # Don't remove 's' from 'business' | ||
| else: | ||
| resource_singular = resource[:-1] if resource.endswith('s') else resource | ||
|
|
||
| def api_method(*args, **kwargs) -> Dict[str, Any]: | ||
| # Format path with any path parameters from args | ||
| formatted_path = path | ||
| if '{' in path and args: | ||
| param_values = list(args) | ||
| while '{' in formatted_path and param_values: | ||
| start_idx = formatted_path.find('{') | ||
| end_idx = formatted_path.find('}') | ||
| if start_idx >= 0 and end_idx >= 0: | ||
| formatted_path = formatted_path[:start_idx] + str(param_values.pop(0)) + formatted_path[end_idx + 1:] | ||
|
|
||
| # Handle query/body split | ||
| if http_method in ('GET', 'DELETE'): | ||
| query_params = {k: v for k, v in kwargs.items() if v is not None} | ||
| payload = None | ||
| else: | ||
| # Lift ONLY declared query_keys into the query string | ||
| qset = set(query_keys or ()) | ||
| query_params = {k: kwargs.pop(k) for k in list(kwargs) if k in qset and kwargs[k] is not None} | ||
| # Remaining kwargs go to JSON body | ||
| payload = {k: v for k, v in kwargs.items() if v is not None} | ||
|
|
||
| # FIXED: Use param_serialize to properly construct the full URL with host | ||
| # Handle query parameters by appending them to the path | ||
| final_path = formatted_path | ||
| if query_params and http_method in ('GET', 'DELETE'): | ||
| query_string = '&'.join([f"{k}={v}" for k, v in query_params.items() if v is not None]) | ||
| if query_string: | ||
| separator = '&' if '?' in final_path else '?' | ||
| final_path = f"{final_path}{separator}{query_string}" | ||
|
|
||
| # Use param_serialize to get the proper URL with host | ||
| # Remove /api/v1 prefix from resource_path since host already includes it | ||
| resource_path_for_serialize = formatted_path.replace('/api/v1', '', 1) | ||
|
|
||
| method, url, header_params, serialized_body, post_params = self.api_client.param_serialize( | ||
| method=http_method, | ||
| resource_path=resource_path_for_serialize, # Use path without /api/v1 prefix | ||
| query_params=query_params or None, | ||
| header_params={}, | ||
| body=payload if http_method not in ('GET', 'DELETE') else None | ||
| ) | ||
|
|
||
| # Ensure required headers for PATCH/POST/PUT | ||
| if http_method not in ('GET', 'DELETE'): | ||
| header_params.setdefault('Content-Type', 'application/json') | ||
| header_params.setdefault('Accept', 'application/json') | ||
|
|
||
| # Add the authorization token to headers | ||
| original_call_api = self.api_client.call_api | ||
|
|
||
| def call_api_with_token(*args, **kwargs): | ||
| """Wrapper that adds authentication token to all API calls.""" | ||
| # Get the access token | ||
| token = self.token_manager.get_access_token() | ||
| header_params['Authorization'] = f"Bearer {token}" | ||
|
|
||
| # Call the REST client directly with the constructed URL | ||
| response = self.api_client.rest_client.request( | ||
| method=http_method, | ||
| url=url, | ||
| headers=header_params, | ||
| body=serialized_body if http_method not in ('GET', 'DELETE') else None, | ||
| post_params=post_params, | ||
| _request_timeout=None | ||
| ) | ||
|
|
||
| # Use the API client's response_deserialize to properly handle the response | ||
| # First read the response data | ||
| response.read() | ||
|
|
||
| # Then deserialize it | ||
| api_response = self.api_client.response_deserialize(response, self.RESPONSE_TYPES[resource][action]) | ||
| # Inject the token into headers | ||
| if 'header_params' not in kwargs: | ||
| kwargs['header_params'] = {} | ||
| kwargs['header_params']['Authorization'] = f"Bearer {token}" | ||
|
|
||
| return api_response.data | ||
| # Call the original method | ||
| return original_call_api(*args, **kwargs) | ||
|
|
||
| # Add docstring to the method based on the action and resource | ||
| if action == 'list': | ||
| docstring = f""" | ||
| Get a list of {resource}. | ||
| Args: | ||
| **kwargs: Optional arguments to pass to the API. | ||
| sort (str): Sort {resource} by field. (Optional) | ||
| page_size (int): Number of results per page. (Optional) | ||
| next_token (str): Token for the next page of results. (Optional) | ||
| Returns: | ||
| Dict containing {resource} data. | ||
| """ | ||
| elif action == 'get': | ||
| param_name = path.split('{')[-1].split('}')[0] if '{' in path else f"{resource_singular}_id" | ||
| docstring = f""" | ||
| Get a {resource_singular} by ID. | ||
| Args: | ||
| {param_name}: The ID of the {resource_singular} to get. | ||
| Returns: | ||
| Dict containing {resource_singular} data. | ||
| """ | ||
| elif action == 'create': | ||
| docstring = f""" | ||
| Create a new {resource_singular}. | ||
| Args: | ||
| **kwargs: {resource_singular.capitalize()} data to create. | ||
| # Replace the call_api method | ||
| self.api_client.call_api = call_api_with_token |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add error handling for token acquisition and prevent silent header overwrites.
The token injection wrapper has several concerns:
- No error handling if
get_access_token()fails or returnsNone- this will result inAuthorization: Bearer Nonebeing sent. - Silently overwrites any existing Authorization header without checking or warning.
- Token fetch failures will propagate as unclear API errors rather than authentication errors.
🛡️ Proposed fix with error handling
def _setup_token_handling(self):
"""
Set up automatic token injection for all API calls.
This modifies the API client to automatically add the Bearer token
to all outgoing requests.
"""
original_call_api = self.api_client.call_api
def call_api_with_token(*args, **kwargs):
"""Wrapper that adds authentication token to all API calls."""
- # Get the access token
- token = self.token_manager.get_access_token()
+ # Get the access token with error handling
+ try:
+ token = self.token_manager.get_access_token()
+ if not token:
+ raise ValueError("Failed to acquire access token: token is empty")
+ except Exception as e:
+ logger.error(f"Token acquisition failed: {e}")
+ raise RuntimeError(f"Failed to authenticate with Kinde API: {e}") from e
# Inject the token into headers
if 'header_params' not in kwargs:
kwargs['header_params'] = {}
kwargs['header_params']['Authorization'] = f"Bearer {token}"
# Call the original method
return original_call_api(*args, **kwargs)
# Replace the call_api method
self.api_client.call_api = call_api_with_tokenCommittable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In @kinde_sdk/management/management_client.py around lines 100 - 123, The
token-injection wrapper in _setup_token_handling must handle failures from
token_manager.get_access_token() and avoid silently overwriting existing
Authorization headers: update call_api_with_token to try/except around
self.token_manager.get_access_token(), raise or return a clear
AuthenticationError (or raise ValueError) if the call raises or returns a falsy
token instead of injecting "Bearer None", and before setting
kwargs['header_params']['Authorization'] check for an existing Authorization
value and either preserve it or log/raise a warning so you don't overwrite it
silently; ensure the error you raise is descriptive (e.g., "Failed to acquire
access token") and replace api_client.call_api only after this robust wrapper is
implemented.
| def _initialize_api_classes(self): | ||
| """ | ||
| Dynamically initialize all API classes from the api module. | ||
| This method inspects the api module and creates an instance of each | ||
| API class (classes ending with 'Api'), making them available as | ||
| snake_case attributes on this client. | ||
| For example: | ||
| - UsersApi -> self.users | ||
| - OrganizationsApi -> self.organizations | ||
| - FeatureFlagsApi -> self.feature_flags | ||
| """ | ||
| # Get all members of the api module | ||
| for name, obj in inspect.getmembers(api): | ||
| # Check if it's a class and ends with 'Api' | ||
| if inspect.isclass(obj) and name.endswith('Api'): | ||
| # Convert class name to snake_case attribute name | ||
| # e.g., UsersApi -> users, FeatureFlagsApi -> feature_flags | ||
| attr_name = self._class_name_to_snake_case(name) | ||
|
|
||
| Returns: | ||
| Dict containing the created {resource_singular}. | ||
| """ | ||
| elif action == 'update': | ||
| param_name = path.split('{')[-1].split('}')[0] if '{' in path else f"{resource_singular}_id" | ||
| docstring = f""" | ||
| Update a {resource_singular}. | ||
| Args: | ||
| {param_name}: The ID of the {resource_singular} to update. | ||
| **kwargs: {resource_singular.capitalize()} data to update. | ||
| # Initialize the API class with our configured api_client | ||
| api_instance = obj(api_client=self.api_client) | ||
|
|
||
| Returns: | ||
| Dict containing the updated {resource_singular}. | ||
| """ | ||
| elif action == 'delete': | ||
| param_name = path.split('{')[-1].split('}')[0] if '{' in path else f"{resource_singular}_id" | ||
| docstring = f""" | ||
| Delete a {resource_singular}. | ||
| Args: | ||
| {param_name}: The ID of the {resource_singular} to delete. | ||
| # Set it as an attribute on this client | ||
| setattr(self, attr_name, api_instance) | ||
|
|
||
| Returns: | ||
| Dict containing the result. | ||
| """ | ||
| else: | ||
| docstring = f""" | ||
| {action.capitalize()} {resource}. | ||
| logger.debug(f"Initialized {name} as client.{attr_name}") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion | 🟠 Major
Add error handling and validation for dynamic API class loading.
The dynamic API initialization has several robustness concerns:
- No error handling if an API class fails to instantiate - this could happen if a class requires additional constructor parameters beyond
api_client. - No validation that
attr_namedoesn't collide with existing ManagementClient attributes (e.g.,domain,api_client,configuration). - Silent success if no API classes are found - the client would appear to work but have no methods.
- No verification that discovered classes are actually instantiable or are the expected API classes.
♻️ Proposed fix with validation and error handling
def _initialize_api_classes(self):
"""
Dynamically initialize all API classes from the api module.
This method inspects the api module and creates an instance of each
API class (classes ending with 'Api'), making them available as
snake_case attributes on this client.
For example:
- UsersApi -> self.users
- OrganizationsApi -> self.organizations
- FeatureFlagsApi -> self.feature_flags
"""
+ # Track loaded APIs for verification
+ loaded_apis = []
+
# Get all members of the api module
for name, obj in inspect.getmembers(api):
# Check if it's a class and ends with 'Api'
if inspect.isclass(obj) and name.endswith('Api'):
# Convert class name to snake_case attribute name
# e.g., UsersApi -> users, FeatureFlagsApi -> feature_flags
attr_name = self._class_name_to_snake_case(name)
+
+ # Check for attribute collisions
+ if hasattr(self, attr_name):
+ logger.warning(
+ f"Attribute '{attr_name}' already exists on ManagementClient, "
+ f"skipping {name}"
+ )
+ continue
- # Initialize the API class with our configured api_client
- api_instance = obj(api_client=self.api_client)
+ # Initialize the API class with error handling
+ try:
+ api_instance = obj(api_client=self.api_client)
+ except Exception as e:
+ logger.error(f"Failed to initialize {name}: {e}")
+ raise RuntimeError(
+ f"Failed to initialize API class {name}. "
+ f"This may indicate a compatibility issue with the generated API."
+ ) from e
# Set it as an attribute on this client
setattr(self, attr_name, api_instance)
+ loaded_apis.append(attr_name)
logger.debug(f"Initialized {name} as client.{attr_name}")
+
+ # Verify at least some APIs were loaded
+ if not loaded_apis:
+ raise RuntimeError(
+ "No API classes found in kinde_sdk.management.api module. "
+ "This may indicate the SDK was not properly installed or generated."
+ )
+
+ logger.info(f"Loaded {len(loaded_apis)} API classes: {', '.join(sorted(loaded_apis))}")| def get_users(self, **kwargs): | ||
| """ | ||
| Get users. | ||
| .. deprecated:: | ||
| Use :meth:`client.users.get_users()` instead. | ||
| For full documentation and parameters, see UsersApi.get_users() | ||
| """ | ||
| warnings.warn( | ||
| "get_users() is deprecated. Use client.users.get_users() instead.", | ||
| DeprecationWarning, | ||
| stacklevel=2 | ||
| ) | ||
| return self.users.get_users(**kwargs) | ||
|
|
||
| def get_user_data(self, user_id: str): | ||
| """ | ||
| Get user data by ID. | ||
| .. deprecated:: | ||
| Use :meth:`client.users.get_user_data()` instead. | ||
| For full documentation and parameters, see UsersApi.get_user_data() | ||
| """ | ||
| warnings.warn( | ||
| "get_user_data() is deprecated. Use client.users.get_user_data() instead.", | ||
| DeprecationWarning, | ||
| stacklevel=2 | ||
| ) | ||
| return self.users.get_user_data(user_id=user_id) | ||
|
|
||
| def create_user(self, create_user_request=None, **kwargs): | ||
| """ | ||
| Create a user. | ||
| .. deprecated:: | ||
| Use :meth:`client.users.create_user()` instead. | ||
| For full documentation and parameters, see UsersApi.create_user() | ||
| """ | ||
| warnings.warn( | ||
| "create_user() is deprecated. Use client.users.create_user() instead.", | ||
| DeprecationWarning, | ||
| stacklevel=2 | ||
| ) | ||
| return self.users.create_user(create_user_request=create_user_request, **kwargs) | ||
|
|
||
| def update_user(self, id: str, update_user_request, **kwargs): | ||
| """ | ||
| Update a user. | ||
| .. deprecated:: | ||
| Use :meth:`client.users.update_user()` instead. | ||
| For full documentation and parameters, see UsersApi.update_user() | ||
| """ | ||
| warnings.warn( | ||
| "update_user() is deprecated. Use client.users.update_user() instead.", | ||
| DeprecationWarning, | ||
| stacklevel=2 | ||
| ) | ||
| return self.users.update_user(id=id, update_user_request=update_user_request, **kwargs) | ||
|
|
||
| def delete_user(self, id: str, **kwargs): | ||
| """ | ||
| Delete a user. | ||
| .. deprecated:: | ||
| Use :meth:`client.users.delete_user()` instead. | ||
| For full documentation and parameters, see UsersApi.delete_user() | ||
| """ | ||
| warnings.warn( | ||
| "delete_user() is deprecated. Use client.users.delete_user() instead.", | ||
| DeprecationWarning, | ||
| stacklevel=2 | ||
| ) | ||
| return self.users.delete_user(id=id, **kwargs) | ||
|
|
||
| def get_organizations(self, **kwargs): | ||
| """ | ||
| Get organizations. | ||
| .. deprecated:: | ||
| Use :meth:`client.organizations.get_organizations()` instead. | ||
| For full documentation and parameters, see OrganizationsApi.get_organizations() | ||
| """ | ||
| warnings.warn( | ||
| "get_organizations() is deprecated. Use client.organizations.get_organizations() instead.", | ||
| DeprecationWarning, | ||
| stacklevel=2 | ||
| ) | ||
| return self.organizations.get_organizations(**kwargs) | ||
|
|
||
| def get_organization(self, code: str = None, **kwargs): | ||
| """ | ||
| Get an organization. | ||
| .. deprecated:: | ||
| Use :meth:`client.organizations.get_organization()` instead. | ||
| For full documentation and parameters, see OrganizationsApi.get_organization() | ||
| """ | ||
| warnings.warn( | ||
| "get_organization() is deprecated. Use client.organizations.get_organization() instead.", | ||
| DeprecationWarning, | ||
| stacklevel=2 | ||
| ) | ||
| return self.organizations.get_organization(code=code, **kwargs) | ||
|
|
||
| def create_organization(self, create_organization_request, **kwargs): | ||
| """ | ||
| Create an organization. | ||
| .. deprecated:: | ||
| Use :meth:`client.organizations.create_organization()` instead. | ||
| For full documentation and parameters, see OrganizationsApi.create_organization() | ||
| """ | ||
| warnings.warn( | ||
| "create_organization() is deprecated. Use client.organizations.create_organization() instead.", | ||
| DeprecationWarning, | ||
| stacklevel=2 | ||
| ) | ||
| return self.organizations.create_organization( | ||
| create_organization_request=create_organization_request, **kwargs | ||
| ) | ||
|
|
||
| def update_organization(self, org_code: str, **kwargs): | ||
| """ | ||
| Update an organization. | ||
| .. deprecated:: | ||
| Use :meth:`client.organizations.update_organization()` instead. | ||
| For full documentation and parameters, see OrganizationsApi.update_organization() | ||
| """ | ||
| warnings.warn( | ||
| "update_organization() is deprecated. Use client.organizations.update_organization() instead.", | ||
| DeprecationWarning, | ||
| stacklevel=2 | ||
| ) | ||
| return self.organizations.update_organization(org_code=org_code, **kwargs) | ||
|
|
||
| def delete_organization(self, org_code: str, **kwargs): | ||
| """ | ||
| Delete an organization. | ||
| .. deprecated:: | ||
| Use :meth:`client.organizations.delete_organization()` instead. | ||
| For full documentation and parameters, see OrganizationsApi.delete_organization() | ||
| """ | ||
| warnings.warn( | ||
| "delete_organization() is deprecated. Use client.organizations.delete_organization() instead.", | ||
| DeprecationWarning, | ||
| stacklevel=2 | ||
| ) | ||
| return self.organizations.delete_organization(org_code=org_code, **kwargs) | ||
|
|
||
| def get_roles(self, **kwargs): | ||
| """ | ||
| Get roles. | ||
| .. deprecated:: | ||
| Use :meth:`client.roles.get_roles()` instead. | ||
| For full documentation and parameters, see RolesApi.get_roles() | ||
| """ | ||
| warnings.warn( | ||
| "get_roles() is deprecated. Use client.roles.get_roles() instead.", | ||
| DeprecationWarning, | ||
| stacklevel=2 | ||
| ) | ||
| return self.roles.get_roles(**kwargs) | ||
|
|
||
| def get_role(self, role_id: str): | ||
| """ | ||
| Get a role. | ||
| .. deprecated:: | ||
| Use :meth:`client.roles.get_role()` instead. | ||
| For full documentation and parameters, see RolesApi.get_role() | ||
| """ | ||
| warnings.warn( | ||
| "get_role() is deprecated. Use client.roles.get_role() instead.", | ||
| DeprecationWarning, | ||
| stacklevel=2 | ||
| ) | ||
| return self.roles.get_role(role_id=role_id) | ||
|
|
||
| def create_role(self, create_role_request=None, **kwargs): | ||
| """ | ||
| Create a role. | ||
| .. deprecated:: | ||
| Use :meth:`client.roles.create_role()` instead. | ||
| For full documentation and parameters, see RolesApi.create_role() | ||
| """ | ||
| warnings.warn( | ||
| "create_role() is deprecated. Use client.roles.create_role() instead.", | ||
| DeprecationWarning, | ||
| stacklevel=2 | ||
| ) | ||
| return self.roles.create_role(create_role_request=create_role_request, **kwargs) | ||
|
|
||
| def update_role(self, role_id: str, update_role_request=None, **kwargs): | ||
| """ | ||
| Update a role. | ||
| .. deprecated:: | ||
| Use :meth:`client.roles.update_role()` instead. | ||
| For full documentation and parameters, see RolesApi.update_role() | ||
| """ | ||
| warnings.warn( | ||
| "update_role() is deprecated. Use client.roles.update_role() instead.", | ||
| DeprecationWarning, | ||
| stacklevel=2 | ||
| ) | ||
| return self.roles.update_role(role_id=role_id, update_role_request=update_role_request, **kwargs) | ||
|
|
||
| def delete_role(self, role_id: str): | ||
| """ | ||
| Delete a role. | ||
| .. deprecated:: | ||
| Use :meth:`client.roles.delete_role()` instead. | ||
| For full documentation and parameters, see RolesApi.delete_role() | ||
| """ | ||
| warnings.warn( | ||
| "delete_role() is deprecated. Use client.roles.delete_role() instead.", | ||
| DeprecationWarning, | ||
| stacklevel=2 | ||
| ) | ||
| return self.roles.delete_role(role_id=role_id) | ||
|
|
||
| def get_feature_flags(self, **kwargs): | ||
| """ | ||
| Get feature flags. | ||
| .. deprecated:: | ||
| Use :meth:`client.feature_flags.get_feature_flags()` instead. | ||
| For full documentation and parameters, see FeatureFlagsApi.get_feature_flags() | ||
| """ | ||
| warnings.warn( | ||
| "get_feature_flags() is deprecated. Use client.feature_flags.get_feature_flags() instead.", | ||
| DeprecationWarning, | ||
| stacklevel=2 | ||
| ) | ||
| return self.feature_flags.get_feature_flags(**kwargs) | ||
|
|
||
| def create_feature_flag(self, create_feature_flag_request=None, **kwargs): | ||
| """ | ||
| Create a feature flag. | ||
| .. deprecated:: | ||
| Use :meth:`client.feature_flags.create_feature_flag()` instead. | ||
| For full documentation and parameters, see FeatureFlagsApi.create_feature_flag() | ||
| """ | ||
| warnings.warn( | ||
| "create_feature_flag() is deprecated. Use client.feature_flags.create_feature_flag() instead.", | ||
| DeprecationWarning, | ||
| stacklevel=2 | ||
| ) | ||
| return self.feature_flags.create_feature_flag( | ||
| create_feature_flag_request=create_feature_flag_request, **kwargs | ||
| ) | ||
|
|
||
| def update_feature_flag(self, feature_flag_key: str, update_feature_flag_request=None, **kwargs): | ||
| """ | ||
| Update a feature flag. | ||
| .. deprecated:: | ||
| Use :meth:`client.feature_flags.update_feature_flag()` instead. | ||
| For full documentation and parameters, see FeatureFlagsApi.update_feature_flag() | ||
| """ | ||
| warnings.warn( | ||
| "update_feature_flag() is deprecated. Use client.feature_flags.update_feature_flag() instead.", | ||
| DeprecationWarning, | ||
| stacklevel=2 | ||
| ) | ||
| return self.feature_flags.update_feature_flag( | ||
| feature_flag_key=feature_flag_key, | ||
| update_feature_flag_request=update_feature_flag_request, | ||
| **kwargs | ||
| ) | ||
|
|
||
| def delete_feature_flag(self, feature_flag_key: str): | ||
| """ | ||
| Delete a feature flag. | ||
| .. deprecated:: | ||
| Use :meth:`client.feature_flags.delete_feature_flag()` instead. | ||
| For full documentation and parameters, see FeatureFlagsApi.delete_feature_flag() | ||
| """ | ||
| warnings.warn( | ||
| "delete_feature_flag() is deprecated. Use client.feature_flags.delete_feature_flag() instead.", | ||
| DeprecationWarning, | ||
| stacklevel=2 | ||
| ) | ||
| return self.feature_flags.delete_feature_flag(feature_flag_key=feature_flag_key) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
find kinde_sdk/management/api -type f -name "*.py" | head -20Repository: kinde-oss/kinde-python-sdk
Length of output: 129
🏁 Script executed:
ls -la kinde_sdk/management/Repository: kinde-oss/kinde-python-sdk
Length of output: 965
🏁 Script executed:
rg "class.*Api" kinde_sdk/management/api --type py -B 2 | head -100Repository: kinde-oss/kinde-python-sdk
Length of output: 134
🏁 Script executed:
rg "class (UsersApi|OrganizationsApi|RolesApi|FeatureFlagsApi)" --type pyRepository: kinde-oss/kinde-python-sdk
Length of output: 52
🏁 Script executed:
head -100 kinde_sdk/management/__init__.pyRepository: kinde-oss/kinde-python-sdk
Length of output: 6907
🏁 Script executed:
rg "UsersApi|OrganizationsApi|RolesApi|FeatureFlagsApi" kinde_sdk/management/management_client.py -B 5 -A 5Repository: kinde-oss/kinde-python-sdk
Length of output: 9788
🏁 Script executed:
find . -path "./kinde_sdk/management/api/*.py" -type f | head -20Repository: kinde-oss/kinde-python-sdk
Length of output: 52
🏁 Script executed:
ls -la kinde_sdk/management/api/ 2>/dev/null || echo "Checking alternative locations..."Repository: kinde-oss/kinde-python-sdk
Length of output: 103
🏁 Script executed:
rg "def create_user|def update_user|def get_users" --type py -A 10 | head -150Repository: kinde-oss/kinde-python-sdk
Length of output: 3630
🏁 Script executed:
find . -name "*.py" -type f -exec grep -l "class UsersApi" {} \;Repository: kinde-oss/kinde-python-sdk
Length of output: 52
🏁 Script executed:
git ls-files | grep -i api | head -30Repository: kinde-oss/kinde-python-sdk
Length of output: 1251
🏁 Script executed:
ls -la kinde_sdk/Repository: kinde-oss/kinde-python-sdk
Length of output: 582
🏁 Script executed:
cat .openapi-generator/FILES | grep -i "api" | head -40Repository: kinde-oss/kinde-python-sdk
Length of output: 1636
🏁 Script executed:
ls -la .openapi-generator/Repository: kinde-oss/kinde-python-sdk
Length of output: 301
🏁 Script executed:
grep -r "class.*Api" kinde_sdk/ --type py | head -20Repository: kinde-oss/kinde-python-sdk
Length of output: 187
🏁 Script executed:
grep -n "def create_user\|def update_user\|def get_users" kinde_sdk/apis/tags/users_api.py -A 15Repository: kinde-oss/kinde-python-sdk
Length of output: 135
🏁 Script executed:
grep -n "def create_organization\|def update_organization" kinde_sdk/apis/tags/organizations_api.py -A 15Repository: kinde-oss/kinde-python-sdk
Length of output: 143
🏁 Script executed:
grep -n "def create_role\|def update_role" kinde_sdk/apis/tags/roles_api.py -A 15Repository: kinde-oss/kinde-python-sdk
Length of output: 135
🏁 Script executed:
grep -n "def create_feature_flag\|def update_feature_flag" kinde_sdk/apis/tags/feature_flags_api.py -A 15Repository: kinde-oss/kinde-python-sdk
Length of output: 143
🏁 Script executed:
ls -la kinde_sdk/apis/tags/ 2>/dev/null | head -20Repository: kinde-oss/kinde-python-sdk
Length of output: 52
🏁 Script executed:
find kinde_sdk -name "users_api.py" -type fRepository: kinde-oss/kinde-python-sdk
Length of output: 52
🏁 Script executed:
pwd && git statusRepository: kinde-oss/kinde-python-sdk
Length of output: 352
🏁 Script executed:
git sparse-checkout listRepository: kinde-oss/kinde-python-sdk
Length of output: 1374
🏁 Script executed:
git ls-files | grep "api" | wc -lRepository: kinde-oss/kinde-python-sdk
Length of output: 73
🏁 Script executed:
cat kinde_sdk/management/__init__.py | head -50Repository: kinde-oss/kinde-python-sdk
Length of output: 2737
🏁 Script executed:
wc -l kinde_sdk/management/management_client.pyRepository: kinde-oss/kinde-python-sdk
Length of output: 115
🏁 Script executed:
head -100 kinde_sdk/management/management_client.pyRepository: kinde-oss/kinde-python-sdk
Length of output: 3608
🏁 Script executed:
sed -n '1,184p' kinde_sdk/management/management_client.pyRepository: kinde-oss/kinde-python-sdk
Length of output: 6933
🏁 Script executed:
sed -n '184,250p' kinde_sdk/management/management_client.pyRepository: kinde-oss/kinde-python-sdk
Length of output: 2238
🏁 Script executed:
grep -n "def " kinde_sdk/management/management_client.py | tail -30Repository: kinde-oss/kinde-python-sdk
Length of output: 1509
🏁 Script executed:
cat kinde_sdk/management/management_client.py | sed -n '185,495p' > /tmp/deprecated_methods.txt && wc -l /tmp/deprecated_methods.txtRepository: kinde-oss/kinde-python-sdk
Length of output: 101
🏁 Script executed:
find . -name "*.py" -type f -path "*/test*" -o -path "*/example*" | head -20Repository: kinde-oss/kinde-python-sdk
Length of output: 842
🏁 Script executed:
grep -r "create_user\|update_user" --include="*.py" | grep -E "(test|example)" | head -20Repository: kinde-oss/kinde-python-sdk
Length of output: 2106
🏁 Script executed:
grep -n "pipeline\|failed\|error" kinde_sdk/management/management_client.pyRepository: kinde-oss/kinde-python-sdk
Length of output: 52
🏁 Script executed:
cat testv2/testv2_management/test_management_client.py | head -200Repository: kinde-oss/kinde-python-sdk
Length of output: 9100
🏁 Script executed:
cat testv2/testv2_management/test_management_client.py | grep -A 30 "test_create_user_with_body_data"Repository: kinde-oss/kinde-python-sdk
Length of output: 1655
🏁 Script executed:
cat examples/management_client_example.pyRepository: kinde-oss/kinde-python-sdk
Length of output: 11360
🏁 Script executed:
python3 << 'EOF'
import re
# Read the management_client.py file
with open('kinde_sdk/management/management_client.py', 'r') as f:
content = f.read()
# Extract all deprecated wrapper methods
pattern = r'def (\w+)\(self(?:, ([^)]*))?\):'
matches = re.findall(pattern, content)
# Focus on the deprecated methods (lines 185+)
deprecated_start = content.find('def get_users(self')
deprecated_section = content[deprecated_start:]
# Count methods and analyze kwargs patterns
kwargs_methods = []
for match in re.finditer(r'def (\w+)\(self([^)]*)\):', deprecated_section):
method_name = match.group(1)
params = match.group(2)
if '**kwargs' in params:
kwargs_methods.append((method_name, params.strip()))
print(f"Total deprecated methods with **kwargs: {len(kwargs_methods)}")
print("\nMethods and their signatures:")
for method, params in kwargs_methods[:10]:
print(f" {method}({params})")
EOFRepository: kinde-oss/kinde-python-sdk
Length of output: 576
🌐 Web query:
Kinde Python SDK OpenAPI generated API kwargs handling
💡 Result:
Short answer: Kinde’s Python SDK uses OpenAPI-generated client methods that accept standard OpenAPI-generator **kwargs (e.g. async_req, _preload_content, _return_http_data_only, _request_timeout, _request_auth). Pass these kwargs to control async behavior, timeouts, response form, and per-request auth; the SDK’s higher-level ManagementClient/clients simply call those generated methods under the hood. [1][2][3]
Common kwargs (supported by OpenAPI‑generated clients)
- async_req=True — run request in background thread and return a thread/future. [2]
- _return_http_data_only=True/False — return only parsed data vs (data, status, headers). [2]
- _preload_content=False — return raw urllib3.HTTPResponse without decoding. [2]
- _request_timeout=(connect_timeout, read_timeout) or single number. [2]
- _request_auth=dict — override auth for that call. [2]
- Any endpoint-specific optional parameters are passed as named args or in **kwargs.
Example (concise)
- Synchronous, only data:
client.some_api_call(required1=val, optional2=val, _return_http_data_only=True) - Async with timeout:
thread = client.some_api_call(required1=val, async_req=True, _request_timeout=(3, 10))
result = thread.get()
Notes
- The Kinde SDK exposes higher-level async helpers; consult the SDK docs if you prefer their async/await wrappers. [3]
- If you need custom headers or auth per call, use the generated client kwargs (_request_auth) or set headers on the ApiClient instance. [2]
Sources
- Kinde Python SDK (GitHub). [1]
- OpenAPI‑generated Python client method kwargs documentation (example: kubernetes/openapi-generated docs). [2]
- Kinde Python SDK docs (Python SDK / Management API usage). [3]
Add type hints to request parameter defaults for clarity.
Several request parameters default to None without proper type annotations:
create_user(self, create_user_request=None, **kwargs)create_organization(self, create_organization_request, **kwargs)create_role(self, create_role_request=None, **kwargs)create_feature_flag(self, create_feature_flag_request=None, **kwargs)update_user(self, id: str, update_user_request, **kwargs)update_role(self, role_id: str, update_role_request=None, **kwargs)update_feature_flag(self, feature_flag_key: str, update_feature_flag_request=None, **kwargs)
Import the request model types and annotate: create_user_request: Optional[CreateUserRequest] = None.
The **kwargs forwarding is correct—OpenAPI-generated methods are designed to accept standard kwargs (e.g., async_req, _request_timeout, _return_http_data_only).
🧰 Tools
🪛 GitHub Actions: CI
[error] 231-231: pydantic.ValidationError: create_user received unexpected keyword arguments (email, given_name, family_name) when calling UsersApi.create_user.
🪛 Ruff (0.14.10)
281-281: PEP 484 prohibits implicit Optional
Convert to T | None
(RUF013)
🤖 Prompt for AI Agents
In @kinde_sdk/management/management_client.py around lines 185 - 495, Add
explicit Optional[type] annotations for parameters that default to None: import
typing.Optional and the corresponding request models (e.g., CreateUserRequest,
CreateOrganizationRequest, CreateRoleRequest, CreateFeatureFlagRequest,
UpdateUserRequest, UpdateRoleRequest, UpdateFeatureFlagRequest) and update
signatures such as create_user(create_user_request: Optional[CreateUserRequest]
= None, **kwargs), create_organization(create_organization_request:
Optional[CreateOrganizationRequest] = None, **kwargs),
create_role(create_role_request: Optional[CreateRoleRequest] = None, **kwargs),
create_feature_flag(create_feature_flag_request:
Optional[CreateFeatureFlagRequest] = None, **kwargs), update_user(id: str,
update_user_request: Optional[UpdateUserRequest] = None, **kwargs),
update_role(role_id: str, update_role_request: Optional[UpdateRoleRequest] =
None, **kwargs), and update_feature_flag(feature_flag_key: str,
update_feature_flag_request: Optional[UpdateFeatureFlagRequest] = None,
**kwargs); keep the existing **kwargs forwarding behavior unchanged.
| ) | ||
| return self.organizations.get_organizations(**kwargs) | ||
|
|
||
| def get_organization(self, code: str = None, **kwargs): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix implicit Optional type hint.
PEP 484 requires explicit Optional or union syntax when a parameter defaults to None.
🔧 Proposed fix
- def get_organization(self, code: str = None, **kwargs):
+ def get_organization(self, code: Optional[str] = None, **kwargs):Or using Python 3.10+ union syntax:
- def get_organization(self, code: str = None, **kwargs):
+ def get_organization(self, code: str | None = None, **kwargs):📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| def get_organization(self, code: str = None, **kwargs): | |
| def get_organization(self, code: Optional[str] = None, **kwargs): |
| def get_organization(self, code: str = None, **kwargs): | |
| def get_organization(self, code: str | None = None, **kwargs): |
🧰 Tools
🪛 Ruff (0.14.10)
281-281: PEP 484 prohibits implicit Optional
Convert to T | None
(RUF013)
🤖 Prompt for AI Agents
In @kinde_sdk/management/management_client.py at line 281, The parameter type
for get_organization should explicitly allow None; update the signature of
get_organization to use an explicit Optional type (e.g., Optional[str] or Python
3.10+ union syntax str | None) instead of relying on a default None, and add the
corresponding import (from typing import Optional) if using Optional to satisfy
PEP 484.
Explain your changes
This updates how the management client is initialised.
Instead of defining each route, it handles directly from the generated code.
Checklist
🛟 If you need help, consider asking for advice over in the Kinde community.