|
1 | 1 | import asyncio |
2 | 2 | import uuid |
| 3 | +import urllib.parse |
| 4 | +from typing import Any |
| 5 | +import json |
3 | 6 |
|
4 | 7 | from openfga_sdk.api.open_fga_api import OpenFgaApi |
5 | 8 | from openfga_sdk.api_client import ApiClient |
|
33 | 36 | construct_write_single_response, |
34 | 37 | ) |
35 | 38 | from openfga_sdk.client.models.write_transaction_opts import WriteTransactionOpts |
| 39 | +from openfga_sdk.client.models.raw_response import RawResponse |
36 | 40 | from openfga_sdk.constants import ( |
37 | 41 | CLIENT_BULK_REQUEST_ID_HEADER, |
38 | 42 | CLIENT_MAX_BATCH_SIZE, |
39 | 43 | CLIENT_MAX_METHOD_PARALLEL_REQUESTS, |
40 | 44 | CLIENT_METHOD_HEADER, |
41 | 45 | ) |
42 | 46 | from openfga_sdk.exceptions import ( |
| 47 | + ApiException, |
43 | 48 | AuthenticationError, |
44 | 49 | FgaValidationException, |
45 | 50 | UnauthorizedException, |
|
68 | 73 | ) |
69 | 74 | from openfga_sdk.models.write_request import WriteRequest |
70 | 75 | from openfga_sdk.validation import is_well_formed_ulid_string |
| 76 | +from openfga_sdk.rest import RESTResponse |
71 | 77 |
|
72 | 78 |
|
73 | 79 | def _chuck_array(array, max_size): |
@@ -1096,3 +1102,164 @@ def map_to_assertion(client_assertion: ClientAssertion): |
1096 | 1102 | authorization_model_id, api_request_body, **kwargs |
1097 | 1103 | ) |
1098 | 1104 | return api_response |
| 1105 | + |
| 1106 | + ####################### |
| 1107 | + # Raw Request |
| 1108 | + ####################### |
| 1109 | + async def raw_request( |
| 1110 | + self, |
| 1111 | + method: str, |
| 1112 | + path: str, |
| 1113 | + query_params: dict[str, str | int | list[str | int]] | None = None, |
| 1114 | + path_params: dict[str, str] | None = None, |
| 1115 | + headers: dict[str, str] | None = None, |
| 1116 | + body: dict[str, Any] | list[Any] | str | bytes | None = None, |
| 1117 | + operation_name: str | None = None, |
| 1118 | + options: dict[str, int | str | dict[str, int | str]] | None = None, |
| 1119 | + ) -> RawResponse: |
| 1120 | + """ |
| 1121 | + Make a raw HTTP request to any OpenFGA API endpoint. |
| 1122 | +
|
| 1123 | + :param method: HTTP method (GET, POST, PUT, DELETE, PATCH, etc.) |
| 1124 | + :param path: API endpoint path (e.g., "/stores/{store_id}/check" or "/stores") |
| 1125 | + :param query_params: Optional query parameters as a dictionary |
| 1126 | + :param path_params: Optional path parameters to replace placeholders in path |
| 1127 | + (e.g., {"store_id": "abc", "model_id": "xyz"}) |
| 1128 | + :param headers: Optional request headers (will be merged with default headers) |
| 1129 | + :param body: Optional request body (dict/list will be JSON serialized, str/bytes sent as-is) |
| 1130 | + :param operation_name: Required operation name for telemetry/logging (e.g., "Check", "Write", "CustomEndpoint") |
| 1131 | + :param options: Optional request options: |
| 1132 | + - headers: Additional headers (merged with headers parameter) |
| 1133 | + - retry_params: Override retry parameters for this request |
| 1134 | + - authorization_model_id: Not used in raw_request, but kept for consistency |
| 1135 | + :return: RawResponse object with status, headers, and body |
| 1136 | + :raises FgaValidationException: If path contains {store_id} but store_id is not configured |
| 1137 | + :raises ApiException: For HTTP errors (with SDK error handling applied) |
| 1138 | + """ |
| 1139 | + |
| 1140 | + request_headers = dict(headers) if headers else {} |
| 1141 | + if options and options.get("headers"): |
| 1142 | + request_headers.update(options["headers"]) |
| 1143 | + |
| 1144 | + if not operation_name: |
| 1145 | + raise FgaValidationException("operation_name is required for raw_request") |
| 1146 | + |
| 1147 | + resource_path = path |
| 1148 | + path_params_dict = dict(path_params) if path_params else {} |
| 1149 | + |
| 1150 | + if "{store_id}" in resource_path and "store_id" not in path_params_dict: |
| 1151 | + store_id = self.get_store_id() |
| 1152 | + if store_id is None or store_id == "": |
| 1153 | + raise FgaValidationException( |
| 1154 | + "Path contains {store_id} but store_id is not configured. " |
| 1155 | + "Set store_id in ClientConfiguration, use set_store_id(), or provide it in path_params." |
| 1156 | + ) |
| 1157 | + path_params_dict["store_id"] = store_id |
| 1158 | + |
| 1159 | + for param_name, param_value in path_params_dict.items(): |
| 1160 | + placeholder = f"{{{param_name}}}" |
| 1161 | + if placeholder in resource_path: |
| 1162 | + encoded_value = urllib.parse.quote(str(param_value), safe="") |
| 1163 | + resource_path = resource_path.replace(placeholder, encoded_value) |
| 1164 | + if "{" in resource_path or "}" in resource_path: |
| 1165 | + raise FgaValidationException( |
| 1166 | + f"Not all path parameters were provided for path: {path}" |
| 1167 | + ) |
| 1168 | + |
| 1169 | + query_params_list = [] |
| 1170 | + if query_params: |
| 1171 | + for key, value in query_params.items(): |
| 1172 | + if value is None: |
| 1173 | + continue |
| 1174 | + if isinstance(value, list): |
| 1175 | + for item in value: |
| 1176 | + if item is not None: |
| 1177 | + query_params_list.append((key, str(item))) |
| 1178 | + continue |
| 1179 | + query_params_list.append((key, str(value))) |
| 1180 | + |
| 1181 | + body_params = None |
| 1182 | + if body is not None: |
| 1183 | + if isinstance(body, (dict, list)): |
| 1184 | + body_params = json.dumps(body) |
| 1185 | + elif isinstance(body, str): |
| 1186 | + body_params = body |
| 1187 | + elif isinstance(body, bytes): |
| 1188 | + body_params = body |
| 1189 | + else: |
| 1190 | + body_params = json.dumps(body) |
| 1191 | + |
| 1192 | + retry_params = None |
| 1193 | + if options and options.get("retry_params"): |
| 1194 | + retry_params = options["retry_params"] |
| 1195 | + |
| 1196 | + if "Content-Type" not in request_headers: |
| 1197 | + request_headers["Content-Type"] = "application/json" |
| 1198 | + if "Accept" not in request_headers: |
| 1199 | + request_headers["Accept"] = "application/json" |
| 1200 | + |
| 1201 | + auth_headers = dict(request_headers) if request_headers else {} |
| 1202 | + await self._api_client.update_params_for_auth( |
| 1203 | + auth_headers, |
| 1204 | + query_params_list, |
| 1205 | + auth_settings=[], |
| 1206 | + oauth2_client=self._api._oauth2_client, |
| 1207 | + ) |
| 1208 | + |
| 1209 | + telemetry_attributes = None |
| 1210 | + if operation_name: |
| 1211 | + from openfga_sdk.telemetry.attributes import TelemetryAttribute, TelemetryAttributes |
| 1212 | + telemetry_attributes = { |
| 1213 | + TelemetryAttributes.fga_client_request_method: operation_name.lower(), |
| 1214 | + } |
| 1215 | + if self.get_store_id(): |
| 1216 | + telemetry_attributes[TelemetryAttributes.fga_client_request_store_id] = self.get_store_id() |
| 1217 | + |
| 1218 | + try: |
| 1219 | + _, http_status, http_headers = await self._api_client.call_api( |
| 1220 | + resource_path=resource_path, |
| 1221 | + method=method.upper(), |
| 1222 | + query_params=query_params_list if query_params_list else None, |
| 1223 | + header_params=auth_headers if auth_headers else None, |
| 1224 | + body=body_params, |
| 1225 | + response_types_map={}, |
| 1226 | + auth_settings=[], |
| 1227 | + _return_http_data_only=False, |
| 1228 | + _preload_content=True, |
| 1229 | + _retry_params=retry_params, |
| 1230 | + _oauth2_client=self._api._oauth2_client, |
| 1231 | + _telemetry_attributes=telemetry_attributes, |
| 1232 | + ) |
| 1233 | + except ApiException as e: |
| 1234 | + raise |
| 1235 | + rest_response: RESTResponse | None = getattr( |
| 1236 | + self._api_client, "last_response", None |
| 1237 | + ) |
| 1238 | + |
| 1239 | + if rest_response is None: |
| 1240 | + raise RuntimeError("Failed to get response from API client") |
| 1241 | + |
| 1242 | + response_body: bytes | str | dict[str, Any] | None = None |
| 1243 | + if rest_response.data is not None: |
| 1244 | + if isinstance(rest_response.data, str): |
| 1245 | + try: |
| 1246 | + response_body = json.loads(rest_response.data) |
| 1247 | + except (json.JSONDecodeError, ValueError): |
| 1248 | + response_body = rest_response.data |
| 1249 | + elif isinstance(rest_response.data, bytes): |
| 1250 | + try: |
| 1251 | + decoded = rest_response.data.decode("utf-8") |
| 1252 | + try: |
| 1253 | + response_body = json.loads(decoded) |
| 1254 | + except (json.JSONDecodeError, ValueError): |
| 1255 | + response_body = decoded |
| 1256 | + except UnicodeDecodeError: |
| 1257 | + response_body = rest_response.data |
| 1258 | + else: |
| 1259 | + response_body = rest_response.data |
| 1260 | + |
| 1261 | + return RawResponse( |
| 1262 | + status=http_status, |
| 1263 | + headers=http_headers if http_headers else {}, |
| 1264 | + body=response_body, |
| 1265 | + ) |
0 commit comments