diff --git a/.openapi-generator/FILES b/.openapi-generator/FILES index 7c1f87b..eeac083 100644 --- a/.openapi-generator/FILES +++ b/.openapi-generator/FILES @@ -19,6 +19,11 @@ docs/Assertion.md docs/AssertionTupleKey.md docs/AuthErrorCode.md docs/AuthorizationModel.md +docs/BatchCheckItem.md +docs/BatchCheckRequest.md +docs/BatchCheckResponse.md +docs/BatchCheckSingleResult.md +docs/CheckError.md docs/CheckRequest.md docs/CheckRequestTupleKey.md docs/CheckResponse.md @@ -119,8 +124,12 @@ openfga_sdk/client/client.py openfga_sdk/client/configuration.py openfga_sdk/client/models/__init__.py openfga_sdk/client/models/assertion.py +openfga_sdk/client/models/batch_check_item.py +openfga_sdk/client/models/batch_check_request.py openfga_sdk/client/models/batch_check_response.py +openfga_sdk/client/models/batch_check_single_response.py openfga_sdk/client/models/check_request.py +openfga_sdk/client/models/client_batch_check_response.py openfga_sdk/client/models/expand_request.py openfga_sdk/client/models/list_objects_request.py openfga_sdk/client/models/list_relations_request.py @@ -142,6 +151,11 @@ openfga_sdk/models/assertion.py openfga_sdk/models/assertion_tuple_key.py openfga_sdk/models/auth_error_code.py openfga_sdk/models/authorization_model.py +openfga_sdk/models/batch_check_item.py +openfga_sdk/models/batch_check_request.py +openfga_sdk/models/batch_check_response.py +openfga_sdk/models/batch_check_single_result.py +openfga_sdk/models/check_error.py openfga_sdk/models/check_request.py openfga_sdk/models/check_request_tuple_key.py openfga_sdk/models/check_response.py diff --git a/CHANGELOG.md b/CHANGELOG.md index a298846..a7d60ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,9 +3,17 @@ ## [Unreleased](https://github.com/openfga/python-sdk/compare/v0.8.1...HEAD) - feat: remove client-side validation - thanks @GMorris-professional (#155) -- feat: add support for `start_time` parameter in `ReadChanges` endpoint (#156) +- feat: add support for `start_time` parameter in `ReadChanges` endpoint (#156) - Note, this feature requires v1.8.0 of OpenFGA or newer +- feat!: add support for `BatchCheck` API (#154) - Note, this feature requires v1.8.2 of OpenFGA or newer - fix: change default max retry limit to 3 from 15 - thanks @ovindu-a (#155) +BREAKING CHANGE: + +Usage of the existing batch_check should now use client_batch_check instead, additionally the existing +BatchCheckResponse has been renamed to ClientBatchCheckClientResponse. + +Please see (#154)(https://github.com/openfga/python-sdk/pull/154) for more details on this change. + ## v0.8.1 ### [0.8.1](https://github.com/openfga/python-sdk/compare/v0.8.0...v0.8.1) (2024-11-26) diff --git a/README.md b/README.md index 60185d8..9568404 100644 --- a/README.md +++ b/README.md @@ -724,9 +724,11 @@ If 429s or 5xxs are encountered, the underlying check will retry up to 3 times b ```python # from openfga_sdk import OpenFgaClient -# from openfga_sdk.client import ClientCheckRequest -# from openfga_sdk.client.models import ClientTuple - +# from openfga_sdk.client.models import ( +# ClientTuple, +# ClientBatchCheckItem, +# ClientBatchCheckRequest, +# ) # Initialize the fga_client # fga_client = OpenFgaClient(configuration) @@ -734,7 +736,7 @@ options = { # You can rely on the model id set in the configuration or override it for this specific request "authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1" } -body = [ClientCheckRequest( +checks = [ClientBatchCheckItem( user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", relation="viewer", object="document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", @@ -748,7 +750,7 @@ body = [ClientCheckRequest( context=dict( ViewCount=100 ) -), ClientCheckRequest( +), ClientBatchCheckItem( user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", relation="admin", object="document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", @@ -759,20 +761,21 @@ body = [ClientCheckRequest( object="document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", ), ] -), ClientCheckRequest( +), ClientBatchCheckItem( user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", relation="creator", object="document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", -), ClientCheckRequest( +), ClientBatchCheckItem( user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", relation="deleter", object="document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", )] -response = await fga_client.batch_check(body, options) -# response.responses = [{ +response = await fga_client.batch_check(ClientBatchCheckRequest(checks=checks), options) +# response.result = [{ # allowed: false, -# request: { +# correlation_id: "de3630c2-f9be-4ee5-9441-cb1fbd82ce75", +# tuple: { # user: "user:81684243-9356-4421-8fbf-a4f8d36aa31b", # relation: "viewer", # object: "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", @@ -787,7 +790,8 @@ response = await fga_client.batch_check(body, options) # } # }, { # allowed: false, -# request: { +# correlation_id: "6d7c7129-9607-480e-bfd0-17c16e46b9ec", +# tuple: { # user: "user:81684243-9356-4421-8fbf-a4f8d36aa31b", # relation: "admin", # object: "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", @@ -799,14 +803,19 @@ response = await fga_client.batch_check(body, options) # } # }, { # allowed: false, -# request: { +# correlation_id: "210899b9-6bc3-4491-bdd1-d3d79780aa31", +# tuple: { # user: "user:81684243-9356-4421-8fbf-a4f8d36aa31b", # relation: "creator", # object: "document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", # }, -# error: +# error: { +# input_error: "validation_error", +# message: "relation 'document#creator' not found" +# } # }, { # allowed: true, +# correlation_id: "55cc1946-9fc3-4710-bd40-8fe2687ed8da", # request: { # user: "user:81684243-9356-4421-8fbf-a4f8d36aa31b", # relation: "deleter", @@ -1043,6 +1052,7 @@ async def main(): Class | Method | HTTP request | Description ------------ | ------------- | ------------- | ------------- +*OpenFgaApi* | [**batch_check**](https://github.com/openfga/python-sdk/blob/main/docs/OpenFgaApi.md#batch_check) | **POST** /stores/{store_id}/batch-check | Send a list of `check` operations in a single request *OpenFgaApi* | [**check**](https://github.com/openfga/python-sdk/blob/main/docs/OpenFgaApi.md#check) | **POST** /stores/{store_id}/check | Check whether a user is authorized to access an object *OpenFgaApi* | [**create_store**](https://github.com/openfga/python-sdk/blob/main/docs/OpenFgaApi.md#create_store) | **POST** /stores | Create a store *OpenFgaApi* | [**delete_store**](https://github.com/openfga/python-sdk/blob/main/docs/OpenFgaApi.md#delete_store) | **DELETE** /stores/{store_id} | Delete a store @@ -1072,6 +1082,11 @@ Class | Method | HTTP request | Description - [AssertionTupleKey](https://github.com/openfga/python-sdk/blob/main/docs/AssertionTupleKey.md) - [AuthErrorCode](https://github.com/openfga/python-sdk/blob/main/docs/AuthErrorCode.md) - [AuthorizationModel](https://github.com/openfga/python-sdk/blob/main/docs/AuthorizationModel.md) + - [BatchCheckItem](https://github.com/openfga/python-sdk/blob/main/docs/BatchCheckItem.md) + - [BatchCheckRequest](https://github.com/openfga/python-sdk/blob/main/docs/BatchCheckRequest.md) + - [BatchCheckResponse](https://github.com/openfga/python-sdk/blob/main/docs/BatchCheckResponse.md) + - [BatchCheckSingleResult](https://github.com/openfga/python-sdk/blob/main/docs/BatchCheckSingleResult.md) + - [CheckError](https://github.com/openfga/python-sdk/blob/main/docs/CheckError.md) - [CheckRequest](https://github.com/openfga/python-sdk/blob/main/docs/CheckRequest.md) - [CheckRequestTupleKey](https://github.com/openfga/python-sdk/blob/main/docs/CheckRequestTupleKey.md) - [CheckResponse](https://github.com/openfga/python-sdk/blob/main/docs/CheckResponse.md) diff --git a/docs/BatchCheckItem.md b/docs/BatchCheckItem.md new file mode 100644 index 0000000..7d374cd --- /dev/null +++ b/docs/BatchCheckItem.md @@ -0,0 +1,14 @@ +# BatchCheckItem + + +## Properties +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**tuple_key** | [**CheckRequestTupleKey**](CheckRequestTupleKey.md) | | +**contextual_tuples** | [**ContextualTupleKeys**](ContextualTupleKeys.md) | | [optional] +**context** | **object** | | [optional] +**correlation_id** | **str** | correlation_id must be a string containing only letters, numbers, or hyphens, with length ≤ 36 characters. | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/docs/BatchCheckRequest.md b/docs/BatchCheckRequest.md new file mode 100644 index 0000000..7f8c119 --- /dev/null +++ b/docs/BatchCheckRequest.md @@ -0,0 +1,13 @@ +# BatchCheckRequest + + +## Properties +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**checks** | [**list[BatchCheckItem]**](BatchCheckItem.md) | | +**authorization_model_id** | **str** | | [optional] +**consistency** | [**ConsistencyPreference**](ConsistencyPreference.md) | | [optional] + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/docs/BatchCheckResponse.md b/docs/BatchCheckResponse.md new file mode 100644 index 0000000..0a7c3d6 --- /dev/null +++ b/docs/BatchCheckResponse.md @@ -0,0 +1,11 @@ +# BatchCheckResponse + + +## Properties +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**result** | [**dict[str, BatchCheckSingleResult]**](BatchCheckSingleResult.md) | map keys are the correlation_id values from the BatchCheckItems in the request | [optional] + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/docs/BatchCheckSingleResult.md b/docs/BatchCheckSingleResult.md new file mode 100644 index 0000000..5a0c40e --- /dev/null +++ b/docs/BatchCheckSingleResult.md @@ -0,0 +1,12 @@ +# BatchCheckSingleResult + + +## Properties +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**allowed** | **bool** | | [optional] +**error** | [**CheckError**](CheckError.md) | | [optional] + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/docs/CheckError.md b/docs/CheckError.md new file mode 100644 index 0000000..cb43a01 --- /dev/null +++ b/docs/CheckError.md @@ -0,0 +1,13 @@ +# CheckError + + +## Properties +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**input_error** | [**ErrorCode**](ErrorCode.md) | | [optional] +**internal_error** | [**InternalErrorCode**](InternalErrorCode.md) | | [optional] +**message** | **str** | | [optional] + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/docs/OpenFgaApi.md b/docs/OpenFgaApi.md index 0c542fd..ce8e50e 100644 --- a/docs/OpenFgaApi.md +++ b/docs/OpenFgaApi.md @@ -4,6 +4,7 @@ All URIs are relative to *api.fga.example* Method | HTTP request | Description ------------- | ------------- | ------------- +[**batch_check**](OpenFgaApi.md#batch_check) | **POST** /stores/{store_id}/batch-check | Send a list of `check` operations in a single request [**check**](OpenFgaApi.md#check) | **POST** /stores/{store_id}/check | Check whether a user is authorized to access an object [**create_store**](OpenFgaApi.md#create_store) | **POST** /stores | Create a store [**delete_store**](OpenFgaApi.md#delete_store) | **DELETE** /stores/{store_id} | Delete a store @@ -22,6 +23,90 @@ Method | HTTP request | Description [**write_authorization_model**](OpenFgaApi.md#write_authorization_model) | **POST** /stores/{store_id}/authorization-models | Create a new authorization model +# **batch_check** +> BatchCheckResponse batch_check(body) + +Send a list of `check` operations in a single request + +The `BatchCheck` API functions nearly identically to `Check`, but instead of checking a single user-object relationship BatchCheck accepts a list of relationships to check and returns a map containing `BatchCheckItem` response for each check it received. An associated `correlation_id` is required for each check in the batch. This ID is used to correlate a check to the appropriate response. It is a string consisting of only alphanumeric characters or hyphens with a maximum length of 36 characters. This `correlation_id` is used to map the result of each check to the item which was checked, so it must be unique for each item in the batch. We recommend using a UUID or ULID as the `correlation_id`, but you can use whatever unique identifier you need as long as it matches this regex pattern: `^[\\w\\d-]{1,36}$` For more details on how `Check` functions, see the docs for `/check`. ### Examples #### A BatchCheckRequest ```json { \"checks\": [ { \"tuple_key\": { \"object\": \"document:2021-budget\" \"relation\": \"reader\", \"user\": \"user:anne\", }, \"contextual_tuples\": {...} \"context\": {} \"correlation_id\": \"01JA8PM3QM7VBPGB8KMPK8SBD5\" }, { \"tuple_key\": { \"object\": \"document:2021-budget\" \"relation\": \"reader\", \"user\": \"user:bob\", }, \"contextual_tuples\": {...} \"context\": {} \"correlation_id\": \"01JA8PMM6A90NV5ET0F28CYSZQ\" } ] } ``` Below is a possible response to the above request. Note that the result map's keys are the `correlation_id` values from the checked items in the request: ```json { \"result\": { \"01JA8PMM6A90NV5ET0F28CYSZQ\": { \"allowed\": false, \"error\": {\"message\": \"\"} }, \"01JA8PM3QM7VBPGB8KMPK8SBD5\": { \"allowed\": true, \"error\": {\"message\": \"\"} } } ``` + +### Example + +```python +import time +import openfga_sdk +from openfga_sdk.rest import ApiException +from pprint import pprint +# To configure the configuration +# host is mandatory +# api_scheme is optional and default to https +# store_id is mandatory +# See configuration.py for a list of all supported configuration parameters. +configuration = openfga_sdk.Configuration( + scheme = "https", + api_host = "api.fga.example", + store_id = 'YOUR_STORE_ID', +) + + +# When authenticating via the API TOKEN method +credentials = Credentials(method='api_token', configuration=CredentialConfiguration(api_token='TOKEN1')) +configuration = openfga_sdk.Configuration( + scheme = "https", + api_host = "api.fga.example", + store_id = 'YOUR_STORE_ID', + credentials = credentials +) + +# Enter a context with an instance of the API client +async with openfga_sdk.ApiClient(configuration) as api_client: + # Create an instance of the API class + api_instance = openfga_sdk.OpenFgaApi(api_client) + body = openfga_sdk.BatchCheckRequest() # BatchCheckRequest | + + try: + # Send a list of `check` operations in a single request + api_response = await api_instance.api_instance.batch_check(body) + pprint(api_response) + except ApiException as e: + print("Exception when calling OpenFgaApi->batch_check: %s\n" % e) + await api_client.close() +``` + + +### Parameters + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **body** | [**BatchCheckRequest**](BatchCheckRequest.md)| | + +### Return type + +[**BatchCheckResponse**](BatchCheckResponse.md) + +### Authorization + +No authorization required + +### HTTP request headers + + - **Content-Type**: application/json + - **Accept**: application/json + +### HTTP response details +| Status code | Description | Response headers | +|-------------|-------------|------------------| +**200** | A successful response. | - | +**400** | Request failed due to invalid input. | - | +**401** | Not authenticated. | - | +**403** | Forbidden. | - | +**404** | Request failed due to incorrect path. | - | +**409** | Request was aborted due a transaction conflict. | - | +**422** | Request timed out due to excessive request throttling. | - | +**500** | Request failed due to internal server error. | - | + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + # **check** > CheckResponse check(body) diff --git a/docs/opentelemetry.md b/docs/opentelemetry.md index 8a75043..ce4ca6b 100644 --- a/docs/opentelemetry.md +++ b/docs/opentelemetry.md @@ -30,23 +30,24 @@ If you configure the OpenTelemetry SDK, these metrics will be exported and sent ### Supported Attributes -| Attribute Name | Type | Enabled by Default | Description | -| ------------------------------ | ------ | ------------------ | --------------------------------------------------------------------------------- | -| `fga-client.request.client_id` | string | Yes | Client ID associated with the request, if any | -| `fga-client.request.method` | string | Yes | FGA method/action that was performed (e.g., Check, ListObjects) in TitleCase | -| `fga-client.request.model_id` | string | Yes | Authorization model ID that was sent as part of the request, if any | -| `fga-client.request.store_id` | string | Yes | Store ID that was sent as part of the request | -| `fga-client.response.model_id` | string | Yes | Authorization model ID that the FGA server used | -| `fga-client.user` | string | No | User associated with the action of the request for check and list users | -| `http.client.request.duration` | int | No | Duration for the SDK to complete the request, in milliseconds | -| `http.host` | string | Yes | Host identifier of the origin the request was sent to | -| `http.request.method` | string | Yes | HTTP method for the request | -| `http.request.resend_count` | int | Yes | Number of retries attempted, if any | -| `http.response.status_code` | int | Yes | Status code of the response (e.g., `200` for success) | -| `http.server.request.duration` | int | No | Time taken by the FGA server to process and evaluate the request, in milliseconds | -| `url.scheme` | string | Yes | HTTP scheme of the request (`http`/`https`) | -| `url.full` | string | Yes | Full URL of the request | -| `user_agent.original` | string | Yes | User Agent used in the query | +| Attribute Name | Type | Enabled by Default | Description | +| ------------------------------------- | ------ | ------------------ | --------------------------------------------------------------------------------- | +| `fga-client.request.batch_check_size` | int | No | The total size of the `check` list in a `BatchCheck` call | +| `fga-client.request.client_id` | string | Yes | Client ID associated with the request, if any | +| `fga-client.request.method` | string | Yes | FGA method/action that was performed (e.g., Check, ListObjects) in TitleCase | +| `fga-client.request.model_id` | string | Yes | Authorization model ID that was sent as part of the request, if any | +| `fga-client.request.store_id` | string | Yes | Store ID that was sent as part of the request | +| `fga-client.response.model_id` | string | Yes | Authorization model ID that the FGA server used | +| `fga-client.user` | string | No | User associated with the action of the request for check and list users | +| `http.client.request.duration` | int | No | Duration for the SDK to complete the request, in milliseconds | +| `http.host` | string | Yes | Host identifier of the origin the request was sent to | +| `http.request.method` | string | Yes | HTTP method for the request | +| `http.request.resend_count` | int | Yes | Number of retries attempted, if any | +| `http.response.status_code` | int | Yes | Status code of the response (e.g., `200` for success) | +| `http.server.request.duration` | int | No | Time taken by the FGA server to process and evaluate the request, in milliseconds | +| `url.scheme` | string | Yes | HTTP scheme of the request (`http`/`https`) | +| `url.full` | string | Yes | Full URL of the request | +| `user_agent.original` | string | Yes | User Agent used in the query | ## Customizing Reporting diff --git a/example/example1/example1.py b/example/example1/example1.py index 22fddbf..9fc0975 100644 --- a/example/example1/example1.py +++ b/example/example1/example1.py @@ -1,5 +1,6 @@ import asyncio import os +import uuid from openfga_sdk import ( ClientConfiguration, @@ -21,6 +22,8 @@ ) from openfga_sdk.client.models import ( ClientAssertion, + ClientBatchCheckItem, + ClientBatchCheckRequest, ClientCheckRequest, ClientListObjectsRequest, ClientListRelationsRequest, @@ -268,6 +271,36 @@ async def main(): ) print(f"Allowed: {response.allowed}") + # Performing a BatchCheck + print("Checking for access via BatchCheck") + + anne_cor_id = str(uuid.uuid4()) + response = await fga_client.batch_check( + ClientBatchCheckRequest( + checks=[ + ClientBatchCheckItem( + user="user:anne", + relation="viewer", + object="document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", + context=dict(ViewCount=100), + correlation_id=anne_cor_id, # correlation_id is an optional parameter, the SDK will insert a value if not provided. + ), + ClientBatchCheckItem( + user="user:bob", + relation="viewer", + object="document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a", + context=dict(ViewCount=100), + ), + ] + ) + ) + + for result in response.result: + if result.correlation_id == anne_cor_id: + print(f"Anne allowed: {result.allowed}") + else: + print(f"{result.request.user} allowed: {result.allowed}") + # List objects with context print("Listing objects for access with context") diff --git a/openfga_sdk/__init__.py b/openfga_sdk/__init__.py index 65852e4..cc11d04 100644 --- a/openfga_sdk/__init__.py +++ b/openfga_sdk/__init__.py @@ -31,6 +31,11 @@ from openfga_sdk.models.assertion_tuple_key import AssertionTupleKey from openfga_sdk.models.auth_error_code import AuthErrorCode from openfga_sdk.models.authorization_model import AuthorizationModel +from openfga_sdk.models.batch_check_item import BatchCheckItem +from openfga_sdk.models.batch_check_request import BatchCheckRequest +from openfga_sdk.models.batch_check_response import BatchCheckResponse +from openfga_sdk.models.batch_check_single_result import BatchCheckSingleResult +from openfga_sdk.models.check_error import CheckError from openfga_sdk.models.check_request import CheckRequest from openfga_sdk.models.check_request_tuple_key import CheckRequestTupleKey from openfga_sdk.models.check_response import CheckResponse diff --git a/openfga_sdk/api/open_fga_api.py b/openfga_sdk/api/open_fga_api.py index 6c88b91..ccb7f86 100644 --- a/openfga_sdk/api/open_fga_api.py +++ b/openfga_sdk/api/open_fga_api.py @@ -48,6 +48,186 @@ async def __aexit__(self, exc_type, exc_value, traceback): async def close(self): await self.api_client.close() + async def batch_check(self, body, **kwargs): + """Send a list of `check` operations in a single request + + The `BatchCheck` API functions nearly identically to `Check`, but instead of checking a single user-object relationship BatchCheck accepts a list of relationships to check and returns a map containing `BatchCheckItem` response for each check it received. An associated `correlation_id` is required for each check in the batch. This ID is used to correlate a check to the appropriate response. It is a string consisting of only alphanumeric characters or hyphens with a maximum length of 36 characters. This `correlation_id` is used to map the result of each check to the item which was checked, so it must be unique for each item in the batch. We recommend using a UUID or ULID as the `correlation_id`, but you can use whatever unique identifier you need as long as it matches this regex pattern: `^[\\w\\d-]{1,36}$` For more details on how `Check` functions, see the docs for `/check`. ### Examples #### A BatchCheckRequest ```json { \"checks\": [ { \"tuple_key\": { \"object\": \"document:2021-budget\" \"relation\": \"reader\", \"user\": \"user:anne\", }, \"contextual_tuples\": {...} \"context\": {} \"correlation_id\": \"01JA8PM3QM7VBPGB8KMPK8SBD5\" }, { \"tuple_key\": { \"object\": \"document:2021-budget\" \"relation\": \"reader\", \"user\": \"user:bob\", }, \"contextual_tuples\": {...} \"context\": {} \"correlation_id\": \"01JA8PMM6A90NV5ET0F28CYSZQ\" } ] } ``` Below is a possible response to the above request. Note that the result map's keys are the `correlation_id` values from the checked items in the request: ```json { \"result\": { \"01JA8PMM6A90NV5ET0F28CYSZQ\": { \"allowed\": false, \"error\": {\"message\": \"\"} }, \"01JA8PM3QM7VBPGB8KMPK8SBD5\": { \"allowed\": true, \"error\": {\"message\": \"\"} } } ``` + + >>> thread = await api.batch_check(body) + + :param body: (required) + :type body: BatchCheckRequest + :param async_req: Whether to execute the request asynchronously. + :type async_req: bool, optional + :param _preload_content: if False, the urllib3.HTTPResponse object will + be returned without reading/decoding response + data. Default is True. + :type _preload_content: bool, optional + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :return: Returns the result object. + If the method is called asynchronously, + returns the request thread. + :rtype: BatchCheckResponse + """ + kwargs["_return_http_data_only"] = True + return await self.batch_check_with_http_info(body, **kwargs) + + async def batch_check_with_http_info(self, body, **kwargs): + """Send a list of `check` operations in a single request + + The `BatchCheck` API functions nearly identically to `Check`, but instead of checking a single user-object relationship BatchCheck accepts a list of relationships to check and returns a map containing `BatchCheckItem` response for each check it received. An associated `correlation_id` is required for each check in the batch. This ID is used to correlate a check to the appropriate response. It is a string consisting of only alphanumeric characters or hyphens with a maximum length of 36 characters. This `correlation_id` is used to map the result of each check to the item which was checked, so it must be unique for each item in the batch. We recommend using a UUID or ULID as the `correlation_id`, but you can use whatever unique identifier you need as long as it matches this regex pattern: `^[\\w\\d-]{1,36}$` For more details on how `Check` functions, see the docs for `/check`. ### Examples #### A BatchCheckRequest ```json { \"checks\": [ { \"tuple_key\": { \"object\": \"document:2021-budget\" \"relation\": \"reader\", \"user\": \"user:anne\", }, \"contextual_tuples\": {...} \"context\": {} \"correlation_id\": \"01JA8PM3QM7VBPGB8KMPK8SBD5\" }, { \"tuple_key\": { \"object\": \"document:2021-budget\" \"relation\": \"reader\", \"user\": \"user:bob\", }, \"contextual_tuples\": {...} \"context\": {} \"correlation_id\": \"01JA8PMM6A90NV5ET0F28CYSZQ\" } ] } ``` Below is a possible response to the above request. Note that the result map's keys are the `correlation_id` values from the checked items in the request: ```json { \"result\": { \"01JA8PMM6A90NV5ET0F28CYSZQ\": { \"allowed\": false, \"error\": {\"message\": \"\"} }, \"01JA8PM3QM7VBPGB8KMPK8SBD5\": { \"allowed\": true, \"error\": {\"message\": \"\"} } } ``` + + >>> thread = api.batch_check_with_http_info(body) + + :param body: (required) + :type body: BatchCheckRequest + :param async_req: Whether to execute the request asynchronously. + :type async_req: bool, optional + :param _return_http_data_only: response data without head status code + and headers + :type _return_http_data_only: bool, optional + :param _preload_content: if False, the urllib3.HTTPResponse object will + be returned without reading/decoding response + data. Default is True. + :type _preload_content: bool, optional + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the authentication + in the spec for a single request. + :param _retry_param: if specified, override the retry parameters specified in configuration + :type _request_auth: dict, optional + :type _content_type: string, optional: force content-type for the request + :return: Returns the result object. + If the method is called asynchronously, + returns the request thread. + :rtype: tuple(BatchCheckResponse, status_code(int), headers(HTTPHeaderDict)) + """ + + local_var_params = locals() + + all_params = ["body"] + all_params.extend( + [ + "async_req", + "_return_http_data_only", + "_preload_content", + "_request_timeout", + "_request_auth", + "_content_type", + "_headers", + "_retry_params", + ] + ) + + for key, val in local_var_params["kwargs"].items(): + if key not in all_params: + raise FgaValidationException( + "Got an unexpected keyword argument '%s'" + " to method batch_check" % key + ) + local_var_params[key] = val + del local_var_params["kwargs"] + # verify the required parameter 'body' is set + if ( + self.api_client.client_side_validation + and local_var_params.get("body") is None + ): + raise ApiValueError( + "Missing the required parameter `body` when calling `batch_check`" + ) + + collection_formats = {} + + path_params = {} + + store_id = None + + if self.api_client._get_store_id() is None: + raise ApiValueError( + "Store ID expected in api_client's configuration when calling `batch_check`" + ) + store_id = self.api_client._get_store_id() + + query_params = [] + + header_params = dict(local_var_params.get("_headers", {})) + + form_params = [] + local_var_files = {} + + body_params = None + if "body" in local_var_params: + body_params = local_var_params["body"] + # HTTP header `Accept` + header_params["Accept"] = self.api_client.select_header_accept( + ["application/json"] + ) + + # HTTP header `Content-Type` + content_types_list = local_var_params.get( + "_content_type", + self.api_client.select_header_content_type( + ["application/json"], "POST", body_params + ), + ) + if content_types_list: + header_params["Content-Type"] = content_types_list + + # Authentication setting + auth_settings = [] + + response_types_map = { + 200: "BatchCheckResponse", + 400: "ValidationErrorMessageResponse", + 401: "UnauthenticatedResponse", + 403: "ForbiddenResponse", + 404: "PathUnknownErrorMessageResponse", + 409: "AbortedMessageResponse", + 422: "UnprocessableContentMessageResponse", + 500: "InternalErrorMessageResponse", + } + + telemetry_attributes: dict[TelemetryAttribute, str | int] = { + TelemetryAttributes.fga_client_request_method: "batch_check", + TelemetryAttributes.fga_client_request_store_id: self.api_client.get_store_id(), + TelemetryAttributes.fga_client_request_model_id: local_var_params.get( + "authorization_model_id", "" + ), + } + + telemetry_attributes = TelemetryAttributes.fromBody( + body=body_params, + attributes=telemetry_attributes, + ) + + return await self.api_client.call_api( + "/stores/{store_id}/batch-check".replace("{store_id}", store_id), + "POST", + path_params, + query_params, + header_params, + body=body_params, + post_params=form_params, + files=local_var_files, + response_types_map=response_types_map, + auth_settings=auth_settings, + async_req=local_var_params.get("async_req"), + _return_http_data_only=local_var_params.get("_return_http_data_only"), + _preload_content=local_var_params.get("_preload_content", True), + _request_timeout=local_var_params.get("_request_timeout"), + _retry_params=local_var_params.get("_retry_params"), + collection_formats=collection_formats, + _request_auth=local_var_params.get("_request_auth"), + _oauth2_client=self._oauth2_client, + _telemetry_attributes=telemetry_attributes, + ) + async def check(self, body, **kwargs): """Check whether a user is authorized to access an object @@ -200,6 +380,11 @@ async def check_with_http_info(self, body, **kwargs): ), } + telemetry_attributes = TelemetryAttributes.fromBody( + body=body_params, + attributes=telemetry_attributes, + ) + return await self.api_client.call_api( "/stores/{store_id}/check".replace("{store_id}", store_id), "POST", @@ -361,6 +546,11 @@ async def create_store_with_http_info(self, body, **kwargs): ), } + telemetry_attributes = TelemetryAttributes.fromBody( + body=body_params, + attributes=telemetry_attributes, + ) + return await self.api_client.call_api( "/stores", "POST", @@ -503,6 +693,11 @@ async def delete_store_with_http_info(self, **kwargs): ), } + telemetry_attributes = TelemetryAttributes.fromBody( + body=body_params, + attributes=telemetry_attributes, + ) + return await self.api_client.call_api( "/stores/{store_id}".replace("{store_id}", store_id), "DELETE", @@ -677,6 +872,11 @@ async def expand_with_http_info(self, body, **kwargs): ), } + telemetry_attributes = TelemetryAttributes.fromBody( + body=body_params, + attributes=telemetry_attributes, + ) + return await self.api_client.call_api( "/stores/{store_id}/expand".replace("{store_id}", store_id), "POST", @@ -828,6 +1028,11 @@ async def get_store_with_http_info(self, **kwargs): ), } + telemetry_attributes = TelemetryAttributes.fromBody( + body=body_params, + attributes=telemetry_attributes, + ) + return await self.api_client.call_api( "/stores/{store_id}".replace("{store_id}", store_id), "GET", @@ -1003,6 +1208,11 @@ async def list_objects_with_http_info(self, body, **kwargs): ), } + telemetry_attributes = TelemetryAttributes.fromBody( + body=body_params, + attributes=telemetry_attributes, + ) + return await self.api_client.call_api( "/stores/{store_id}/list-objects".replace("{store_id}", store_id), "POST", @@ -1162,6 +1372,11 @@ async def list_stores_with_http_info(self, **kwargs): ), } + telemetry_attributes = TelemetryAttributes.fromBody( + body=body_params, + attributes=telemetry_attributes, + ) + return await self.api_client.call_api( "/stores", "GET", @@ -1337,6 +1552,11 @@ async def list_users_with_http_info(self, body, **kwargs): ), } + telemetry_attributes = TelemetryAttributes.fromBody( + body=body_params, + attributes=telemetry_attributes, + ) + return await self.api_client.call_api( "/stores/{store_id}/list-users".replace("{store_id}", store_id), "POST", @@ -1511,6 +1731,11 @@ async def read_with_http_info(self, body, **kwargs): ), } + telemetry_attributes = TelemetryAttributes.fromBody( + body=body_params, + attributes=telemetry_attributes, + ) + return await self.api_client.call_api( "/stores/{store_id}/read".replace("{store_id}", store_id), "POST", @@ -1681,6 +1906,11 @@ async def read_assertions_with_http_info(self, authorization_model_id, **kwargs) ), } + telemetry_attributes = TelemetryAttributes.fromBody( + body=body_params, + attributes=telemetry_attributes, + ) + return await self.api_client.call_api( "/stores/{store_id}/assertions/{authorization_model_id}".replace( "{store_id}", store_id @@ -1849,6 +2079,11 @@ async def read_authorization_model_with_http_info(self, id, **kwargs): ), } + telemetry_attributes = TelemetryAttributes.fromBody( + body=body_params, + attributes=telemetry_attributes, + ) + return await self.api_client.call_api( "/stores/{store_id}/authorization-models/{id}".replace( "{store_id}", store_id @@ -2016,6 +2251,11 @@ async def read_authorization_models_with_http_info(self, **kwargs): ), } + telemetry_attributes = TelemetryAttributes.fromBody( + body=body_params, + attributes=telemetry_attributes, + ) + return await self.api_client.call_api( "/stores/{store_id}/authorization-models".replace("{store_id}", store_id), "GET", @@ -2193,6 +2433,11 @@ async def read_changes_with_http_info(self, **kwargs): ), } + telemetry_attributes = TelemetryAttributes.fromBody( + body=body_params, + attributes=telemetry_attributes, + ) + return await self.api_client.call_api( "/stores/{store_id}/changes".replace("{store_id}", store_id), "GET", @@ -2367,6 +2612,11 @@ async def write_with_http_info(self, body, **kwargs): ), } + telemetry_attributes = TelemetryAttributes.fromBody( + body=body_params, + attributes=telemetry_attributes, + ) + return await self.api_client.call_api( "/stores/{store_id}/write".replace("{store_id}", store_id), "POST", @@ -2554,6 +2804,11 @@ async def write_assertions_with_http_info( ), } + telemetry_attributes = TelemetryAttributes.fromBody( + body=body_params, + attributes=telemetry_attributes, + ) + return await self.api_client.call_api( "/stores/{store_id}/assertions/{authorization_model_id}".replace( "{store_id}", store_id @@ -2731,6 +2986,11 @@ async def write_authorization_model_with_http_info(self, body, **kwargs): ), } + telemetry_attributes = TelemetryAttributes.fromBody( + body=body_params, + attributes=telemetry_attributes, + ) + return await self.api_client.call_api( "/stores/{store_id}/authorization-models".replace("{store_id}", store_id), "POST", diff --git a/openfga_sdk/client/client.py b/openfga_sdk/client/client.py index 974e511..bb795cd 100644 --- a/openfga_sdk/client/client.py +++ b/openfga_sdk/client/client.py @@ -17,11 +17,24 @@ from openfga_sdk.api_client import ApiClient from openfga_sdk.client.configuration import ClientConfiguration from openfga_sdk.client.models.assertion import ClientAssertion -from openfga_sdk.client.models.batch_check_response import BatchCheckResponse +from openfga_sdk.client.models.batch_check_item import ( + ClientBatchCheckItem, + construct_batch_item, +) +from openfga_sdk.client.models.batch_check_request import ClientBatchCheckRequest +from openfga_sdk.client.models.batch_check_response import ( + ClientBatchCheckResponse, +) +from openfga_sdk.client.models.batch_check_single_response import ( + ClientBatchCheckSingleResponse, +) from openfga_sdk.client.models.check_request import ( ClientCheckRequest, construct_check_request, ) +from openfga_sdk.client.models.client_batch_check_response import ( + ClientBatchCheckClientResponse, +) from openfga_sdk.client.models.expand_request import ClientExpandRequest from openfga_sdk.client.models.list_objects_request import ClientListObjectsRequest from openfga_sdk.client.models.list_relations_request import ClientListRelationsRequest @@ -40,6 +53,7 @@ UnauthorizedException, ) from openfga_sdk.models.assertion import Assertion +from openfga_sdk.models.batch_check_request import BatchCheckRequest from openfga_sdk.models.check_request import CheckRequest from openfga_sdk.models.contextual_tuple_keys import ContextualTupleKeys from openfga_sdk.models.create_store_request import CreateStoreRequest @@ -115,7 +129,7 @@ def options_to_transaction_info(options: dict[str, int | str] = None): return WriteTransactionOpts() -def _check_allowed(response: BatchCheckResponse): +def _check_allowed(response: ClientBatchCheckClientResponse): """ Helper function to return whether the response is check is allowed """ @@ -571,7 +585,7 @@ async def check(self, body: ClientCheckRequest, options: dict[str, str] = None): api_response = await self._api.check(body=req_body, **kwargs) return api_response - async def _single_batch_check( + async def _single_client_batch_check( self, body: ClientCheckRequest, semaphore: asyncio.Semaphore, @@ -585,7 +599,7 @@ async def _single_batch_check( await semaphore.acquire() try: api_response = await self.check(body, options) - return BatchCheckResponse( + return ClientBatchCheckClientResponse( allowed=api_response.allowed, request=body, response=api_response, @@ -594,13 +608,13 @@ async def _single_batch_check( except (AuthenticationError, UnauthorizedException) as err: raise err except Exception as err: - return BatchCheckResponse( + return ClientBatchCheckClientResponse( allowed=False, request=body, response=None, error=err ) finally: semaphore.release() - async def batch_check( + async def client_batch_check( self, body: list[ClientCheckRequest], options: dict[str, str | int] = None ): """ @@ -630,12 +644,126 @@ async def batch_check( sem = asyncio.Semaphore(max_parallel_requests) batch_check_coros = [ - self._single_batch_check(request, sem, options) for request in body + self._single_client_batch_check(request, sem, options) for request in body ] batch_check_response = await asyncio.gather(*batch_check_coros) return batch_check_response + async def _single_batch_check( + self, + body: BatchCheckRequest, + semaphore: asyncio.Semaphore, + options: dict[str, str] = None, + ): + """ + Run a single BatchCheck request + :param body - list[ClientCheckRequest] defining check request + :param authorization_model_id(options) - Overrides the authorization model id in the configuration + """ + await semaphore.acquire() + try: + kwargs = options_to_kwargs(options) + api_response = await self._api.batch_check(body, **kwargs) + return api_response + except Exception as err: + raise err + finally: + semaphore.release() + + async def batch_check(self, body: ClientBatchCheckRequest, options=None): + """ + Run a batchcheck request + :param body - BatchCheck request + :param authorization_model_id(options) - Overrides the authorization model id in the configuration + :param max_parallel_requests(options) - Max number of requests to issue in parallel. Defaults to 10 + :param max_batch_size(options) - Max number of checks to include in a request. Defaults to 50 + :param header(options) - Custom headers to send alongside the request + :param retryParams(options) - Override the retry parameters for this request + :param retryParams.maxRetry(options) - Override the max number of retries on each API request + :param retryParams.minWaitInMs(options) - Override the minimum wait before a retry is initiated + """ + options = set_heading_if_not_set( + options, CLIENT_BULK_REQUEST_ID_HEADER, str(uuid.uuid4()) + ) + + max_parallel_requests = 10 + if options is not None and "max_parallel_requests" in options: + if ( + isinstance(options["max_parallel_requests"], str) + and options["max_parallel_requests"].isdigit() + ): + max_parallel_requests = int(options["max_parallel_requests"]) + elif isinstance(options["max_parallel_requests"], int): + max_parallel_requests = options["max_parallel_requests"] + + max_batch_size = 50 + if options is not None and "max_batch_size" in options: + if ( + isinstance(options["max_batch_size"], str) + and options["max_batch_size"].isdigit() + ): + max_batch_size = int(options["max_batch_size"]) + elif isinstance(options["max_batch_size"], int): + max_batch_size = options["max_batch_size"] + + id_to_check: dict[str, ClientBatchCheckItem] = {} + + def track_and_transform(checks): + transformed = [] + for check in checks: + if check.correlation_id is None: + check.correlation_id = str(uuid.uuid4()) + + if check.correlation_id in id_to_check: + raise FgaValidationException( + f"Duplicate correlation_id ({check.correlation_id}) provided" + ) + + id_to_check[check.correlation_id] = check + + transformed.append(construct_batch_item(check)) + return transformed + + checks = [ + track_and_transform( + body.checks[i * max_batch_size : (i + 1) * max_batch_size] + ) + for i in range((len(body.checks) + max_batch_size - 1) // max_batch_size) + ] + + result = [] + sem = asyncio.Semaphore(max_parallel_requests) + + def map_response(id, result): + check = id_to_check[id] + return ClientBatchCheckSingleResponse( + allowed=result.allowed, + request=check, + correlation_id=id, + error=result.error, + ) + + async def coro(checks): + res = await self._single_batch_check( + BatchCheckRequest( + checks=checks, + authorization_model_id=self._get_authorization_model_id(options), + consistency=self._get_consistency(options), + ), + sem, + options, + ) + + result.extend( + [map_response(c_id, c_result) for c_id, c_result in res.result.items()] + ) + + batch_check_coros = [coro(request) for request in checks] + await asyncio.gather(*batch_check_coros) + + return ClientBatchCheckResponse(result) + async def expand(self, body: ClientExpandRequest, options: dict[str, str] = None): """ Run expand request @@ -718,7 +846,7 @@ async def list_relations( ) for i in body.relations ] - result = await self.batch_check(request_body, options) + result = await self.client_batch_check(request_body, options) # need to filter with the allowed response result_iterator = filter(_check_allowed, result) result_list = list(result_iterator) diff --git a/openfga_sdk/client/models/__init__.py b/openfga_sdk/client/models/__init__.py index 5fadde2..4ff9763 100644 --- a/openfga_sdk/client/models/__init__.py +++ b/openfga_sdk/client/models/__init__.py @@ -11,8 +11,16 @@ """ from openfga_sdk.client.models.assertion import ClientAssertion -from openfga_sdk.client.models.batch_check_response import BatchCheckResponse +from openfga_sdk.client.models.batch_check_item import ClientBatchCheckItem +from openfga_sdk.client.models.batch_check_request import ClientBatchCheckRequest +from openfga_sdk.client.models.batch_check_response import ClientBatchCheckResponse +from openfga_sdk.client.models.batch_check_single_response import ( + ClientBatchCheckSingleResponse, +) from openfga_sdk.client.models.check_request import ClientCheckRequest +from openfga_sdk.client.models.client_batch_check_response import ( + ClientBatchCheckClientResponse, +) from openfga_sdk.client.models.expand_request import ClientExpandRequest from openfga_sdk.client.models.list_objects_request import ClientListObjectsRequest from openfga_sdk.client.models.list_relations_request import ClientListRelationsRequest diff --git a/openfga_sdk/client/models/batch_check_item.py b/openfga_sdk/client/models/batch_check_item.py new file mode 100644 index 0000000..f29fe77 --- /dev/null +++ b/openfga_sdk/client/models/batch_check_item.py @@ -0,0 +1,135 @@ +""" + Python SDK for OpenFGA + + API version: 1.x + Website: https://openfga.dev + Documentation: https://openfga.dev/docs + Support: https://openfga.dev/community + License: [Apache-2.0](https://github.com/openfga/python-sdk/blob/main/LICENSE) + + NOTE: This file was auto generated by OpenAPI Generator (https://openapi-generator.tech). DO NOT EDIT. +""" + +from openfga_sdk.client.models.tuple import ClientTuple, convert_tuple_keys +from openfga_sdk.models.batch_check_item import BatchCheckItem +from openfga_sdk.models.check_request_tuple_key import CheckRequestTupleKey +from openfga_sdk.models.contextual_tuple_keys import ContextualTupleKeys + + +def construct_batch_item(check): + batch_item = BatchCheckItem( + tuple_key=CheckRequestTupleKey( + user=check.user, + relation=check.relation, + object=check.object, + ), + context=check.context, + correlation_id=check.correlation_id, + ) + + if check.contextual_tuples: + batch_item.contextual_tuples = ContextualTupleKeys( + tuple_keys=convert_tuple_keys(check.contextual_tuples) + ) + + return batch_item + + +class ClientBatchCheckItem: + def __init__( + self, + user: str, + relation: str, + object: str, + correlation_id: str = None, + contextual_tuples: list[ClientTuple] = None, + context: object = None, + ): + self._user = user + self._relation = relation + self._object = object + self._correlation_id = correlation_id + self._contextual_tuples = None + if contextual_tuples: + self._contextual_tuples = contextual_tuples + self._context = context + + @property + def user(self): + """ + Return user + """ + return self._user + + @property + def relation(self): + """ + Return relation + """ + return self._relation + + @property + def object(self): + """ + Return object + """ + return self._object + + @property + def contextual_tuples(self): + """ + Return contextual tuples + """ + return self._contextual_tuples + + @property + def context(self): + """ + Return context + """ + return self._context + + @property + def correlation_id(self): + """ """ + return self._correlation_id + + @user.setter + def user(self, value): + """ + Set user + """ + self._user = value + + @relation.setter + def relation(self, value): + """ + Set relation + """ + self._relation = value + + @object.setter + def object(self, value): + """ + Set object + """ + self._object = value + + @contextual_tuples.setter + def contextual_tuples(self, value): + """ + Set contextual tuples + """ + self._contextual_tuples = value + + @context.setter + def context(self, value): + """ + Set context + """ + self._context = value + + @correlation_id.setter + def correlation_id(self, value): + """ """ + self._correlation_id = value diff --git a/openfga_sdk/client/models/batch_check_request.py b/openfga_sdk/client/models/batch_check_request.py new file mode 100644 index 0000000..d81c437 --- /dev/null +++ b/openfga_sdk/client/models/batch_check_request.py @@ -0,0 +1,36 @@ +""" + Python SDK for OpenFGA + + API version: 1.x + Website: https://openfga.dev + Documentation: https://openfga.dev/docs + Support: https://openfga.dev/community + License: [Apache-2.0](https://github.com/openfga/python-sdk/blob/main/LICENSE) + + NOTE: This file was auto generated by OpenAPI Generator (https://openapi-generator.tech). DO NOT EDIT. +""" + +from openfga_sdk.client.models.batch_check_item import ClientBatchCheckItem + + +class ClientBatchCheckRequest: + """ + ClientBatchCheckRequest encapsulates the parameters for a BatchCheck request + """ + + def __init__(self, checks: list[ClientBatchCheckItem]): + self._checks = checks + + @property + def checks(self): + """ + Return checks + """ + return self._checks + + @checks.setter + def checks(self, checks): + """ + Set checks + """ + self._checks = checks diff --git a/openfga_sdk/client/models/batch_check_response.py b/openfga_sdk/client/models/batch_check_response.py index 3c411c2..bdc9621 100644 --- a/openfga_sdk/client/models/batch_check_response.py +++ b/openfga_sdk/client/models/batch_check_response.py @@ -10,57 +10,25 @@ NOTE: This file was auto generated by OpenAPI Generator (https://openapi-generator.tech). DO NOT EDIT. """ -from openfga_sdk.client.models.check_request import ClientCheckRequest -from openfga_sdk.models.check_response import CheckResponse +from openfga_sdk.client.models.batch_check_single_response import ( + ClientBatchCheckSingleResponse, +) -class BatchCheckResponse: - """ - BatchCheckResponse encapsulates the response for a single batch check - """ - - def __init__( - self, - allowed: bool, - request: ClientCheckRequest, - response: CheckResponse, - error: Exception = None, - ): - self._allowed = allowed - self._request = request - self._response = response - self._error = error - - @property - def allowed(self): - """ - Return whether request is allowed - """ - return self._allowed - - @property - def request(self): - """ - Return original request - """ - return self._request - - @property - def response(self): - """ - Return original request - """ - return self._response +class ClientBatchCheckResponse: + def __init__(self, result: list[ClientBatchCheckSingleResponse]): + self._result = result @property - def error(self): + def result(self): """ - Return error associated with batch request (if any) + Return result """ - return self._error + return self._result - def __str__(self): + @result.setter + def result(self, result): """ - Return the class string + Set result """ - return f"allowed {self._allowed} request {self._request} error {self._error}" + self._result = result diff --git a/openfga_sdk/client/models/batch_check_single_response.py b/openfga_sdk/client/models/batch_check_single_response.py new file mode 100644 index 0000000..69c020d --- /dev/null +++ b/openfga_sdk/client/models/batch_check_single_response.py @@ -0,0 +1,87 @@ +""" + Python SDK for OpenFGA + + API version: 1.x + Website: https://openfga.dev + Documentation: https://openfga.dev/docs + Support: https://openfga.dev/community + License: [Apache-2.0](https://github.com/openfga/python-sdk/blob/main/LICENSE) + + NOTE: This file was auto generated by OpenAPI Generator (https://openapi-generator.tech). DO NOT EDIT. +""" + +from openfga_sdk.client.models.tuple import ClientTuple +from openfga_sdk.models.check_error import CheckError + + +class ClientBatchCheckSingleResponse: + def __init__( + self, + allowed: bool, + request: ClientTuple, + correlation_id: str, + error: CheckError = None, + ): + self._allowed = allowed + self._request = request + self._correlation_id = correlation_id + self._error = error + # Set "false" if there was an error and allowed isn't set + if error is not None and allowed is None: + self._allowed = False + + @property + def allowed(self): + """ + Return allowed + """ + return self._allowed + + @property + def request(self): + """ + Return request + """ + return self._request + + @property + def correlation_id(self): + """ + Return correlation_id + """ + return self._correlation_id + + @property + def error(self): + """ + Return error + """ + return self._error + + @allowed.setter + def allowed(self, allowed): + """ + Set allowed + """ + self._allowed = allowed + + @request.setter + def request(self, request): + """ + Set request + """ + self._request = request + + @correlation_id.setter + def correlation_id(self, correlation_id): + """ + Set correlation_id + """ + self._correlation_id = correlation_id + + @error.setter + def error(self, error): + """ + Set error + """ + self._error = error diff --git a/openfga_sdk/client/models/client_batch_check_response.py b/openfga_sdk/client/models/client_batch_check_response.py new file mode 100644 index 0000000..0c38dff --- /dev/null +++ b/openfga_sdk/client/models/client_batch_check_response.py @@ -0,0 +1,66 @@ +""" + Python SDK for OpenFGA + + API version: 1.x + Website: https://openfga.dev + Documentation: https://openfga.dev/docs + Support: https://openfga.dev/community + License: [Apache-2.0](https://github.com/openfga/python-sdk/blob/main/LICENSE) + + NOTE: This file was auto generated by OpenAPI Generator (https://openapi-generator.tech). DO NOT EDIT. +""" + +from openfga_sdk.client.models.check_request import ClientCheckRequest +from openfga_sdk.models.check_response import CheckResponse + + +class ClientBatchCheckClientResponse: + """ + ClientBatchCheckClientResponse encapsulates the response for a single batch check + """ + + def __init__( + self, + allowed: bool, + request: ClientCheckRequest, + response: CheckResponse, + error: Exception = None, + ): + self._allowed = allowed + self._request = request + self._response = response + self._error = error + + @property + def allowed(self): + """ + Return whether request is allowed + """ + return self._allowed + + @property + def request(self): + """ + Return original request + """ + return self._request + + @property + def response(self): + """ + Return original request + """ + return self._response + + @property + def error(self): + """ + Return error associated with batch request (if any) + """ + return self._error + + def __str__(self): + """ + Return the class string + """ + return f"allowed {self._allowed} request {self._request} error {self._error}" diff --git a/openfga_sdk/models/__init__.py b/openfga_sdk/models/__init__.py index 9c970e7..523c9b0 100644 --- a/openfga_sdk/models/__init__.py +++ b/openfga_sdk/models/__init__.py @@ -16,6 +16,11 @@ from openfga_sdk.models.assertion_tuple_key import AssertionTupleKey from openfga_sdk.models.auth_error_code import AuthErrorCode from openfga_sdk.models.authorization_model import AuthorizationModel +from openfga_sdk.models.batch_check_item import BatchCheckItem +from openfga_sdk.models.batch_check_request import BatchCheckRequest +from openfga_sdk.models.batch_check_response import BatchCheckResponse +from openfga_sdk.models.batch_check_single_result import BatchCheckSingleResult +from openfga_sdk.models.check_error import CheckError from openfga_sdk.models.check_request import CheckRequest from openfga_sdk.models.check_request_tuple_key import CheckRequestTupleKey from openfga_sdk.models.check_response import CheckResponse diff --git a/openfga_sdk/models/batch_check_item.py b/openfga_sdk/models/batch_check_item.py new file mode 100644 index 0000000..bd1179e --- /dev/null +++ b/openfga_sdk/models/batch_check_item.py @@ -0,0 +1,217 @@ +""" + Python SDK for OpenFGA + + API version: 1.x + Website: https://openfga.dev + Documentation: https://openfga.dev/docs + Support: https://openfga.dev/community + License: [Apache-2.0](https://github.com/openfga/python-sdk/blob/main/LICENSE) + + NOTE: This file was auto generated by OpenAPI Generator (https://openapi-generator.tech). DO NOT EDIT. +""" + +try: + from inspect import getfullargspec +except ImportError: + from inspect import getargspec as getfullargspec +import pprint + +from openfga_sdk.configuration import Configuration + + +class BatchCheckItem: + """NOTE: This class is auto generated by OpenAPI Generator. + Ref: https://openapi-generator.tech + + Do not edit the class manually. + """ + + """ + Attributes: + openapi_types (dict): The key is attribute name + and the value is attribute type. + attribute_map (dict): The key is attribute name + and the value is json key in definition. + """ + openapi_types = { + "tuple_key": "CheckRequestTupleKey", + "contextual_tuples": "ContextualTupleKeys", + "context": "object", + "correlation_id": "str", + } + + attribute_map = { + "tuple_key": "tuple_key", + "contextual_tuples": "contextual_tuples", + "context": "context", + "correlation_id": "correlation_id", + } + + def __init__( + self, + tuple_key=None, + contextual_tuples=None, + context=None, + correlation_id=None, + local_vars_configuration=None, + ): + """BatchCheckItem - a model defined in OpenAPI""" + if local_vars_configuration is None: + local_vars_configuration = Configuration.get_default_copy() + self.local_vars_configuration = local_vars_configuration + + self._tuple_key = None + self._contextual_tuples = None + self._context = None + self._correlation_id = None + self.discriminator = None + + self.tuple_key = tuple_key + if contextual_tuples is not None: + self.contextual_tuples = contextual_tuples + if context is not None: + self.context = context + self.correlation_id = correlation_id + + @property + def tuple_key(self): + """Gets the tuple_key of this BatchCheckItem. + + + :return: The tuple_key of this BatchCheckItem. + :rtype: CheckRequestTupleKey + """ + return self._tuple_key + + @tuple_key.setter + def tuple_key(self, tuple_key): + """Sets the tuple_key of this BatchCheckItem. + + + :param tuple_key: The tuple_key of this BatchCheckItem. + :type tuple_key: CheckRequestTupleKey + """ + if self.local_vars_configuration.client_side_validation and tuple_key is None: + raise ValueError("Invalid value for `tuple_key`, must not be `None`") + + self._tuple_key = tuple_key + + @property + def contextual_tuples(self): + """Gets the contextual_tuples of this BatchCheckItem. + + + :return: The contextual_tuples of this BatchCheckItem. + :rtype: ContextualTupleKeys + """ + return self._contextual_tuples + + @contextual_tuples.setter + def contextual_tuples(self, contextual_tuples): + """Sets the contextual_tuples of this BatchCheckItem. + + + :param contextual_tuples: The contextual_tuples of this BatchCheckItem. + :type contextual_tuples: ContextualTupleKeys + """ + + self._contextual_tuples = contextual_tuples + + @property + def context(self): + """Gets the context of this BatchCheckItem. + + + :return: The context of this BatchCheckItem. + :rtype: object + """ + return self._context + + @context.setter + def context(self, context): + """Sets the context of this BatchCheckItem. + + + :param context: The context of this BatchCheckItem. + :type context: object + """ + + self._context = context + + @property + def correlation_id(self): + """Gets the correlation_id of this BatchCheckItem. + + correlation_id must be a string containing only letters, numbers, or hyphens, with length ≤ 36 characters. + + :return: The correlation_id of this BatchCheckItem. + :rtype: str + """ + return self._correlation_id + + @correlation_id.setter + def correlation_id(self, correlation_id): + """Sets the correlation_id of this BatchCheckItem. + + correlation_id must be a string containing only letters, numbers, or hyphens, with length ≤ 36 characters. + + :param correlation_id: The correlation_id of this BatchCheckItem. + :type correlation_id: str + """ + if ( + self.local_vars_configuration.client_side_validation + and correlation_id is None + ): + raise ValueError("Invalid value for `correlation_id`, must not be `None`") + + self._correlation_id = correlation_id + + def to_dict(self, serialize=False): + """Returns the model properties as a dict""" + result = {} + + def convert(x): + if hasattr(x, "to_dict"): + args = getfullargspec(x.to_dict).args + if len(args) == 1: + return x.to_dict() + else: + return x.to_dict(serialize) + else: + return x + + for attr, _ in self.openapi_types.items(): + value = getattr(self, attr) + attr = self.attribute_map.get(attr, attr) if serialize else attr + if isinstance(value, list): + result[attr] = list(map(lambda x: convert(x), value)) + elif isinstance(value, dict): + result[attr] = dict( + map(lambda item: (item[0], convert(item[1])), value.items()) + ) + else: + result[attr] = convert(value) + + return result + + def to_str(self): + """Returns the string representation of the model""" + return pprint.pformat(self.to_dict()) + + def __repr__(self): + """For `print` and `pprint`""" + return self.to_str() + + def __eq__(self, other): + """Returns true if both objects are equal""" + if not isinstance(other, BatchCheckItem): + return False + + return self.to_dict() == other.to_dict() + + def __ne__(self, other): + """Returns true if both objects are not equal""" + if not isinstance(other, BatchCheckItem): + return True + + return self.to_dict() != other.to_dict() diff --git a/openfga_sdk/models/batch_check_request.py b/openfga_sdk/models/batch_check_request.py new file mode 100644 index 0000000..71bd65c --- /dev/null +++ b/openfga_sdk/models/batch_check_request.py @@ -0,0 +1,184 @@ +""" + Python SDK for OpenFGA + + API version: 1.x + Website: https://openfga.dev + Documentation: https://openfga.dev/docs + Support: https://openfga.dev/community + License: [Apache-2.0](https://github.com/openfga/python-sdk/blob/main/LICENSE) + + NOTE: This file was auto generated by OpenAPI Generator (https://openapi-generator.tech). DO NOT EDIT. +""" + +try: + from inspect import getfullargspec +except ImportError: + from inspect import getargspec as getfullargspec +import pprint + +from openfga_sdk.configuration import Configuration + + +class BatchCheckRequest: + """NOTE: This class is auto generated by OpenAPI Generator. + Ref: https://openapi-generator.tech + + Do not edit the class manually. + """ + + """ + Attributes: + openapi_types (dict): The key is attribute name + and the value is attribute type. + attribute_map (dict): The key is attribute name + and the value is json key in definition. + """ + openapi_types = { + "checks": "list[BatchCheckItem]", + "authorization_model_id": "str", + "consistency": "ConsistencyPreference", + } + + attribute_map = { + "checks": "checks", + "authorization_model_id": "authorization_model_id", + "consistency": "consistency", + } + + def __init__( + self, + checks=None, + authorization_model_id=None, + consistency=None, + local_vars_configuration=None, + ): + """BatchCheckRequest - a model defined in OpenAPI""" + if local_vars_configuration is None: + local_vars_configuration = Configuration.get_default_copy() + self.local_vars_configuration = local_vars_configuration + + self._checks = None + self._authorization_model_id = None + self._consistency = None + self.discriminator = None + + self.checks = checks + if authorization_model_id is not None: + self.authorization_model_id = authorization_model_id + if consistency is not None: + self.consistency = consistency + + @property + def checks(self): + """Gets the checks of this BatchCheckRequest. + + + :return: The checks of this BatchCheckRequest. + :rtype: list[BatchCheckItem] + """ + return self._checks + + @checks.setter + def checks(self, checks): + """Sets the checks of this BatchCheckRequest. + + + :param checks: The checks of this BatchCheckRequest. + :type checks: list[BatchCheckItem] + """ + if self.local_vars_configuration.client_side_validation and checks is None: + raise ValueError("Invalid value for `checks`, must not be `None`") + + self._checks = checks + + @property + def authorization_model_id(self): + """Gets the authorization_model_id of this BatchCheckRequest. + + + :return: The authorization_model_id of this BatchCheckRequest. + :rtype: str + """ + return self._authorization_model_id + + @authorization_model_id.setter + def authorization_model_id(self, authorization_model_id): + """Sets the authorization_model_id of this BatchCheckRequest. + + + :param authorization_model_id: The authorization_model_id of this BatchCheckRequest. + :type authorization_model_id: str + """ + + self._authorization_model_id = authorization_model_id + + @property + def consistency(self): + """Gets the consistency of this BatchCheckRequest. + + + :return: The consistency of this BatchCheckRequest. + :rtype: ConsistencyPreference + """ + return self._consistency + + @consistency.setter + def consistency(self, consistency): + """Sets the consistency of this BatchCheckRequest. + + + :param consistency: The consistency of this BatchCheckRequest. + :type consistency: ConsistencyPreference + """ + + self._consistency = consistency + + def to_dict(self, serialize=False): + """Returns the model properties as a dict""" + result = {} + + def convert(x): + if hasattr(x, "to_dict"): + args = getfullargspec(x.to_dict).args + if len(args) == 1: + return x.to_dict() + else: + return x.to_dict(serialize) + else: + return x + + for attr, _ in self.openapi_types.items(): + value = getattr(self, attr) + attr = self.attribute_map.get(attr, attr) if serialize else attr + if isinstance(value, list): + result[attr] = list(map(lambda x: convert(x), value)) + elif isinstance(value, dict): + result[attr] = dict( + map(lambda item: (item[0], convert(item[1])), value.items()) + ) + else: + result[attr] = convert(value) + + return result + + def to_str(self): + """Returns the string representation of the model""" + return pprint.pformat(self.to_dict()) + + def __repr__(self): + """For `print` and `pprint`""" + return self.to_str() + + def __eq__(self, other): + """Returns true if both objects are equal""" + if not isinstance(other, BatchCheckRequest): + return False + + return self.to_dict() == other.to_dict() + + def __ne__(self, other): + """Returns true if both objects are not equal""" + if not isinstance(other, BatchCheckRequest): + return True + + return self.to_dict() != other.to_dict() diff --git a/openfga_sdk/models/batch_check_response.py b/openfga_sdk/models/batch_check_response.py new file mode 100644 index 0000000..a9c9f13 --- /dev/null +++ b/openfga_sdk/models/batch_check_response.py @@ -0,0 +1,123 @@ +""" + Python SDK for OpenFGA + + API version: 1.x + Website: https://openfga.dev + Documentation: https://openfga.dev/docs + Support: https://openfga.dev/community + License: [Apache-2.0](https://github.com/openfga/python-sdk/blob/main/LICENSE) + + NOTE: This file was auto generated by OpenAPI Generator (https://openapi-generator.tech). DO NOT EDIT. +""" + +try: + from inspect import getfullargspec +except ImportError: + from inspect import getargspec as getfullargspec +import pprint + +from openfga_sdk.configuration import Configuration + + +class BatchCheckResponse: + """NOTE: This class is auto generated by OpenAPI Generator. + Ref: https://openapi-generator.tech + + Do not edit the class manually. + """ + + """ + Attributes: + openapi_types (dict): The key is attribute name + and the value is attribute type. + attribute_map (dict): The key is attribute name + and the value is json key in definition. + """ + openapi_types = {"result": "dict[str, BatchCheckSingleResult]"} + + attribute_map = {"result": "result"} + + def __init__(self, result=None, local_vars_configuration=None): + """BatchCheckResponse - a model defined in OpenAPI""" + if local_vars_configuration is None: + local_vars_configuration = Configuration.get_default_copy() + self.local_vars_configuration = local_vars_configuration + + self._result = None + self.discriminator = None + + if result is not None: + self.result = result + + @property + def result(self): + """Gets the result of this BatchCheckResponse. + + map keys are the correlation_id values from the BatchCheckItems in the request + + :return: The result of this BatchCheckResponse. + :rtype: dict[str, BatchCheckSingleResult] + """ + return self._result + + @result.setter + def result(self, result): + """Sets the result of this BatchCheckResponse. + + map keys are the correlation_id values from the BatchCheckItems in the request + + :param result: The result of this BatchCheckResponse. + :type result: dict[str, BatchCheckSingleResult] + """ + + self._result = result + + def to_dict(self, serialize=False): + """Returns the model properties as a dict""" + result = {} + + def convert(x): + if hasattr(x, "to_dict"): + args = getfullargspec(x.to_dict).args + if len(args) == 1: + return x.to_dict() + else: + return x.to_dict(serialize) + else: + return x + + for attr, _ in self.openapi_types.items(): + value = getattr(self, attr) + attr = self.attribute_map.get(attr, attr) if serialize else attr + if isinstance(value, list): + result[attr] = list(map(lambda x: convert(x), value)) + elif isinstance(value, dict): + result[attr] = dict( + map(lambda item: (item[0], convert(item[1])), value.items()) + ) + else: + result[attr] = convert(value) + + return result + + def to_str(self): + """Returns the string representation of the model""" + return pprint.pformat(self.to_dict()) + + def __repr__(self): + """For `print` and `pprint`""" + return self.to_str() + + def __eq__(self, other): + """Returns true if both objects are equal""" + if not isinstance(other, BatchCheckResponse): + return False + + return self.to_dict() == other.to_dict() + + def __ne__(self, other): + """Returns true if both objects are not equal""" + if not isinstance(other, BatchCheckResponse): + return True + + return self.to_dict() != other.to_dict() diff --git a/openfga_sdk/models/batch_check_single_result.py b/openfga_sdk/models/batch_check_single_result.py new file mode 100644 index 0000000..1378af3 --- /dev/null +++ b/openfga_sdk/models/batch_check_single_result.py @@ -0,0 +1,145 @@ +""" + Python SDK for OpenFGA + + API version: 1.x + Website: https://openfga.dev + Documentation: https://openfga.dev/docs + Support: https://openfga.dev/community + License: [Apache-2.0](https://github.com/openfga/python-sdk/blob/main/LICENSE) + + NOTE: This file was auto generated by OpenAPI Generator (https://openapi-generator.tech). DO NOT EDIT. +""" + +try: + from inspect import getfullargspec +except ImportError: + from inspect import getargspec as getfullargspec +import pprint + +from openfga_sdk.configuration import Configuration + + +class BatchCheckSingleResult: + """NOTE: This class is auto generated by OpenAPI Generator. + Ref: https://openapi-generator.tech + + Do not edit the class manually. + """ + + """ + Attributes: + openapi_types (dict): The key is attribute name + and the value is attribute type. + attribute_map (dict): The key is attribute name + and the value is json key in definition. + """ + openapi_types = {"allowed": "bool", "error": "CheckError"} + + attribute_map = {"allowed": "allowed", "error": "error"} + + def __init__(self, allowed=None, error=None, local_vars_configuration=None): + """BatchCheckSingleResult - a model defined in OpenAPI""" + if local_vars_configuration is None: + local_vars_configuration = Configuration.get_default_copy() + self.local_vars_configuration = local_vars_configuration + + self._allowed = None + self._error = None + self.discriminator = None + + if allowed is not None: + self.allowed = allowed + if error is not None: + self.error = error + + @property + def allowed(self): + """Gets the allowed of this BatchCheckSingleResult. + + + :return: The allowed of this BatchCheckSingleResult. + :rtype: bool + """ + return self._allowed + + @allowed.setter + def allowed(self, allowed): + """Sets the allowed of this BatchCheckSingleResult. + + + :param allowed: The allowed of this BatchCheckSingleResult. + :type allowed: bool + """ + + self._allowed = allowed + + @property + def error(self): + """Gets the error of this BatchCheckSingleResult. + + + :return: The error of this BatchCheckSingleResult. + :rtype: CheckError + """ + return self._error + + @error.setter + def error(self, error): + """Sets the error of this BatchCheckSingleResult. + + + :param error: The error of this BatchCheckSingleResult. + :type error: CheckError + """ + + self._error = error + + def to_dict(self, serialize=False): + """Returns the model properties as a dict""" + result = {} + + def convert(x): + if hasattr(x, "to_dict"): + args = getfullargspec(x.to_dict).args + if len(args) == 1: + return x.to_dict() + else: + return x.to_dict(serialize) + else: + return x + + for attr, _ in self.openapi_types.items(): + value = getattr(self, attr) + attr = self.attribute_map.get(attr, attr) if serialize else attr + if isinstance(value, list): + result[attr] = list(map(lambda x: convert(x), value)) + elif isinstance(value, dict): + result[attr] = dict( + map(lambda item: (item[0], convert(item[1])), value.items()) + ) + else: + result[attr] = convert(value) + + return result + + def to_str(self): + """Returns the string representation of the model""" + return pprint.pformat(self.to_dict()) + + def __repr__(self): + """For `print` and `pprint`""" + return self.to_str() + + def __eq__(self, other): + """Returns true if both objects are equal""" + if not isinstance(other, BatchCheckSingleResult): + return False + + return self.to_dict() == other.to_dict() + + def __ne__(self, other): + """Returns true if both objects are not equal""" + if not isinstance(other, BatchCheckSingleResult): + return True + + return self.to_dict() != other.to_dict() diff --git a/openfga_sdk/models/check_error.py b/openfga_sdk/models/check_error.py new file mode 100644 index 0000000..54b2187 --- /dev/null +++ b/openfga_sdk/models/check_error.py @@ -0,0 +1,183 @@ +""" + Python SDK for OpenFGA + + API version: 1.x + Website: https://openfga.dev + Documentation: https://openfga.dev/docs + Support: https://openfga.dev/community + License: [Apache-2.0](https://github.com/openfga/python-sdk/blob/main/LICENSE) + + NOTE: This file was auto generated by OpenAPI Generator (https://openapi-generator.tech). DO NOT EDIT. +""" + +try: + from inspect import getfullargspec +except ImportError: + from inspect import getargspec as getfullargspec +import pprint + +from openfga_sdk.configuration import Configuration + + +class CheckError: + """NOTE: This class is auto generated by OpenAPI Generator. + Ref: https://openapi-generator.tech + + Do not edit the class manually. + """ + + """ + Attributes: + openapi_types (dict): The key is attribute name + and the value is attribute type. + attribute_map (dict): The key is attribute name + and the value is json key in definition. + """ + openapi_types = { + "input_error": "ErrorCode", + "internal_error": "InternalErrorCode", + "message": "str", + } + + attribute_map = { + "input_error": "input_error", + "internal_error": "internal_error", + "message": "message", + } + + def __init__( + self, + input_error=None, + internal_error=None, + message=None, + local_vars_configuration=None, + ): + """CheckError - a model defined in OpenAPI""" + if local_vars_configuration is None: + local_vars_configuration = Configuration.get_default_copy() + self.local_vars_configuration = local_vars_configuration + + self._input_error = None + self._internal_error = None + self._message = None + self.discriminator = None + + if input_error is not None: + self.input_error = input_error + if internal_error is not None: + self.internal_error = internal_error + if message is not None: + self.message = message + + @property + def input_error(self): + """Gets the input_error of this CheckError. + + + :return: The input_error of this CheckError. + :rtype: ErrorCode + """ + return self._input_error + + @input_error.setter + def input_error(self, input_error): + """Sets the input_error of this CheckError. + + + :param input_error: The input_error of this CheckError. + :type input_error: ErrorCode + """ + + self._input_error = input_error + + @property + def internal_error(self): + """Gets the internal_error of this CheckError. + + + :return: The internal_error of this CheckError. + :rtype: InternalErrorCode + """ + return self._internal_error + + @internal_error.setter + def internal_error(self, internal_error): + """Sets the internal_error of this CheckError. + + + :param internal_error: The internal_error of this CheckError. + :type internal_error: InternalErrorCode + """ + + self._internal_error = internal_error + + @property + def message(self): + """Gets the message of this CheckError. + + + :return: The message of this CheckError. + :rtype: str + """ + return self._message + + @message.setter + def message(self, message): + """Sets the message of this CheckError. + + + :param message: The message of this CheckError. + :type message: str + """ + + self._message = message + + def to_dict(self, serialize=False): + """Returns the model properties as a dict""" + result = {} + + def convert(x): + if hasattr(x, "to_dict"): + args = getfullargspec(x.to_dict).args + if len(args) == 1: + return x.to_dict() + else: + return x.to_dict(serialize) + else: + return x + + for attr, _ in self.openapi_types.items(): + value = getattr(self, attr) + attr = self.attribute_map.get(attr, attr) if serialize else attr + if isinstance(value, list): + result[attr] = list(map(lambda x: convert(x), value)) + elif isinstance(value, dict): + result[attr] = dict( + map(lambda item: (item[0], convert(item[1])), value.items()) + ) + else: + result[attr] = convert(value) + + return result + + def to_str(self): + """Returns the string representation of the model""" + return pprint.pformat(self.to_dict()) + + def __repr__(self): + """For `print` and `pprint`""" + return self.to_str() + + def __eq__(self, other): + """Returns true if both objects are equal""" + if not isinstance(other, CheckError): + return False + + return self.to_dict() == other.to_dict() + + def __ne__(self, other): + """Returns true if both objects are not equal""" + if not isinstance(other, CheckError): + return True + + return self.to_dict() != other.to_dict() diff --git a/openfga_sdk/sync/client/client.py b/openfga_sdk/sync/client/client.py index ede0b8b..39022c6 100644 --- a/openfga_sdk/sync/client/client.py +++ b/openfga_sdk/sync/client/client.py @@ -15,11 +15,22 @@ from openfga_sdk.client.configuration import ClientConfiguration from openfga_sdk.client.models.assertion import ClientAssertion -from openfga_sdk.client.models.batch_check_response import BatchCheckResponse +from openfga_sdk.client.models.batch_check_item import ( + ClientBatchCheckItem, + construct_batch_item, +) +from openfga_sdk.client.models.batch_check_request import ClientBatchCheckRequest +from openfga_sdk.client.models.batch_check_response import ClientBatchCheckResponse +from openfga_sdk.client.models.batch_check_single_response import ( + ClientBatchCheckSingleResponse, +) from openfga_sdk.client.models.check_request import ( ClientCheckRequest, construct_check_request, ) +from openfga_sdk.client.models.client_batch_check_response import ( + ClientBatchCheckClientResponse, +) from openfga_sdk.client.models.expand_request import ClientExpandRequest from openfga_sdk.client.models.list_objects_request import ClientListObjectsRequest from openfga_sdk.client.models.list_relations_request import ClientListRelationsRequest @@ -38,6 +49,7 @@ UnauthorizedException, ) from openfga_sdk.models.assertion import Assertion +from openfga_sdk.models.batch_check_request import BatchCheckRequest from openfga_sdk.models.check_request import CheckRequest from openfga_sdk.models.contextual_tuple_keys import ContextualTupleKeys from openfga_sdk.models.create_store_request import CreateStoreRequest @@ -115,7 +127,7 @@ def options_to_transaction_info(options: dict[str, int | str] = None): return WriteTransactionOpts() -def _check_allowed(response: BatchCheckResponse): +def _check_allowed(response: ClientBatchCheckClientResponse): """ Helper function to return whether the response is check is allowed """ @@ -564,7 +576,7 @@ def check(self, body: ClientCheckRequest, options: dict[str, str] = None): api_response = self._api.check(body=req_body, **kwargs) return api_response - def _single_batch_check( + def _single_client_batch_check( self, body: ClientCheckRequest, options: dict[str, str] = None ): """ @@ -574,7 +586,7 @@ def _single_batch_check( """ try: api_response = self.check(body, options) - return BatchCheckResponse( + return ClientBatchCheckClientResponse( allowed=api_response.allowed, request=body, response=api_response, @@ -583,11 +595,11 @@ def _single_batch_check( except (AuthenticationError, UnauthorizedException) as err: raise err except Exception as err: - return BatchCheckResponse( + return ClientBatchCheckClientResponse( allowed=False, request=body, response=None, error=err ) - def batch_check( + def client_batch_check( self, body: list[ClientCheckRequest], options: dict[str, str | int] = None ): """ @@ -619,7 +631,7 @@ def batch_check( batch_check_response = [] def single_batch_check(request): - return self._single_batch_check(request, options) + return self._single_client_batch_check(request, options) with ThreadPoolExecutor(max_workers=max_parallel_requests) as executor: for response in executor.map(single_batch_check, body): @@ -627,6 +639,119 @@ def single_batch_check(request): return batch_check_response + def _single_batch_check( + self, + body: BatchCheckRequest, + options: dict[str, str] = None, + ): + """ + Run a single BatchCheck request + :param body - list[ClientCheckRequest] defining check request + :param authorization_model_id(options) - Overrides the authorization model id in the configuration + """ + try: + kwargs = options_to_kwargs(options) + api_response = self._api.batch_check(body, **kwargs) + return api_response + # Does this cover all error cases? If one fails with a 4xx/5xx then all should? + except Exception as err: + raise err + + def batch_check(self, body: ClientBatchCheckRequest, options=None): + """ + Run a batchcheck request + :param body - BatchCheck request + :param authorization_model_id(options) - Overrides the authorization model id in the configuration + :param max_parallel_requests(options) - Max number of requests to issue in parallel. Defaults to 10 + :param max_batch_size(options) - Max number of checks to include in a request. Defaults to 50 + :param header(options) - Custom headers to send alongside the request + :param retryParams(options) - Override the retry parameters for this request + :param retryParams.maxRetry(options) - Override the max number of retries on each API request + :param retryParams.minWaitInMs(options) - Override the minimum wait before a retry is initiated + """ + options = set_heading_if_not_set( + options, CLIENT_BULK_REQUEST_ID_HEADER, str(uuid.uuid4()) + ) + + max_parallel_requests = 10 + if options is not None and "max_parallel_requests" in options: + if ( + isinstance(options["max_parallel_requests"], str) + and options["max_parallel_requests"].isdigit() + ): + max_parallel_requests = int(options["max_parallel_requests"]) + elif isinstance(options["max_parallel_requests"], int): + max_parallel_requests = options["max_parallel_requests"] + + max_batch_size = 50 + if options is not None and "max_batch_size" in options: + if ( + isinstance(options["max_batch_size"], str) + and options["max_batch_size"].isdigit() + ): + max_batch_size = int(options["max_batch_size"]) + elif isinstance(options["max_batch_size"], int): + max_batch_size = options["max_batch_size"] + + id_to_check: dict[str, ClientBatchCheckItem] = {} + + def track_and_transform(checks): + transformed = [] + for check in checks: + if check.correlation_id is None: + check.correlation_id = str(uuid.uuid4()) + + if check.correlation_id in id_to_check: + raise FgaValidationException( + f"Duplicate correlation_id ({check.correlation_id}) provided" + ) + + id_to_check[check.correlation_id] = check + + transformed.append(construct_batch_item(check)) + return transformed + + checks = [ + track_and_transform( + body.checks[i * max_batch_size : (i + 1) * max_batch_size] + ) + for i in range((len(body.checks) + max_batch_size - 1) // max_batch_size) + ] + + def map_response(id, result): + check = id_to_check[id] + return ClientBatchCheckSingleResponse( + allowed=result.allowed, + request=check, + correlation_id=id, + error=result.error, + ) + + def single_batch_check(checks): + res = self._single_batch_check( + BatchCheckRequest( + checks=checks, + authorization_model_id=self._get_authorization_model_id(options), + consistency=self._get_consistency(options), + ), + options, + ) + + return res + + result = [] + + with ThreadPoolExecutor(max_workers=max_parallel_requests) as executor: + for response in executor.map(single_batch_check, checks): + result.extend( + [ + map_response(c_id, c_result) + for c_id, c_result in response.result.items() + ] + ) + + return ClientBatchCheckResponse(result) + def expand(self, body: ClientExpandRequest, options: dict[str, str] = None): """ Run expand request @@ -709,7 +834,7 @@ def list_relations( ) for i in body.relations ] - result = self.batch_check(request_body, options) + result = self.client_batch_check(request_body, options) # need to filter with the allowed response result_iterator = filter(_check_allowed, result) result_list = list(result_iterator) diff --git a/openfga_sdk/sync/open_fga_api.py b/openfga_sdk/sync/open_fga_api.py index 1a1910c..928430d 100644 --- a/openfga_sdk/sync/open_fga_api.py +++ b/openfga_sdk/sync/open_fga_api.py @@ -46,6 +46,186 @@ def __exit__(self): def close(self): self.api_client.close() + def batch_check(self, body, **kwargs): + """Send a list of `check` operations in a single request + + The `BatchCheck` API functions nearly identically to `Check`, but instead of checking a single user-object relationship BatchCheck accepts a list of relationships to check and returns a map containing `BatchCheckItem` response for each check it received. An associated `correlation_id` is required for each check in the batch. This ID is used to correlate a check to the appropriate response. It is a string consisting of only alphanumeric characters or hyphens with a maximum length of 36 characters. This `correlation_id` is used to map the result of each check to the item which was checked, so it must be unique for each item in the batch. We recommend using a UUID or ULID as the `correlation_id`, but you can use whatever unique identifier you need as long as it matches this regex pattern: `^[\\w\\d-]{1,36}$` For more details on how `Check` functions, see the docs for `/check`. ### Examples #### A BatchCheckRequest ```json { \"checks\": [ { \"tuple_key\": { \"object\": \"document:2021-budget\" \"relation\": \"reader\", \"user\": \"user:anne\", }, \"contextual_tuples\": {...} \"context\": {} \"correlation_id\": \"01JA8PM3QM7VBPGB8KMPK8SBD5\" }, { \"tuple_key\": { \"object\": \"document:2021-budget\" \"relation\": \"reader\", \"user\": \"user:bob\", }, \"contextual_tuples\": {...} \"context\": {} \"correlation_id\": \"01JA8PMM6A90NV5ET0F28CYSZQ\" } ] } ``` Below is a possible response to the above request. Note that the result map's keys are the `correlation_id` values from the checked items in the request: ```json { \"result\": { \"01JA8PMM6A90NV5ET0F28CYSZQ\": { \"allowed\": false, \"error\": {\"message\": \"\"} }, \"01JA8PM3QM7VBPGB8KMPK8SBD5\": { \"allowed\": true, \"error\": {\"message\": \"\"} } } ``` + + >>> thread = api.batch_check(body) + + :param body: (required) + :type body: BatchCheckRequest + :param async_req: Whether to execute the request asynchronously. + :type async_req: bool, optional + :param _preload_content: if False, the urllib3.HTTPResponse object will + be returned without reading/decoding response + data. Default is True. + :type _preload_content: bool, optional + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :return: Returns the result object. + If the method is called asynchronously, + returns the request thread. + :rtype: BatchCheckResponse + """ + kwargs["_return_http_data_only"] = True + return self.batch_check_with_http_info(body, **kwargs) + + def batch_check_with_http_info(self, body, **kwargs): + """Send a list of `check` operations in a single request + + The `BatchCheck` API functions nearly identically to `Check`, but instead of checking a single user-object relationship BatchCheck accepts a list of relationships to check and returns a map containing `BatchCheckItem` response for each check it received. An associated `correlation_id` is required for each check in the batch. This ID is used to correlate a check to the appropriate response. It is a string consisting of only alphanumeric characters or hyphens with a maximum length of 36 characters. This `correlation_id` is used to map the result of each check to the item which was checked, so it must be unique for each item in the batch. We recommend using a UUID or ULID as the `correlation_id`, but you can use whatever unique identifier you need as long as it matches this regex pattern: `^[\\w\\d-]{1,36}$` For more details on how `Check` functions, see the docs for `/check`. ### Examples #### A BatchCheckRequest ```json { \"checks\": [ { \"tuple_key\": { \"object\": \"document:2021-budget\" \"relation\": \"reader\", \"user\": \"user:anne\", }, \"contextual_tuples\": {...} \"context\": {} \"correlation_id\": \"01JA8PM3QM7VBPGB8KMPK8SBD5\" }, { \"tuple_key\": { \"object\": \"document:2021-budget\" \"relation\": \"reader\", \"user\": \"user:bob\", }, \"contextual_tuples\": {...} \"context\": {} \"correlation_id\": \"01JA8PMM6A90NV5ET0F28CYSZQ\" } ] } ``` Below is a possible response to the above request. Note that the result map's keys are the `correlation_id` values from the checked items in the request: ```json { \"result\": { \"01JA8PMM6A90NV5ET0F28CYSZQ\": { \"allowed\": false, \"error\": {\"message\": \"\"} }, \"01JA8PM3QM7VBPGB8KMPK8SBD5\": { \"allowed\": true, \"error\": {\"message\": \"\"} } } ``` + + >>> thread = api.batch_check_with_http_info(body) + + :param body: (required) + :type body: BatchCheckRequest + :param async_req: Whether to execute the request asynchronously. + :type async_req: bool, optional + :param _return_http_data_only: response data without head status code + and headers + :type _return_http_data_only: bool, optional + :param _preload_content: if False, the urllib3.HTTPResponse object will + be returned without reading/decoding response + data. Default is True. + :type _preload_content: bool, optional + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the authentication + in the spec for a single request. + :param _retry_param: if specified, override the retry parameters specified in configuration + :type _request_auth: dict, optional + :type _content_type: string, optional: force content-type for the request + :return: Returns the result object. + If the method is called asynchronously, + returns the request thread. + :rtype: tuple(BatchCheckResponse, status_code(int), headers(HTTPHeaderDict)) + """ + + local_var_params = locals() + + all_params = ["body"] + all_params.extend( + [ + "async_req", + "_return_http_data_only", + "_preload_content", + "_request_timeout", + "_request_auth", + "_content_type", + "_headers", + "_retry_params", + ] + ) + + for key, val in local_var_params["kwargs"].items(): + if key not in all_params: + raise FgaValidationException( + "Got an unexpected keyword argument '%s'" + " to method batch_check" % key + ) + local_var_params[key] = val + del local_var_params["kwargs"] + # verify the required parameter 'body' is set + if ( + self.api_client.client_side_validation + and local_var_params.get("body") is None + ): + raise ApiValueError( + "Missing the required parameter `body` when calling `batch_check`" + ) + + collection_formats = {} + + path_params = {} + + store_id = None + + if self.api_client._get_store_id() is None: + raise ApiValueError( + "Store ID expected in api_client's configuration when calling `batch_check`" + ) + store_id = self.api_client._get_store_id() + + query_params = [] + + header_params = dict(local_var_params.get("_headers", {})) + + form_params = [] + local_var_files = {} + + body_params = None + if "body" in local_var_params: + body_params = local_var_params["body"] + # HTTP header `Accept` + header_params["Accept"] = self.api_client.select_header_accept( + ["application/json"] + ) + + # HTTP header `Content-Type` + content_types_list = local_var_params.get( + "_content_type", + self.api_client.select_header_content_type( + ["application/json"], "POST", body_params + ), + ) + if content_types_list: + header_params["Content-Type"] = content_types_list + + # Authentication setting + auth_settings = [] + + response_types_map = { + 200: "BatchCheckResponse", + 400: "ValidationErrorMessageResponse", + 401: "UnauthenticatedResponse", + 403: "ForbiddenResponse", + 404: "PathUnknownErrorMessageResponse", + 409: "AbortedMessageResponse", + 422: "UnprocessableContentMessageResponse", + 500: "InternalErrorMessageResponse", + } + + telemetry_attributes: dict[TelemetryAttribute, str | int] = { + TelemetryAttributes.fga_client_request_method: "batch_check", + TelemetryAttributes.fga_client_request_store_id: self.api_client.get_store_id(), + TelemetryAttributes.fga_client_request_model_id: local_var_params.get( + "authorization_model_id", "" + ), + } + + telemetry_attributes = TelemetryAttributes.fromBody( + body=body_params, + attributes=telemetry_attributes, + ) + + return self.api_client.call_api( + "/stores/{store_id}/batch-check".replace("{store_id}", store_id), + "POST", + path_params, + query_params, + header_params, + body=body_params, + post_params=form_params, + files=local_var_files, + response_types_map=response_types_map, + auth_settings=auth_settings, + async_req=local_var_params.get("async_req"), + _return_http_data_only=local_var_params.get("_return_http_data_only"), + _preload_content=local_var_params.get("_preload_content", True), + _request_timeout=local_var_params.get("_request_timeout"), + _retry_params=local_var_params.get("_retry_params"), + collection_formats=collection_formats, + _request_auth=local_var_params.get("_request_auth"), + _oauth2_client=self._oauth2_client, + _telemetry_attributes=telemetry_attributes, + ) + def check(self, body, **kwargs): """Check whether a user is authorized to access an object @@ -198,6 +378,11 @@ def check_with_http_info(self, body, **kwargs): ), } + telemetry_attributes = TelemetryAttributes.fromBody( + body=body_params, + attributes=telemetry_attributes, + ) + return self.api_client.call_api( "/stores/{store_id}/check".replace("{store_id}", store_id), "POST", @@ -359,6 +544,11 @@ def create_store_with_http_info(self, body, **kwargs): ), } + telemetry_attributes = TelemetryAttributes.fromBody( + body=body_params, + attributes=telemetry_attributes, + ) + return self.api_client.call_api( "/stores", "POST", @@ -501,6 +691,11 @@ def delete_store_with_http_info(self, **kwargs): ), } + telemetry_attributes = TelemetryAttributes.fromBody( + body=body_params, + attributes=telemetry_attributes, + ) + return self.api_client.call_api( "/stores/{store_id}".replace("{store_id}", store_id), "DELETE", @@ -675,6 +870,11 @@ def expand_with_http_info(self, body, **kwargs): ), } + telemetry_attributes = TelemetryAttributes.fromBody( + body=body_params, + attributes=telemetry_attributes, + ) + return self.api_client.call_api( "/stores/{store_id}/expand".replace("{store_id}", store_id), "POST", @@ -826,6 +1026,11 @@ def get_store_with_http_info(self, **kwargs): ), } + telemetry_attributes = TelemetryAttributes.fromBody( + body=body_params, + attributes=telemetry_attributes, + ) + return self.api_client.call_api( "/stores/{store_id}".replace("{store_id}", store_id), "GET", @@ -1001,6 +1206,11 @@ def list_objects_with_http_info(self, body, **kwargs): ), } + telemetry_attributes = TelemetryAttributes.fromBody( + body=body_params, + attributes=telemetry_attributes, + ) + return self.api_client.call_api( "/stores/{store_id}/list-objects".replace("{store_id}", store_id), "POST", @@ -1160,6 +1370,11 @@ def list_stores_with_http_info(self, **kwargs): ), } + telemetry_attributes = TelemetryAttributes.fromBody( + body=body_params, + attributes=telemetry_attributes, + ) + return self.api_client.call_api( "/stores", "GET", @@ -1335,6 +1550,11 @@ def list_users_with_http_info(self, body, **kwargs): ), } + telemetry_attributes = TelemetryAttributes.fromBody( + body=body_params, + attributes=telemetry_attributes, + ) + return self.api_client.call_api( "/stores/{store_id}/list-users".replace("{store_id}", store_id), "POST", @@ -1509,6 +1729,11 @@ def read_with_http_info(self, body, **kwargs): ), } + telemetry_attributes = TelemetryAttributes.fromBody( + body=body_params, + attributes=telemetry_attributes, + ) + return self.api_client.call_api( "/stores/{store_id}/read".replace("{store_id}", store_id), "POST", @@ -1677,6 +1902,11 @@ def read_assertions_with_http_info(self, authorization_model_id, **kwargs): ), } + telemetry_attributes = TelemetryAttributes.fromBody( + body=body_params, + attributes=telemetry_attributes, + ) + return self.api_client.call_api( "/stores/{store_id}/assertions/{authorization_model_id}".replace( "{store_id}", store_id @@ -1845,6 +2075,11 @@ def read_authorization_model_with_http_info(self, id, **kwargs): ), } + telemetry_attributes = TelemetryAttributes.fromBody( + body=body_params, + attributes=telemetry_attributes, + ) + return self.api_client.call_api( "/stores/{store_id}/authorization-models/{id}".replace( "{store_id}", store_id @@ -2012,6 +2247,11 @@ def read_authorization_models_with_http_info(self, **kwargs): ), } + telemetry_attributes = TelemetryAttributes.fromBody( + body=body_params, + attributes=telemetry_attributes, + ) + return self.api_client.call_api( "/stores/{store_id}/authorization-models".replace("{store_id}", store_id), "GET", @@ -2189,6 +2429,11 @@ def read_changes_with_http_info(self, **kwargs): ), } + telemetry_attributes = TelemetryAttributes.fromBody( + body=body_params, + attributes=telemetry_attributes, + ) + return self.api_client.call_api( "/stores/{store_id}/changes".replace("{store_id}", store_id), "GET", @@ -2363,6 +2608,11 @@ def write_with_http_info(self, body, **kwargs): ), } + telemetry_attributes = TelemetryAttributes.fromBody( + body=body_params, + attributes=telemetry_attributes, + ) + return self.api_client.call_api( "/stores/{store_id}/write".replace("{store_id}", store_id), "POST", @@ -2548,6 +2798,11 @@ def write_assertions_with_http_info(self, authorization_model_id, body, **kwargs ), } + telemetry_attributes = TelemetryAttributes.fromBody( + body=body_params, + attributes=telemetry_attributes, + ) + return self.api_client.call_api( "/stores/{store_id}/assertions/{authorization_model_id}".replace( "{store_id}", store_id @@ -2725,6 +2980,11 @@ def write_authorization_model_with_http_info(self, body, **kwargs): ), } + telemetry_attributes = TelemetryAttributes.fromBody( + body=body_params, + attributes=telemetry_attributes, + ) + return self.api_client.call_api( "/stores/{store_id}/authorization-models".replace("{store_id}", store_id), "POST", diff --git a/openfga_sdk/telemetry/attributes.py b/openfga_sdk/telemetry/attributes.py index f09caf6..81360eb 100644 --- a/openfga_sdk/telemetry/attributes.py +++ b/openfga_sdk/telemetry/attributes.py @@ -1,6 +1,6 @@ import time import urllib -from typing import NamedTuple +from typing import Any, NamedTuple from aiohttp import ClientResponse from urllib3 import HTTPResponse @@ -19,6 +19,9 @@ class TelemetryAttribute(NamedTuple): class TelemetryAttributes: + fga_client_request_batch_check_size: TelemetryAttribute = TelemetryAttribute( + name="fga-client.request.batch_check_size", format="int" + ) fga_client_request_client_id: TelemetryAttribute = TelemetryAttribute( name="fga-client.request.client_id", ) @@ -70,6 +73,7 @@ class TelemetryAttributes: ) _attributes: list[TelemetryAttribute] = [ + fga_client_request_batch_check_size, fga_client_request_client_id, fga_client_request_method, fga_client_request_model_id, @@ -165,6 +169,23 @@ def prepare( return response + @staticmethod + def fromBody(body: Any, attributes: dict[TelemetryAttribute, str | int] = None): + from openfga_sdk.models.batch_check_request import BatchCheckRequest + + if attributes is None: + attributes = {} + + if ( + TelemetryAttributes.fga_client_request_batch_check_size not in attributes + and isinstance(body, BatchCheckRequest) + ): + attributes[TelemetryAttributes.fga_client_request_batch_check_size] = len( + body.checks + ) + + return attributes + @staticmethod def fromRequest( user_agent: str = None, diff --git a/openfga_sdk/telemetry/configuration.py b/openfga_sdk/telemetry/configuration.py index 7d780b6..3ba7701 100644 --- a/openfga_sdk/telemetry/configuration.py +++ b/openfga_sdk/telemetry/configuration.py @@ -32,6 +32,7 @@ def __init__( url_scheme: bool | None = None, url_full: bool | None = None, user_agent_original: bool | None = None, + fga_client_request_batch_check_size: bool | None = None, ): """ Initialize a new instance of the `TelemetryMetricConfiguration` class. @@ -52,6 +53,7 @@ def __init__( :param url_scheme: The `url.scheme` attribute includes the scheme of the request URL. :param url_full: The `url.full` attribute includes the full URL of the request. :param user_agent_original: The `user_agent.original` attribute includes the original user agent string of the request. + :param fga_client_request_batch_check_size: The `fga-client.request.batch_check_size` attribute includes the size of the `checks` list in a `BatchCheck` request. """ self.configure( @@ -59,6 +61,11 @@ def __init__( clear=True, ) + if fga_client_request_batch_check_size is not None: + self._state[TelemetryAttributes.fga_client_request_batch_check_size] = ( + fga_client_request_batch_check_size + ) + if fga_client_request_client_id is not None: self._state[TelemetryAttributes.fga_client_request_client_id] = ( fga_client_request_client_id @@ -124,6 +131,25 @@ def __init__( self._valid = None # Reset the validation state + @property + def fga_client_request_batch_check_size(self) -> bool: + """ + Get the configuration for the `fga_client_request_batch_check_size` attribute. + + :return: The configuration for the `fga_client_request_batch_check_size` attribute. + """ + return self._state[TelemetryAttributes.fga_client_request_batch_check_size] + + @fga_client_request_batch_check_size.setter + def fga_client_request_batch_check_size(self, value: bool): + """ + Set the configuration for the `fga_client_request_batch_check_size` attribute. + + :param value: The configuration for the `fga_client_request_batch_check_size` attribute. + """ + self._valid = None # Reset the validation state + self._state[TelemetryAttributes.fga_client_request_batch_check_size] = value + @property def fga_client_request_client_id(self) -> bool: """ @@ -446,6 +472,7 @@ def clear(self) -> None: # Reset the configuration to the default state self._state = { + TelemetryAttributes.fga_client_request_batch_check_size: False, TelemetryAttributes.fga_client_request_client_id: False, TelemetryAttributes.fga_client_request_method: False, TelemetryAttributes.fga_client_request_model_id: False, @@ -578,6 +605,7 @@ def getSdkDefaults() -> dict[TelemetryAttribute, bool]: :return: The default SDK configuration for the telemetry metric. """ return { + TelemetryAttributes.fga_client_request_batch_check_size: False, TelemetryAttributes.fga_client_request_client_id: True, TelemetryAttributes.fga_client_request_method: True, TelemetryAttributes.fga_client_request_model_id: True, diff --git a/test/client/client_test.py b/test/client/client_test.py index 5609b54..bc13570 100644 --- a/test/client/client_test.py +++ b/test/client/client_test.py @@ -10,6 +10,7 @@ NOTE: This file was auto generated by OpenAPI Generator (https://openapi-generator.tech). DO NOT EDIT. """ +import uuid from datetime import datetime from unittest import IsolatedAsyncioTestCase from unittest.mock import ANY, patch @@ -20,6 +21,8 @@ from openfga_sdk.client import ClientConfiguration from openfga_sdk.client.client import OpenFgaClient from openfga_sdk.client.models.assertion import ClientAssertion +from openfga_sdk.client.models.batch_check_item import ClientBatchCheckItem +from openfga_sdk.client.models.batch_check_request import ClientBatchCheckRequest from openfga_sdk.client.models.check_request import ClientCheckRequest from openfga_sdk.client.models.expand_request import ClientExpandRequest from openfga_sdk.client.models.list_objects_request import ClientListObjectsRequest @@ -1917,7 +1920,7 @@ async def test_check_config_auth_model(self, mock_request): await api_client.close() @patch.object(rest.RESTClientObject, "request") - async def test_batch_check_single_request(self, mock_request): + async def test_client_batch_check_single_request(self, mock_request): """Test case for check with single request Check whether a user is authorized to access an object @@ -1936,7 +1939,7 @@ async def test_batch_check_single_request(self, mock_request): configuration = self.configuration configuration.store_id = store_id async with OpenFgaClient(configuration) as api_client: - api_response = await api_client.batch_check( + api_response = await api_client.client_batch_check( body=[body], options={"authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1"}, ) @@ -1966,7 +1969,7 @@ async def test_batch_check_single_request(self, mock_request): await api_client.close() @patch.object(rest.RESTClientObject, "request") - async def test_batch_check_multiple_request(self, mock_request): + async def test_client_batch_check_multiple_request(self, mock_request): """Test case for check with multiple request Check whether a user is authorized to access an object @@ -1996,7 +1999,7 @@ async def test_batch_check_multiple_request(self, mock_request): configuration = self.configuration configuration.store_id = store_id async with OpenFgaClient(configuration) as api_client: - api_response = await api_client.batch_check( + api_response = await api_client.client_batch_check( body=[body1, body2, body3], options={ "authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1", @@ -2069,7 +2072,7 @@ async def test_batch_check_multiple_request(self, mock_request): await api_client.close() @patch.object(rest.RESTClientObject, "request") - async def test_batch_check_multiple_request_fail(self, mock_request): + async def test_client_batch_check_multiple_request_fail(self, mock_request): """Test case for check with multiple request with one request failed Check whether a user is authorized to access an object @@ -2105,7 +2108,7 @@ async def test_batch_check_multiple_request_fail(self, mock_request): configuration = self.configuration configuration.store_id = store_id async with OpenFgaClient(configuration) as api_client: - api_response = await api_client.batch_check( + api_response = await api_client.client_batch_check( body=[body1, body2, body3], options={ "authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1", @@ -2180,6 +2183,361 @@ async def test_batch_check_multiple_request_fail(self, mock_request): ) await api_client.close() + @patch.object(rest.RESTClientObject, "request") + async def test_batch_check_single_request(self, mock_request): + """Test case for check with single request + + Check whether a user is authorized to access an object + """ + + # First, mock the response + response_body = """ + { + "result": { + "1": { + "allowed": true + } + } + } + """ + mock_request.side_effect = [ + mock_response(response_body, 200), + ] + + body = ClientBatchCheckRequest( + checks=[ + ClientBatchCheckItem( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", + correlation_id="1", + ), + ] + ) + configuration = self.configuration + configuration.store_id = store_id + async with OpenFgaClient(configuration) as api_client: + api_response = await api_client.batch_check( + body=body, + options={"authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1"}, + ) + self.assertEqual(len(api_response.result), 1) + self.assertEqual(api_response.result[0].error, None) + self.assertTrue(api_response.result[0].allowed) + self.assertEqual(api_response.result[0].correlation_id, "1") + self.assertEqual(api_response.result[0].request, body.checks[0]) + # Make sure the API was called with the right data + mock_request.assert_any_call( + "POST", + "http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/batch-check", + headers=ANY, + query_params=[], + post_params=[], + body={ + "checks": [ + { + "tuple_key": { + "object": "document:2021-budget", + "relation": "reader", + "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31b", + }, + "correlation_id": "1", + } + ], + "authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1", + }, + _preload_content=ANY, + _request_timeout=None, + ) + await api_client.close() + + @patch.object(uuid, "uuid4") + @patch.object(rest.RESTClientObject, "request") + async def test_batch_check_multiple_request(self, mock_request, mock_uuid): + """Test case for check with multiple request + + Check whether a user is authorized to access an object + """ + first_response_body = """ + { + "result": { + "1": { + "allowed": true + }, + "2": { + "allowed": false + } + } + } +""" + + second_response_body = """ +{ + "result": { + "fake-uuid": { + "error": { + "input_error": "validation_error", + "message": "type 'doc' not found" + } + } + } +}""" + + # First, mock the response + mock_request.side_effect = [ + mock_response(first_response_body, 200), + mock_response(second_response_body, 200), + ] + + def mock_v4(val: str): + return val + + mock_uuid.side_effect = [mock_v4("batch-id-header"), mock_v4("fake-uuid")] + + body = ClientBatchCheckRequest( + checks=[ + ClientBatchCheckItem( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", + correlation_id="1", + ), + ClientBatchCheckItem( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31c", + correlation_id="2", + ), + ClientBatchCheckItem( + object="doc:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31d", + ), + ] + ) + + configuration = self.configuration + configuration.store_id = store_id + async with OpenFgaClient(configuration) as api_client: + api_response = await api_client.batch_check( + body=body, + options={ + "authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1", + "max_parallel_requests": 1, + "max_batch_size": 2, + }, + ) + self.assertEqual(len(api_response.result), 3) + self.assertEqual(api_response.result[0].error, None) + self.assertTrue(api_response.result[0].allowed) + self.assertEqual(api_response.result[1].error, None) + self.assertFalse(api_response.result[1].allowed) + self.assertEqual( + api_response.result[2].error.message, "type 'doc' not found" + ) + self.assertFalse(api_response.result[2].allowed) + # value generated from the uuid mock + self.assertEqual(api_response.result[2].correlation_id, "fake-uuid") + # Make sure the API was called with the right data + mock_request.assert_any_call( + "POST", + "http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/batch-check", + headers=ANY, + query_params=[], + post_params=[], + body={ + "checks": [ + { + "tuple_key": { + "object": "document:2021-budget", + "relation": "reader", + "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31b", + }, + "correlation_id": "1", + }, + { + "tuple_key": { + "object": "document:2021-budget", + "relation": "reader", + "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31c", + }, + "correlation_id": "2", + }, + ], + "authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1", + }, + _preload_content=ANY, + _request_timeout=None, + ) + mock_request.assert_any_call( + "POST", + "http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/batch-check", + headers=ANY, + query_params=[], + post_params=[], + body={ + "checks": [ + { + "tuple_key": { + "object": "doc:2021-budget", + "relation": "reader", + "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31d", + }, + "correlation_id": "fake-uuid", + } + ], + "authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1", + }, + _preload_content=ANY, + _request_timeout=None, + ) + await api_client.close() + + async def test_batch_check_errors_dupe_cor_id(self): + """Test case for duplicate correlation_id being provided to batch_check""" + + body = ClientBatchCheckRequest( + checks=[ + ClientBatchCheckItem( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", + correlation_id="1", + ), + ClientBatchCheckItem( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", + correlation_id="1", + ), + ] + ) + configuration = self.configuration + configuration.store_id = store_id + async with OpenFgaClient(configuration) as api_client: + with self.assertRaises(FgaValidationException) as error: + await api_client.batch_check( + body=body, + options={"authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1"}, + ) + self.assertEqual( + "Duplicate correlation_id (1) provided", str(error.exception) + ) + await api_client.close() + + @patch.object(rest.RESTClientObject, "request") + async def test_batch_check_errors_unauthorized(self, mock_request): + """Test case for BatchCheck with a 401""" + first_response_body = """ + { + "result": { + "1": { + "allowed": true + }, + "2": { + "allowed": false + } + } + } +""" + + # First, mock the response + mock_request.side_effect = [ + mock_response(first_response_body, 200), + UnauthorizedException(http_resp=http_mock_response("{}", 401)), + ] + + body = ClientBatchCheckRequest( + checks=[ + ClientBatchCheckItem( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", + correlation_id="1", + ), + ClientBatchCheckItem( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31c", + correlation_id="2", + ), + ClientBatchCheckItem( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31d", + correlation_id="3", + ), + ] + ) + + configuration = self.configuration + configuration.store_id = store_id + async with OpenFgaClient(configuration) as api_client: + with self.assertRaises(UnauthorizedException): + await api_client.batch_check( + body=body, + options={ + "authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1", + "max_parallel_requests": 1, + "max_batch_size": 2, + }, + ) + + # Make sure the API was called with the right data + mock_request.assert_any_call( + "POST", + "http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/batch-check", + headers=ANY, + query_params=[], + post_params=[], + body={ + "checks": [ + { + "tuple_key": { + "object": "document:2021-budget", + "relation": "reader", + "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31b", + }, + "correlation_id": "1", + }, + { + "tuple_key": { + "object": "document:2021-budget", + "relation": "reader", + "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31c", + }, + "correlation_id": "2", + }, + ], + "authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1", + }, + _preload_content=ANY, + _request_timeout=None, + ) + mock_request.assert_any_call( + "POST", + "http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/batch-check", + headers=ANY, + query_params=[], + post_params=[], + body={ + "checks": [ + { + "tuple_key": { + "object": "document:2021-budget", + "relation": "reader", + "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31d", + }, + "correlation_id": "3", + } + ], + "authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1", + }, + _preload_content=ANY, + _request_timeout=None, + ) + await api_client.close() + @patch.object(rest.RESTClientObject, "request") async def test_expand(self, mock_request): """Test case for expand diff --git a/test/sync/client/client_test.py b/test/sync/client/client_test.py index 938a2aa..f7eabe0 100644 --- a/test/sync/client/client_test.py +++ b/test/sync/client/client_test.py @@ -10,6 +10,7 @@ NOTE: This file was auto generated by OpenAPI Generator (https://openapi-generator.tech). DO NOT EDIT. """ +import uuid from datetime import datetime from unittest import IsolatedAsyncioTestCase from unittest.mock import ANY, patch @@ -18,6 +19,8 @@ from openfga_sdk.client import ClientConfiguration from openfga_sdk.client.models.assertion import ClientAssertion +from openfga_sdk.client.models.batch_check_item import ClientBatchCheckItem +from openfga_sdk.client.models.batch_check_request import ClientBatchCheckRequest from openfga_sdk.client.models.check_request import ClientCheckRequest from openfga_sdk.client.models.expand_request import ClientExpandRequest from openfga_sdk.client.models.list_objects_request import ClientListObjectsRequest @@ -1916,7 +1919,7 @@ def test_check_config_auth_model(self, mock_request): api_client.close() @patch.object(rest.RESTClientObject, "request") - def test_batch_check_single_request(self, mock_request): + def test_client_batch_check_single_request(self, mock_request): """Test case for check with single request Check whether a user is authorized to access an object @@ -1935,7 +1938,7 @@ def test_batch_check_single_request(self, mock_request): configuration = self.configuration configuration.store_id = store_id with OpenFgaClient(configuration) as api_client: - api_response = api_client.batch_check( + api_response = api_client.client_batch_check( body=[body], options={ "authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1", @@ -1969,7 +1972,7 @@ def test_batch_check_single_request(self, mock_request): api_client.close() @patch.object(rest.RESTClientObject, "request") - def test_batch_check_multiple_request(self, mock_request): + def test_client_batch_check_multiple_request(self, mock_request): """Test case for check with multiple request Check whether a user is authorized to access an object @@ -1999,7 +2002,7 @@ def test_batch_check_multiple_request(self, mock_request): configuration = self.configuration configuration.store_id = store_id with OpenFgaClient(configuration) as api_client: - api_response = api_client.batch_check( + api_response = api_client.client_batch_check( body=[body1, body2, body3], options={ "authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1", @@ -2072,7 +2075,7 @@ def test_batch_check_multiple_request(self, mock_request): api_client.close() @patch.object(rest.RESTClientObject, "request") - def test_batch_check_multiple_request_fail(self, mock_request): + def test_client_batch_check_multiple_request_fail(self, mock_request): """Test case for check with multiple request with one request failed Check whether a user is authorized to access an object @@ -2108,7 +2111,7 @@ def test_batch_check_multiple_request_fail(self, mock_request): configuration = self.configuration configuration.store_id = store_id with OpenFgaClient(configuration) as api_client: - api_response = api_client.batch_check( + api_response = api_client.client_batch_check( body=[body1, body2, body3], options={ "authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1", @@ -2183,6 +2186,360 @@ def test_batch_check_multiple_request_fail(self, mock_request): ) api_client.close() + @patch.object(rest.RESTClientObject, "request") + def test_batch_check_single_request(self, mock_request): + """Test case for check with single request + + Check whether a user is authorized to access an object + """ + + # First, mock the response + response_body = """ + { + "result": { + "1": { + "allowed": true + } + } + } + """ + mock_request.side_effect = [ + mock_response(response_body, 200), + ] + + body = ClientBatchCheckRequest( + checks=[ + ClientBatchCheckItem( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", + correlation_id="1", + ), + ] + ) + configuration = self.configuration + configuration.store_id = store_id + with OpenFgaClient(configuration) as api_client: + api_response = api_client.batch_check( + body=body, + options={"authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1"}, + ) + self.assertEqual(len(api_response.result), 1) + self.assertEqual(api_response.result[0].error, None) + self.assertTrue(api_response.result[0].allowed) + self.assertEqual(api_response.result[0].correlation_id, "1") + self.assertEqual(api_response.result[0].request, body.checks[0]) + # Make sure the API was called with the right data + mock_request.assert_any_call( + "POST", + "http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/batch-check", + headers=ANY, + query_params=[], + post_params=[], + body={ + "checks": [ + { + "tuple_key": { + "object": "document:2021-budget", + "relation": "reader", + "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31b", + }, + "correlation_id": "1", + } + ], + "authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1", + }, + _preload_content=ANY, + _request_timeout=None, + ) + api_client.close() + + @patch.object(uuid, "uuid4") + @patch.object(rest.RESTClientObject, "request") + def test_batch_check_multiple_request(self, mock_request, mock_uuid): + """Test case for check with multiple request + + Check whether a user is authorized to access an object + """ + first_response_body = """ + { + "result": { + "1": { + "allowed": true + }, + "2": { + "allowed": false + } + } + } +""" + + second_response_body = """ +{ + "result": { + "fake-uuid": { + "error": { + "input_error": "validation_error", + "message": "type 'doc' not found" + } + } + } +}""" + # First, mock the response + mock_request.side_effect = [ + mock_response(first_response_body, 200), + mock_response(second_response_body, 200), + ] + + def mock_v4(val: str): + return val + + mock_uuid.side_effect = [mock_v4("batch-id-header"), mock_v4("fake-uuid")] + + body = ClientBatchCheckRequest( + checks=[ + ClientBatchCheckItem( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", + correlation_id="1", + ), + ClientBatchCheckItem( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31c", + correlation_id="2", + ), + ClientBatchCheckItem( + object="doc:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31d", + ), + ] + ) + + configuration = self.configuration + configuration.store_id = store_id + with OpenFgaClient(configuration) as api_client: + api_response = api_client.batch_check( + body=body, + options={ + "authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1", + "max_parallel_requests": 1, + "max_batch_size": 2, + }, + ) + self.assertEqual(len(api_response.result), 3) + self.assertEqual(api_response.result[0].error, None) + self.assertTrue(api_response.result[0].allowed) + self.assertEqual(api_response.result[1].error, None) + self.assertFalse(api_response.result[1].allowed) + self.assertEqual( + api_response.result[2].error.message, "type 'doc' not found" + ) + self.assertFalse(api_response.result[2].allowed) + # value generated from the uuid mock + self.assertEqual(api_response.result[2].correlation_id, "fake-uuid") + # Make sure the API was called with the right data + mock_request.assert_any_call( + "POST", + "http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/batch-check", + headers=ANY, + query_params=[], + post_params=[], + body={ + "checks": [ + { + "tuple_key": { + "object": "document:2021-budget", + "relation": "reader", + "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31b", + }, + "correlation_id": "1", + }, + { + "tuple_key": { + "object": "document:2021-budget", + "relation": "reader", + "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31c", + }, + "correlation_id": "2", + }, + ], + "authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1", + }, + _preload_content=ANY, + _request_timeout=None, + ) + mock_request.assert_any_call( + "POST", + "http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/batch-check", + headers=ANY, + query_params=[], + post_params=[], + body={ + "checks": [ + { + "tuple_key": { + "object": "doc:2021-budget", + "relation": "reader", + "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31d", + }, + "correlation_id": "fake-uuid", + } + ], + "authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1", + }, + _preload_content=ANY, + _request_timeout=None, + ) + api_client.close() + + def test_batch_check_errors_dupe_cor_id(self): + """Test case for duplicate correlation_id being provided to batch_check""" + + body = ClientBatchCheckRequest( + checks=[ + ClientBatchCheckItem( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", + correlation_id="1", + ), + ClientBatchCheckItem( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", + correlation_id="1", + ), + ] + ) + configuration = self.configuration + configuration.store_id = store_id + with OpenFgaClient(configuration) as api_client: + with self.assertRaises(FgaValidationException) as error: + api_client.batch_check( + body=body, + options={"authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1"}, + ) + self.assertEqual( + "Duplicate correlation_id (1) provided", str(error.exception) + ) + api_client.close() + + @patch.object(rest.RESTClientObject, "request") + def test_batch_check_errors_unauthorized(self, mock_request): + """Test case for BatchCheck with a 401""" + first_response_body = """ + { + "result": { + "1": { + "allowed": true + }, + "2": { + "allowed": false + } + } + } +""" + + # First, mock the response + mock_request.side_effect = [ + mock_response(first_response_body, 200), + UnauthorizedException(http_resp=http_mock_response("{}", 401)), + ] + + body = ClientBatchCheckRequest( + checks=[ + ClientBatchCheckItem( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", + correlation_id="1", + ), + ClientBatchCheckItem( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31c", + correlation_id="2", + ), + ClientBatchCheckItem( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31d", + correlation_id="3", + ), + ] + ) + + configuration = self.configuration + configuration.store_id = store_id + with OpenFgaClient(configuration) as api_client: + with self.assertRaises(UnauthorizedException): + api_client.batch_check( + body=body, + options={ + "authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1", + "max_parallel_requests": 1, + "max_batch_size": 2, + }, + ) + + # Make sure the API was called with the right data + mock_request.assert_any_call( + "POST", + "http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/batch-check", + headers=ANY, + query_params=[], + post_params=[], + body={ + "checks": [ + { + "tuple_key": { + "object": "document:2021-budget", + "relation": "reader", + "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31b", + }, + "correlation_id": "1", + }, + { + "tuple_key": { + "object": "document:2021-budget", + "relation": "reader", + "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31c", + }, + "correlation_id": "2", + }, + ], + "authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1", + }, + _preload_content=ANY, + _request_timeout=None, + ) + mock_request.assert_any_call( + "POST", + "http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/batch-check", + headers=ANY, + query_params=[], + post_params=[], + body={ + "checks": [ + { + "tuple_key": { + "object": "document:2021-budget", + "relation": "reader", + "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31d", + }, + "correlation_id": "3", + } + ], + "authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1", + }, + _preload_content=ANY, + _request_timeout=None, + ) + api_client.close() + @patch.object(rest.RESTClientObject, "request") def test_expand(self, mock_request): """Test case for expand diff --git a/test/telemetry/attributes_test.py b/test/telemetry/attributes_test.py index af001c1..14b854b 100644 --- a/test/telemetry/attributes_test.py +++ b/test/telemetry/attributes_test.py @@ -5,6 +5,8 @@ from urllib3 import HTTPResponse from openfga_sdk.credentials import CredentialConfiguration, Credentials +from openfga_sdk.models.batch_check_request import BatchCheckRequest +from openfga_sdk.models.check_request import CheckRequest from openfga_sdk.rest import RESTResponse from openfga_sdk.telemetry.attributes import ( TelemetryAttributes, @@ -20,14 +22,19 @@ def test_prepare_with_valid_attributes(telemetry_attributes): attributes = { telemetry_attributes.fga_client_request_client_id: "client_123", telemetry_attributes.http_request_method: "GET", + telemetry_attributes.fga_client_request_batch_check_size: 3, } - filter_attributes = [telemetry_attributes.fga_client_request_client_id] + filter_attributes = [ + telemetry_attributes.fga_client_request_client_id, + telemetry_attributes.fga_client_request_batch_check_size, + ] prepared = telemetry_attributes.prepare(attributes, filter=filter_attributes) # Assert that only filtered attributes are returned assert prepared == { "fga-client.request.client_id": "client_123", + "fga-client.request.batch_check_size": 3, } @@ -145,3 +152,20 @@ def test_from_response_with_rest_response(telemetry_attributes): assert attributes[TelemetryAttributes.fga_client_response_model_id] == "model_404" assert attributes[TelemetryAttributes.http_server_request_duration] == "100" assert attributes[TelemetryAttributes.fga_client_request_client_id] == "client_456" + + +def test_from_body_with_batch_check(telemetry_attributes): + body = MagicMock(spec=BatchCheckRequest) + body.checks = ["1", "2", "3"] + + attributes = telemetry_attributes.fromBody(body=body) + + assert attributes[TelemetryAttributes.fga_client_request_batch_check_size] == 3 + + +def test_from_body_with_other_body(telemetry_attributes): + body = MagicMock(spec=CheckRequest) + + attributes = telemetry_attributes.fromBody(body=body) + + assert attributes == {} diff --git a/test/telemetry/configuration_test.py b/test/telemetry/configuration_test.py index 61f3170..7b3761a 100644 --- a/test/telemetry/configuration_test.py +++ b/test/telemetry/configuration_test.py @@ -12,6 +12,7 @@ def test_telemetry_metric_configuration_default_initialization(): config = TelemetryMetricConfiguration() + assert config.fga_client_request_batch_check_size is False assert config.fga_client_request_client_id is False assert config.fga_client_request_method is False assert config.fga_client_request_model_id is False @@ -292,8 +293,11 @@ def test_default_telemetry_metric_configuration(): metric_config = TelemetryMetricConfiguration.getSdkDefaults() assert isinstance(metric_config, dict) - assert len(metric_config) == 15 + assert len(metric_config) == 16 + assert ( + metric_config[TelemetryAttributes.fga_client_request_batch_check_size] is False + ) assert metric_config[TelemetryAttributes.fga_client_request_client_id] is True assert metric_config[TelemetryAttributes.fga_client_request_method] is True assert metric_config[TelemetryAttributes.fga_client_request_model_id] is True