Skip to content

Commit 32b18f6

Browse files
committed
feat: support write conflict idempotency
1 parent d096fca commit 32b18f6

25 files changed

+1101
-798
lines changed

.openapi-generator/FILES

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,8 @@ openfga_sdk/client/models/list_relations_request.py
139139
openfga_sdk/client/models/list_users_request.py
140140
openfga_sdk/client/models/read_changes_request.py
141141
openfga_sdk/client/models/tuple.py
142+
openfga_sdk/client/models/write_conflict_opts.py
143+
openfga_sdk/client/models/write_options.py
142144
openfga_sdk/client/models/write_request.py
143145
openfga_sdk/client/models/write_response.py
144146
openfga_sdk/client/models/write_single_response.py

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ This is an autogenerated python SDK for OpenFGA. It provides a wrapper around th
1818
- [Installation](#installation)
1919
- [Getting Started](#getting-started)
2020
- [Initializing the API Client](#initializing-the-api-client)
21+
- [Custom Headers](#custom-headers)
2122
- [Get your Store ID](#get-your-store-id)
2223
- [Calling the API](#calling-the-api)
2324
- [Stores](#stores)

docs/OpenFgaApi.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1291,7 +1291,7 @@ No authorization required
12911291
12921292
Add or delete tuples from the store
12931293

1294-
The Write API will transactionally update the tuples for a certain store. Tuples and type definitions allow OpenFGA to determine whether a relationship exists between an object and an user. In the body, `writes` adds new tuples and `deletes` removes existing tuples. When deleting a tuple, any `condition` specified with it is ignored. The API is not idempotent: if, later on, you try to add the same tuple key (even if the `condition` is different), or if you try to delete a non-existing tuple, it will throw an error. The API will not allow you to write tuples such as `document:2021-budget#viewer@document:2021-budget#viewer`, because they are implicit. An `authorization_model_id` may be specified in the body. If it is, it will be used to assert that each written tuple (not deleted) is valid for the model specified. If it is not specified, the latest authorization model ID will be used. ## Example ### Adding relationships To add `user:anne` as a `writer` for `document:2021-budget`, call write API with the following ```json { \"writes\": { \"tuple_keys\": [ { \"user\": \"user:anne\", \"relation\": \"writer\", \"object\": \"document:2021-budget\" } ] }, \"authorization_model_id\": \"01G50QVV17PECNVAHX1GG4Y5NC\" } ``` ### Removing relationships To remove `user:bob` as a `reader` for `document:2021-budget`, call write API with the following ```json { \"deletes\": { \"tuple_keys\": [ { \"user\": \"user:bob\", \"relation\": \"reader\", \"object\": \"document:2021-budget\" } ] } } ```
1294+
The Write API will transactionally update the tuples for a certain store. Tuples and type definitions allow OpenFGA to determine whether a relationship exists between an object and an user. In the body, `writes` adds new tuples and `deletes` removes existing tuples. When deleting a tuple, any `condition` specified with it is ignored. The API is not idempotent by default: if, later on, you try to add the same tuple key (even if the `condition` is different), or if you try to delete a non-existing tuple, it will throw an error. To allow writes when an identical tuple already exists in the database, set `\"on_duplicate\": \"ignore\"` on the `writes` object. To allow deletes when a tuple was already removed from the database, set `\"on_missing\": \"ignore\"` on the `deletes` object. If a Write request contains both idempotent (ignore) and non-idempotent (error) operations, the most restrictive action (error) will take precedence. If a condition fails for a sub-request with an error flag, the entire transaction will be rolled back. This gives developers explicit control over the atomicity of the requests. The API will not allow you to write tuples such as `document:2021-budget#viewer@document:2021-budget#viewer`, because they are implicit. An `authorization_model_id` may be specified in the body. If it is, it will be used to assert that each written tuple (not deleted) is valid for the model specified. If it is not specified, the latest authorization model ID will be used. ## Example ### Adding relationships To add `user:anne` as a `writer` for `document:2021-budget`, call write API with the following ```json { \"writes\": { \"tuple_keys\": [ { \"user\": \"user:anne\", \"relation\": \"writer\", \"object\": \"document:2021-budget\" } ], \"on_duplicate\": \"ignore\" }, \"authorization_model_id\": \"01G50QVV17PECNVAHX1GG4Y5NC\" } ``` ### Removing relationships To remove `user:bob` as a `reader` for `document:2021-budget`, call write API with the following ```json { \"deletes\": { \"tuple_keys\": [ { \"user\": \"user:bob\", \"relation\": \"reader\", \"object\": \"document:2021-budget\" } ], \"on_missing\": \"ignore\" } } ```
12951295

12961296
### Example
12971297

docs/WriteRequestDeletes.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
Name | Type | Description | Notes
66
------------ | ------------- | ------------- | -------------
77
**tuple_keys** | [**list[TupleKeyWithoutCondition]**](TupleKeyWithoutCondition.md) | |
8+
**on_missing** | **str** | On 'error', the API returns an error when deleting a tuple that does not exist. On 'ignore', deletes of non-existent tuples are treated as no-ops. | [optional] [default to 'error']
89

910
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
1011

docs/WriteRequestWrites.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
Name | Type | Description | Notes
66
------------ | ------------- | ------------- | -------------
77
**tuple_keys** | [**list[TupleKey]**](TupleKey.md) | |
8+
**on_duplicate** | **str** | On 'error' ( or unspecified ), the API returns an error if an identical tuple already exists. On 'ignore', identical writes are treated as no-ops (matching on user, relation, object, and RelationshipCondition). | [optional] [default to 'error']
89

910
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
1011

example/example1/requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,4 @@ openfga-sdk >= 0.9.7
88
python-dateutil >= 2.9.0.post0
99
urllib3 >= 1.26.19, != 2.0.*, != 2.1.*, != 2.2.0, != 2.2.1, < 3
1010
yarl >= 1.20.1
11-
python-dotenv >= 1, <2
11+
python-dotenv >= 1, <2

openfga_sdk/__init__.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,6 @@
7171
from openfga_sdk.models.not_found_error_code import NotFoundErrorCode
7272
from openfga_sdk.models.null_value import NullValue
7373
from openfga_sdk.models.object_relation import ObjectRelation
74-
from openfga_sdk.models.on_duplicate_writes import OnDuplicateWrites
75-
from openfga_sdk.models.on_missing_deletes import OnMissingDeletes
7674
from openfga_sdk.models.path_unknown_error_message_response import (
7775
PathUnknownErrorMessageResponse,
7876
)
@@ -202,8 +200,6 @@
202200
"NotFoundErrorCode",
203201
"NullValue",
204202
"ObjectRelation",
205-
"OnDuplicateWrites",
206-
"OnMissingDeletes",
207203
"PathUnknownErrorMessageResponse",
208204
"ReadAssertionsResponse",
209205
"ReadAuthorizationModelResponse",
@@ -253,4 +249,4 @@
253249
"TelemetryConfigurationType",
254250
"TelemetryMetricConfiguration",
255251
"TelemetryMetricsConfiguration",
256-
]
252+
]

openfga_sdk/api/open_fga_api.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2667,7 +2667,7 @@ async def streamed_list_objects_with_http_info(self, body, **kwargs):
26672667
async def write(self, body, **kwargs):
26682668
"""Add or delete tuples from the store
26692669
2670-
The Write API will transactionally update the tuples for a certain store. Tuples and type definitions allow OpenFGA to determine whether a relationship exists between an object and an user. In the body, `writes` adds new tuples and `deletes` removes existing tuples. When deleting a tuple, any `condition` specified with it is ignored. The API is not idempotent: if, later on, you try to add the same tuple key (even if the `condition` is different), or if you try to delete a non-existing tuple, it will throw an error. The API will not allow you to write tuples such as `document:2021-budget#viewer@document:2021-budget#viewer`, because they are implicit. An `authorization_model_id` may be specified in the body. If it is, it will be used to assert that each written tuple (not deleted) is valid for the model specified. If it is not specified, the latest authorization model ID will be used. ## Example ### Adding relationships To add `user:anne` as a `writer` for `document:2021-budget`, call write API with the following ```json { \"writes\": { \"tuple_keys\": [ { \"user\": \"user:anne\", \"relation\": \"writer\", \"object\": \"document:2021-budget\" } ] }, \"authorization_model_id\": \"01G50QVV17PECNVAHX1GG4Y5NC\" } ``` ### Removing relationships To remove `user:bob` as a `reader` for `document:2021-budget`, call write API with the following ```json { \"deletes\": { \"tuple_keys\": [ { \"user\": \"user:bob\", \"relation\": \"reader\", \"object\": \"document:2021-budget\" } ] } } ```
2670+
The Write API will transactionally update the tuples for a certain store. Tuples and type definitions allow OpenFGA to determine whether a relationship exists between an object and an user. In the body, `writes` adds new tuples and `deletes` removes existing tuples. When deleting a tuple, any `condition` specified with it is ignored. The API is not idempotent by default: if, later on, you try to add the same tuple key (even if the `condition` is different), or if you try to delete a non-existing tuple, it will throw an error. To allow writes when an identical tuple already exists in the database, set `\"on_duplicate\": \"ignore\"` on the `writes` object. To allow deletes when a tuple was already removed from the database, set `\"on_missing\": \"ignore\"` on the `deletes` object. If a Write request contains both idempotent (ignore) and non-idempotent (error) operations, the most restrictive action (error) will take precedence. If a condition fails for a sub-request with an error flag, the entire transaction will be rolled back. This gives developers explicit control over the atomicity of the requests. The API will not allow you to write tuples such as `document:2021-budget#viewer@document:2021-budget#viewer`, because they are implicit. An `authorization_model_id` may be specified in the body. If it is, it will be used to assert that each written tuple (not deleted) is valid for the model specified. If it is not specified, the latest authorization model ID will be used. ## Example ### Adding relationships To add `user:anne` as a `writer` for `document:2021-budget`, call write API with the following ```json { \"writes\": { \"tuple_keys\": [ { \"user\": \"user:anne\", \"relation\": \"writer\", \"object\": \"document:2021-budget\" } ], \"on_duplicate\": \"ignore\" }, \"authorization_model_id\": \"01G50QVV17PECNVAHX1GG4Y5NC\" } ``` ### Removing relationships To remove `user:bob` as a `reader` for `document:2021-budget`, call write API with the following ```json { \"deletes\": { \"tuple_keys\": [ { \"user\": \"user:bob\", \"relation\": \"reader\", \"object\": \"document:2021-budget\" } ], \"on_missing\": \"ignore\" } } ```
26712671
26722672
>>> thread = await api.write(body)
26732673
@@ -2694,7 +2694,7 @@ async def write(self, body, **kwargs):
26942694
async def write_with_http_info(self, body, **kwargs):
26952695
"""Add or delete tuples from the store
26962696
2697-
The Write API will transactionally update the tuples for a certain store. Tuples and type definitions allow OpenFGA to determine whether a relationship exists between an object and an user. In the body, `writes` adds new tuples and `deletes` removes existing tuples. When deleting a tuple, any `condition` specified with it is ignored. The API is not idempotent: if, later on, you try to add the same tuple key (even if the `condition` is different), or if you try to delete a non-existing tuple, it will throw an error. The API will not allow you to write tuples such as `document:2021-budget#viewer@document:2021-budget#viewer`, because they are implicit. An `authorization_model_id` may be specified in the body. If it is, it will be used to assert that each written tuple (not deleted) is valid for the model specified. If it is not specified, the latest authorization model ID will be used. ## Example ### Adding relationships To add `user:anne` as a `writer` for `document:2021-budget`, call write API with the following ```json { \"writes\": { \"tuple_keys\": [ { \"user\": \"user:anne\", \"relation\": \"writer\", \"object\": \"document:2021-budget\" } ] }, \"authorization_model_id\": \"01G50QVV17PECNVAHX1GG4Y5NC\" } ``` ### Removing relationships To remove `user:bob` as a `reader` for `document:2021-budget`, call write API with the following ```json { \"deletes\": { \"tuple_keys\": [ { \"user\": \"user:bob\", \"relation\": \"reader\", \"object\": \"document:2021-budget\" } ] } } ```
2697+
The Write API will transactionally update the tuples for a certain store. Tuples and type definitions allow OpenFGA to determine whether a relationship exists between an object and an user. In the body, `writes` adds new tuples and `deletes` removes existing tuples. When deleting a tuple, any `condition` specified with it is ignored. The API is not idempotent by default: if, later on, you try to add the same tuple key (even if the `condition` is different), or if you try to delete a non-existing tuple, it will throw an error. To allow writes when an identical tuple already exists in the database, set `\"on_duplicate\": \"ignore\"` on the `writes` object. To allow deletes when a tuple was already removed from the database, set `\"on_missing\": \"ignore\"` on the `deletes` object. If a Write request contains both idempotent (ignore) and non-idempotent (error) operations, the most restrictive action (error) will take precedence. If a condition fails for a sub-request with an error flag, the entire transaction will be rolled back. This gives developers explicit control over the atomicity of the requests. The API will not allow you to write tuples such as `document:2021-budget#viewer@document:2021-budget#viewer`, because they are implicit. An `authorization_model_id` may be specified in the body. If it is, it will be used to assert that each written tuple (not deleted) is valid for the model specified. If it is not specified, the latest authorization model ID will be used. ## Example ### Adding relationships To add `user:anne` as a `writer` for `document:2021-budget`, call write API with the following ```json { \"writes\": { \"tuple_keys\": [ { \"user\": \"user:anne\", \"relation\": \"writer\", \"object\": \"document:2021-budget\" } ], \"on_duplicate\": \"ignore\" }, \"authorization_model_id\": \"01G50QVV17PECNVAHX1GG4Y5NC\" } ``` ### Removing relationships To remove `user:bob` as a `reader` for `document:2021-budget`, call write API with the following ```json { \"deletes\": { \"tuple_keys\": [ { \"user\": \"user:bob\", \"relation\": \"reader\", \"object\": \"document:2021-budget\" } ], \"on_missing\": \"ignore\" } } ```
26982698
26992699
>>> thread = api.write_with_http_info(body)
27002700

openfga_sdk/client/client.py

Lines changed: 27 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,12 @@
3939
from openfga_sdk.client.models.list_users_request import ClientListUsersRequest
4040
from openfga_sdk.client.models.read_changes_request import ClientReadChangesRequest
4141
from openfga_sdk.client.models.tuple import ClientTuple, convert_tuple_keys
42+
from openfga_sdk.client.models.write_conflict_opts import (
43+
ClientWriteRequestOnDuplicateWrites,
44+
ClientWriteRequestOnMissingDeletes,
45+
ConflictOptions,
46+
)
47+
from openfga_sdk.client.models.write_options import ClientWriteOptions
4248
from openfga_sdk.client.models.write_request import ClientWriteRequest
4349
from openfga_sdk.client.models.write_response import ClientWriteResponse
4450
from openfga_sdk.client.models.write_single_response import (
@@ -133,22 +139,6 @@ def options_to_kwargs(
133139
return kwargs
134140

135141

136-
def options_to_conflict(
137-
options: dict[str, int | str | dict[str, int | str]] | None = None,
138-
) -> tuple[str | None, str | None]:
139-
"""
140-
Extract conflict options from options dict
141-
"""
142-
if options is not None and options.get("conflict"):
143-
conflict = options["conflict"]
144-
if isinstance(conflict, dict):
145-
return (
146-
conflict.get("on_duplicate_writes"),
147-
conflict.get("on_missing_deletes"),
148-
)
149-
return (None, None)
150-
151-
152142
def options_to_transaction_info(
153143
options: dict[str, int | str | dict[str, int | str]] | None = None,
154144
):
@@ -160,6 +150,17 @@ def options_to_transaction_info(
160150
return WriteTransactionOpts()
161151

162152

153+
def options_to_conflict_info(
154+
options: dict[str, int | str | dict[str, int | str]] | None = None,
155+
):
156+
"""
157+
Return the conflict info
158+
"""
159+
if options is not None and options.get("conflict"):
160+
return options["conflict"]
161+
return None
162+
163+
163164
def _check_errored(response: ClientBatchCheckClientResponse):
164165
"""
165166
Helper function to return whether the response is errored
@@ -546,22 +547,27 @@ async def _write_with_transaction(
546547
Write or deletes tuples
547548
"""
548549
kwargs = options_to_kwargs(options)
550+
conflict_options = options_to_conflict_info(options)
551+
552+
# Set conflict options on the body if provided
553+
if conflict_options:
554+
if conflict_options.on_duplicate_writes:
555+
body.on_duplicate = conflict_options.on_duplicate_writes.value
556+
if conflict_options.on_missing_deletes:
557+
body.on_missing = conflict_options.on_missing_deletes.value
558+
549559
writes_tuple_keys = None
550560
deletes_tuple_keys = None
551561
if body.writes_tuple_keys:
552562
writes_tuple_keys = body.writes_tuple_keys
553563
if body.deletes_tuple_keys:
554564
deletes_tuple_keys = body.deletes_tuple_keys
555565

556-
on_duplicate_writes, on_missing_deletes = options_to_conflict(options)
557-
558566
await self._api.write(
559567
WriteRequest(
560568
writes=writes_tuple_keys,
561569
deletes=deletes_tuple_keys,
562570
authorization_model_id=self._get_authorization_model_id(options),
563-
on_duplicate_writes=on_duplicate_writes,
564-
on_missing_deletes=on_missing_deletes,
565571
),
566572
**kwargs,
567573
)
@@ -1103,4 +1109,4 @@ def map_to_assertion(client_assertion: ClientAssertion):
11031109
api_response = await self._api.write_assertions(
11041110
authorization_model_id, api_request_body, **kwargs
11051111
)
1106-
return api_response
1112+
return api_response

openfga_sdk/client/models/__init__.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@
2626
from openfga_sdk.client.models.list_relations_request import ClientListRelationsRequest
2727
from openfga_sdk.client.models.read_changes_request import ClientReadChangesRequest
2828
from openfga_sdk.client.models.tuple import ClientTuple
29+
from openfga_sdk.client.models.write_conflict_opts import (
30+
ClientWriteRequestOnDuplicateWrites,
31+
ClientWriteRequestOnMissingDeletes,
32+
ConflictOptions,
33+
)
34+
from openfga_sdk.client.models.write_options import ClientWriteOptions
2935
from openfga_sdk.client.models.write_request import ClientWriteRequest
3036
from openfga_sdk.client.models.write_response import ClientWriteResponse
3137
from openfga_sdk.client.models.write_transaction_opts import WriteTransactionOpts
@@ -47,4 +53,8 @@
4753
"ClientWriteRequest",
4854
"ClientWriteResponse",
4955
"WriteTransactionOpts",
56+
"ClientWriteRequestOnDuplicateWrites",
57+
"ClientWriteRequestOnMissingDeletes",
58+
"ConflictOptions",
59+
"ClientWriteOptions",
5060
]

0 commit comments

Comments
 (0)