-
Notifications
You must be signed in to change notification settings - Fork 33
feat: add raw requestor for calling arbitrary endpoints #252
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
Open
kcbiradar
wants to merge
8
commits into
openfga:main
Choose a base branch
from
kcbiradar:feat/add-raw-request-method
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
f4dec14
feat: add raw_request method
kcbiradar 3c2e66a
update: handling double-JSON-encodes
kcbiradar 3f85dfa
addRawRequestMethod: fix testcases & remove trailing spaces
kcbiradar e9c47d9
Merge branch 'main' into feat/add-raw-request-method
SoulPancake b6b72aa
addRawRequestMethod: update-changes-requested
kcbiradar 6188d9e
updates
kcbiradar a34de52
updated-requested-changes
kcbiradar 5cc73ea
update-suggested-changes
kcbiradar File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,10 @@ | ||
| import asyncio | ||
| import json | ||
| import urllib.parse | ||
| import uuid | ||
|
|
||
| from typing import Any | ||
|
|
||
| from openfga_sdk.api.open_fga_api import OpenFgaApi | ||
| from openfga_sdk.api_client import ApiClient | ||
| from openfga_sdk.client.configuration import ClientConfiguration | ||
|
|
@@ -25,6 +29,7 @@ | |
| from openfga_sdk.client.models.list_objects_request import ClientListObjectsRequest | ||
| from openfga_sdk.client.models.list_relations_request import ClientListRelationsRequest | ||
| from openfga_sdk.client.models.list_users_request import ClientListUsersRequest | ||
| from openfga_sdk.client.models.raw_response import RawResponse | ||
| from openfga_sdk.client.models.read_changes_request import ClientReadChangesRequest | ||
| from openfga_sdk.client.models.tuple import ClientTuple, convert_tuple_keys | ||
| from openfga_sdk.client.models.write_request import ClientWriteRequest | ||
|
|
@@ -67,6 +72,10 @@ | |
| WriteAuthorizationModelRequest, | ||
| ) | ||
| from openfga_sdk.models.write_request import WriteRequest | ||
| from openfga_sdk.rest import RESTResponse | ||
| from openfga_sdk.telemetry.attributes import ( | ||
| TelemetryAttributes, | ||
| ) | ||
| from openfga_sdk.validation import is_well_formed_ulid_string | ||
|
|
||
|
|
||
|
|
@@ -1096,3 +1105,160 @@ def map_to_assertion(client_assertion: ClientAssertion): | |
| authorization_model_id, api_request_body, **kwargs | ||
| ) | ||
| return api_response | ||
|
|
||
| ####################### | ||
| # Raw Request | ||
| ####################### | ||
| async def raw_request( | ||
| self, | ||
| method: str, | ||
| path: str, | ||
| query_params: dict[str, str | int | list[str | int]] | None = None, | ||
| path_params: dict[str, str] | None = None, | ||
| headers: dict[str, str] | None = None, | ||
| body: dict[str, Any] | list[Any] | str | bytes | None = None, | ||
| operation_name: str | None = None, | ||
| options: dict[str, int | str | dict[str, int | str]] | None = None, | ||
| ) -> RawResponse: | ||
| """ | ||
| Make a raw HTTP request to any OpenFGA API endpoint. | ||
|
|
||
| :param method: HTTP method (GET, POST, PUT, DELETE, PATCH, etc.) | ||
| :param path: API endpoint path (e.g., "/stores/{store_id}/check" or "/stores") | ||
| :param query_params: Optional query parameters as a dictionary | ||
| :param path_params: Optional path parameters to replace placeholders in path | ||
| (e.g., {"store_id": "abc", "model_id": "xyz"}) | ||
| :param headers: Optional request headers (will be merged with default headers) | ||
| :param body: Optional request body (dict/list will be JSON serialized, str/bytes sent as-is) | ||
| :param operation_name: Required operation name for telemetry/logging (e.g., "Check", "Write", "CustomEndpoint") | ||
| :param options: Optional request options: | ||
| - headers: Additional headers (merged with headers parameter) | ||
| - retry_params: Override retry parameters for this request | ||
| - authorization_model_id: Not used in raw_request, but kept for consistency | ||
| :return: RawResponse object with status, headers, and body | ||
| :raises FgaValidationException: If path contains {store_id} but store_id is not configured | ||
| :raises ApiException: For HTTP errors (with SDK error handling applied) | ||
| """ | ||
|
|
||
| request_headers = dict(headers) if headers else {} | ||
| if options and options.get("headers"): | ||
| if isinstance(options["headers"], dict): | ||
| request_headers.update(options["headers"]) | ||
|
|
||
| if not operation_name: | ||
| raise FgaValidationException("operation_name is required for raw_request") | ||
|
|
||
| resource_path = path | ||
| path_params_dict = dict(path_params) if path_params else {} | ||
|
|
||
| if "{store_id}" in resource_path and "store_id" not in path_params_dict: | ||
| store_id = self.get_store_id() | ||
| if store_id is None or store_id == "": | ||
| raise FgaValidationException( | ||
| "Path contains {store_id} but store_id is not configured. " | ||
| "Set store_id in ClientConfiguration, use set_store_id(), or provide it in path_params." | ||
| ) | ||
| path_params_dict["store_id"] = store_id | ||
|
|
||
| for param_name, param_value in path_params_dict.items(): | ||
| placeholder = f"{{{param_name}}}" | ||
| if placeholder in resource_path: | ||
| encoded_value = urllib.parse.quote(str(param_value), safe="") | ||
| resource_path = resource_path.replace(placeholder, encoded_value) | ||
kcbiradar marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| if "{" in resource_path or "}" in resource_path: | ||
| raise FgaValidationException( | ||
| f"Not all path parameters were provided for path: {path}" | ||
| ) | ||
|
|
||
| query_params_list = [] | ||
| if query_params: | ||
| for key, value in query_params.items(): | ||
| if value is None: | ||
| continue | ||
| if isinstance(value, list): | ||
| for item in value: | ||
| if item is not None: | ||
| query_params_list.append((key, str(item))) | ||
| continue | ||
| query_params_list.append((key, str(value))) | ||
|
|
||
| body_params = body | ||
kcbiradar marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| if "Content-Type" not in request_headers: | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| request_headers["Content-Type"] = "application/json" | ||
|
|
||
| retry_params = None | ||
| if options and options.get("retry_params"): | ||
| retry_params = options["retry_params"] | ||
| if "Accept" not in request_headers: | ||
| request_headers["Accept"] = "application/json" | ||
|
|
||
| auth_headers = dict(request_headers) if request_headers else {} | ||
| await self._api_client.update_params_for_auth( | ||
| auth_headers, | ||
| query_params_list, | ||
| auth_settings=[], | ||
| oauth2_client=self._api._oauth2_client, | ||
| ) | ||
|
|
||
| telemetry_attributes = None | ||
| if operation_name: | ||
| telemetry_attributes = { | ||
| TelemetryAttributes.fga_client_request_method: operation_name.lower(), | ||
| } | ||
| if self.get_store_id(): | ||
| telemetry_attributes[ | ||
| TelemetryAttributes.fga_client_request_store_id | ||
| ] = self.get_store_id() | ||
|
|
||
| await self._api_client.call_api( | ||
| resource_path=resource_path, | ||
| method=method.upper(), | ||
| query_params=query_params_list if query_params_list else None, | ||
| header_params=auth_headers if auth_headers else None, | ||
| body=body_params, | ||
| response_types_map={}, | ||
| auth_settings=[], | ||
| _return_http_data_only=True, | ||
| _preload_content=True, | ||
| _retry_params=retry_params, | ||
| _oauth2_client=self._api._oauth2_client, | ||
| _telemetry_attributes=telemetry_attributes, | ||
| ) | ||
| rest_response: RESTResponse | None = getattr( | ||
| self._api_client, "last_response", None | ||
| ) | ||
|
|
||
| if rest_response is None: | ||
| operation_suffix = ( | ||
SoulPancake marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| f" (operation: {operation_name})" if operation_name else "" | ||
| ) | ||
| raise RuntimeError( | ||
| f"Failed to get response from API client for {method.upper()} " | ||
| f"request to '{resource_path}'{operation_suffix}. " | ||
| "This may indicate an internal SDK error, network problem, or client configuration issue." | ||
| ) | ||
|
|
||
| response_body: bytes | str | dict[str, Any] | None = None | ||
| if rest_response.data is not None: | ||
| if isinstance(rest_response.data, str): | ||
| try: | ||
| response_body = json.loads(rest_response.data) | ||
| except (json.JSONDecodeError, ValueError): | ||
| response_body = rest_response.data | ||
| elif isinstance(rest_response.data, bytes): | ||
| try: | ||
| decoded = rest_response.data.decode("utf-8") | ||
| try: | ||
| response_body = json.loads(decoded) | ||
| except (json.JSONDecodeError, ValueError): | ||
| response_body = decoded | ||
| except UnicodeDecodeError: | ||
| response_body = rest_response.data | ||
| else: | ||
| response_body = rest_response.data | ||
|
|
||
| return RawResponse( | ||
| status=rest_response.status, | ||
| headers=dict(rest_response.getheaders()), | ||
| body=response_body, | ||
| ) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.