Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 83 additions & 0 deletions docs/dictionary-requests.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# Dictionary request bodies

Both the asynchronous and synchronous `OpenFgaClient` accept dictionaries in
place of request model instances. Dictionary keys use the same names as the
corresponding Python constructor arguments, rather than the low-level HTTP
request shape.

For example, these checks are equivalent:

```python
from openfga_sdk.client.models import ClientCheckRequest

result = await fga_client.check(
ClientCheckRequest(
user="user:bob",
relation="viewer",
object="document:roadmap",
)
)

result = await fga_client.check(
{
"user": "user:bob",
"relation": "viewer",
"object": "document:roadmap",
}
)
```

The synchronous client accepts the same bodies without `await`.

## Nested tuples and batch checks

Nested contextual tuples, write/delete tuples, and batch check items can also
be dictionaries. Existing model instances can be included in those lists.

```python
await fga_client.write(
{
"writes": [
{
"user": "user:bob",
"relation": "viewer",
"object": "document:roadmap",
}
]
}
)

result = await fga_client.batch_check(
{
"checks": [
{
"user": "user:bob",
"relation": "viewer",
"object": "document:roadmap",
"correlation_id": "roadmap-view",
}
]
}
)
```

Methods that take a list (`write_tuples`, `delete_tuples`, `client_batch_check`,
and `write_assertions`) continue to take a list, with dictionaries permitted
as its elements. Options remain a separate argument.

For dictionary-only requests, the SDK does not insert generated correlation IDs
or replace nested tuples in the supplied dictionaries. Existing model instances
keep their existing behavior: for example, BatchCheck assigns a correlation ID
to a supplied `ClientBatchCheckItem` if it does not have one.

Request context and condition context retain their dictionary values. The SDK
constructs the outer request and nested client tuples/checks, so missing required
constructor arguments and unexpected keys there raise constructor errors.
Nested generated-model fields, such as a tuple's `condition`, a ListUsers
`object`, and authorization-model definitions, are serialized directly when
provided as dictionaries; their contents are validated by the server. Supply
generated model instances in these fields if you also want their constructor
validation locally.

This support applies to dictionaries passed as request bodies. It does not
change the accepted constructor arguments of existing request model classes.
51 changes: 34 additions & 17 deletions openfga_sdk/client/client.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import asyncio
import uuid

from collections.abc import AsyncIterator
from collections.abc import AsyncIterator, Sequence
from typing import Any

from openfga_sdk.api.open_fga_api import OpenFgaApi
Expand Down Expand Up @@ -37,6 +37,7 @@
construct_write_single_response,
)
from openfga_sdk.client.models.write_transaction_opts import WriteTransactionOpts
from openfga_sdk.client.request import parse_request
from openfga_sdk.constants import (
CLIENT_BULK_REQUEST_ID_HEADER,
CLIENT_MAX_BATCH_SIZE,
Expand Down Expand Up @@ -273,7 +274,7 @@ async def list_stores(

async def create_store(
self,
body: CreateStoreRequest,
body: CreateStoreRequest | dict[str, Any],
options: dict[str, int | str | dict[str, int | str]] | None = None,
):
"""
Expand All @@ -283,6 +284,7 @@ async def create_store(
: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
"""
body = parse_request(body, CreateStoreRequest)
kwargs = options_to_kwargs(options)
api_response = await self._api.create_store(body, **kwargs)
return api_response
Expand Down Expand Up @@ -341,7 +343,7 @@ async def read_authorization_models(

async def write_authorization_model(
self,
body: WriteAuthorizationModelRequest,
body: WriteAuthorizationModelRequest | dict[str, Any],
options: dict[str, int | str | dict[str, int | str]] | None = None,
):
"""
Expand All @@ -352,6 +354,7 @@ async def write_authorization_model(
: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
"""
body = parse_request(body, WriteAuthorizationModelRequest)
kwargs = options_to_kwargs(options)
api_response = await self._api.write_authorization_model(
body,
Expand Down Expand Up @@ -405,7 +408,7 @@ async def read_latest_authorization_model(

async def read_changes(
self,
body: ClientReadChangesRequest,
body: ClientReadChangesRequest | dict[str, Any],
options: dict[str, int | str | dict[str, int | str]] | None = None,
):
"""
Expand All @@ -418,6 +421,7 @@ async def read_changes(
: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
"""
body = parse_request(body, ClientReadChangesRequest)
kwargs = options_to_kwargs(options)

if body.type is not None:
Expand All @@ -433,7 +437,7 @@ async def read_changes(

async def read(
self,
body: ReadRequestTupleKey,
body: ReadRequestTupleKey | dict[str, Any] | None,
options: dict[str, int | str | dict[str, int | str]] | None = None,
):
"""
Expand All @@ -447,6 +451,7 @@ async def read(
:param retryParams.minWaitInMs(options) - Override the minimum wait before a retry is initiated
:param consistency(options) - The type of consistency preferred for the request
"""
body = parse_request(body, ReadRequestTupleKey)
page_size = None
continuation_token = None
if options:
Expand Down Expand Up @@ -576,7 +581,7 @@ async def _write_with_transaction(

async def write(
self,
body: ClientWriteRequest,
body: ClientWriteRequest | dict[str, Any],
options: dict[str, int | str | dict[str, int | str]] | None = None,
):
"""
Expand All @@ -587,6 +592,7 @@ async def write(
: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
"""
body = parse_request(body, ClientWriteRequest)
options = set_heading_if_not_set(options, CLIENT_METHOD_HEADER, "Write")
transaction = options_to_transaction_info(options)
if not transaction.disabled:
Expand All @@ -612,7 +618,7 @@ async def write(

async def write_tuples(
self,
body: list[ClientTuple],
body: Sequence[ClientTuple | dict[str, Any]],
options: dict[str, int | str | dict[str, int | str]] | None = None,
):
"""
Expand All @@ -623,13 +629,14 @@ async def write_tuples(
: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
"""
body = [parse_request(item, ClientTuple) for item in body]
options = set_heading_if_not_set(options, CLIENT_METHOD_HEADER, "WriteTuples")
result = await self.write(ClientWriteRequest(body, None), options)
return result

async def delete_tuples(
self,
body: list[ClientTuple],
body: Sequence[ClientTuple | dict[str, Any]],
options: dict[str, int | str | dict[str, int | str]] | None = None,
):
"""
Expand All @@ -640,6 +647,7 @@ async def delete_tuples(
: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
"""
body = [parse_request(item, ClientTuple) for item in body]
options = set_heading_if_not_set(options, CLIENT_METHOD_HEADER, "DeleteTuples")
result = await self.write(ClientWriteRequest(None, body), options)
return result
Expand All @@ -649,7 +657,7 @@ async def delete_tuples(
#######################
async def check(
self,
body: ClientCheckRequest,
body: ClientCheckRequest | dict[str, Any],
options: dict[str, int | str | dict[str, int | str]] | None = None,
):
"""
Expand All @@ -662,6 +670,7 @@ async def check(
:param retryParams.minWaitInMs(options) - Override the minimum wait before a retry is initiated
:param consistency(options) - The type of consistency preferred for the request
"""
body = parse_request(body, ClientCheckRequest)
kwargs = options_to_kwargs(options)

req_body = CheckRequest(
Expand Down Expand Up @@ -712,7 +721,7 @@ async def _single_client_batch_check(

async def client_batch_check(
self,
body: list[ClientCheckRequest],
body: Sequence[ClientCheckRequest | dict[str, Any]],
options: dict[str, int | str | dict[str, int | str]] | None = None,
):
"""
Expand All @@ -725,6 +734,7 @@ async def client_batch_check(
: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
"""
body = [parse_request(item, ClientCheckRequest) for item in body]
options = set_heading_if_not_set(options, CLIENT_METHOD_HEADER, "BatchCheck")
options = set_heading_if_not_set(
options, CLIENT_BULK_REQUEST_ID_HEADER, str(uuid.uuid4())
Expand Down Expand Up @@ -771,7 +781,7 @@ async def _single_batch_check(

async def batch_check(
self,
body: ClientBatchCheckRequest,
body: ClientBatchCheckRequest | dict[str, Any],
options: dict[str, int | str | dict[str, int | str]] | None = None,
):
"""
Expand All @@ -785,6 +795,7 @@ async def batch_check(
: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
"""
body = parse_request(body, ClientBatchCheckRequest)
options = set_heading_if_not_set(
options, CLIENT_BULK_REQUEST_ID_HEADER, str(uuid.uuid4())
)
Expand Down Expand Up @@ -868,7 +879,7 @@ async def coro(checks):

async def expand(
self,
body: ClientExpandRequest,
body: ClientExpandRequest | dict[str, Any],
options: dict[str, int | str | dict[str, int | str]] | None = None,
):
"""
Expand All @@ -881,6 +892,7 @@ async def expand(
:param retryParams.minWaitInMs(options) - Override the minimum wait before a retry is initiated
:param consistency(options) - The type of consistency preferred for the request
"""
body = parse_request(body, ClientExpandRequest)
kwargs = options_to_kwargs(options)

req_body = ExpandRequest(
Expand All @@ -900,7 +912,7 @@ async def expand(

async def list_objects(
self,
body: ClientListObjectsRequest,
body: ClientListObjectsRequest | dict[str, Any],
options: dict[str, int | str | dict[str, int | str]] | None = None,
):
"""
Expand All @@ -913,6 +925,7 @@ async def list_objects(
:param retryParams.minWaitInMs(options) - Override the minimum wait before a retry is initiated
:param consistency(options) - The type of consistency preferred for the request
"""
body = parse_request(body, ClientListObjectsRequest)
kwargs = options_to_kwargs(options)

req_body = ListObjectsRequest(
Expand All @@ -932,7 +945,7 @@ async def list_objects(

async def streamed_list_objects(
self,
body: ClientListObjectsRequest,
body: ClientListObjectsRequest | dict[str, Any],
options: dict[str, int | str | dict[str, int | str]] | None = None,
):
"""
Expand All @@ -946,6 +959,7 @@ async def streamed_list_objects(
:param retryParams.minWaitInMs(options) - Override the minimum wait before a retry is initiated
:param consistency(options) - The type of consistency preferred for the request11
"""
body = parse_request(body, ClientListObjectsRequest)
kwargs = options_to_kwargs(options)
kwargs["_streaming"] = True

Expand All @@ -971,7 +985,7 @@ async def streamed_list_objects(

async def list_relations(
self,
body: ClientListRelationsRequest,
body: ClientListRelationsRequest | dict[str, Any],
options: dict[str, int | str | dict[str, int | str]] | None = None,
):
"""
Expand All @@ -984,6 +998,7 @@ async def list_relations(
:param retryParams.minWaitInMs(options) - Override the minimum wait before a retry is initiated
:param consistency(options) - The type of consistency preferred for the request
"""
body = parse_request(body, ClientListRelationsRequest)
options = set_heading_if_not_set(options, CLIENT_METHOD_HEADER, "ListRelations")
options = set_heading_if_not_set(
options, CLIENT_BULK_REQUEST_ID_HEADER, str(uuid.uuid4())
Expand Down Expand Up @@ -1014,7 +1029,7 @@ async def list_relations(

async def list_users(
self,
body: ClientListUsersRequest,
body: ClientListUsersRequest | dict[str, Any],
options: dict[str, int | str | dict[str, int | str]] | None = None,
):
"""
Expand All @@ -1027,6 +1042,7 @@ async def list_users(
:param retryParams.minWaitInMs(options) - Override the minimum wait before a retry is initiated
:param consistency(options) - The type of consistency preferred for the request
"""
body = parse_request(body, ClientListUsersRequest)
kwargs = options_to_kwargs(options)

req_body = ListUsersRequest(
Expand Down Expand Up @@ -1068,7 +1084,7 @@ async def read_assertions(

async def write_assertions(
self,
body: list[ClientAssertion],
body: Sequence[ClientAssertion | dict[str, Any]],
options: dict[str, int | str | dict[str, int | str]] | None = None,
):
"""
Expand All @@ -1080,6 +1096,7 @@ async def write_assertions(
: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
"""
body = [parse_request(item, ClientAssertion) for item in body]
kwargs = options_to_kwargs(options)
authorization_model_id = self._get_authorization_model_id(options)

Expand Down
Loading