Skip to content
Merged
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
# Changelog

## [Unreleased](https://github.com/openfga/python-sdk/compare/v0.9.7...HEAD)
- feat: add support for conflict options for Write operations: (#235)
The client now supports setting `ConflictOptions` on `ClientWriteOptions` to control behavior when writing duplicate tuples or deleting non-existent tuples. This feature requires OpenFGA server [v1.10.0](https://github.com/openfga/openfga/releases/tag/v1.10.0) or later.
See [Conflict Options for Write Operations](./README.md#conflict-options-for-write-operations) for more.
- `on_duplicate` for handling duplicate tuple writes (ERROR or IGNORE)
- `on_missing` for handling deletes of non-existent tuples (ERROR or IGNORE)
- docs: added documentation for write conflict options in README

### [0.9.7](https://github.com/openfga/python-sdk/compare/v0.9.6...0.9.7) (2025-10-06)

Expand Down
43 changes: 43 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -748,6 +748,49 @@ body = ClientWriteRequest(
response = await fga_client.write(body, options)
```

###### Conflict Options for Write Operations

OpenFGA v1.10.0+ supports conflict options for write operations to handle duplicate writes and missing deletes gracefully.

**Example: Ignore duplicate writes and missing deletes**

```python
# from openfga_sdk import OpenFgaClient
# from openfga_sdk.client.models import ClientTuple, ClientWriteRequest
# from openfga_sdk.client.models.write_conflict_opts import (
# ClientWriteRequestOnDuplicateWrites,
# ClientWriteRequestOnMissingDeletes,
# ConflictOptions,
# )

options = {
"authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1",
"conflict": ConflictOptions(
on_duplicate_writes=ClientWriteRequestOnDuplicateWrites.IGNORE, # Available options: ERROR, IGNORE
on_missing_deletes=ClientWriteRequestOnMissingDeletes.IGNORE, # Available options: ERROR, IGNORE
)
}

body = ClientWriteRequest(
writes=[
ClientTuple(
user="user:81684243-9356-4421-8fbf-a4f8d36aa31b",
relation="viewer",
object="document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a",
),
],
deletes=[
ClientTuple(
user="user:81684243-9356-4421-8fbf-a4f8d36aa31b",
relation="writer",
object="document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a",
),
],
)

response = await fga_client.write(body, options)
```

#### Relationship Queries

##### Check
Expand Down
2 changes: 1 addition & 1 deletion docs/OpenFgaApi.md
Original file line number Diff line number Diff line change
Expand Up @@ -1291,7 +1291,7 @@ No authorization required

Add or delete tuples from the store

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\" } ] } } ```
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\" } } ```

### Example

Expand Down
1 change: 1 addition & 0 deletions docs/WriteRequestDeletes.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**tuple_keys** | [**list[TupleKeyWithoutCondition]**](TupleKeyWithoutCondition.md) | |
**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']

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

Expand Down
1 change: 1 addition & 0 deletions docs/WriteRequestWrites.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**tuple_keys** | [**list[TupleKey]**](TupleKey.md) | |
**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']

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

Expand Down
4 changes: 2 additions & 2 deletions openfga_sdk/api/open_fga_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -2667,7 +2667,7 @@ async def streamed_list_objects_with_http_info(self, body, **kwargs):
async def write(self, body, **kwargs):
"""Add or delete tuples from the store

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\" } ] } } ```
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\" } } ```

>>> thread = await api.write(body)

Expand All @@ -2694,7 +2694,7 @@ async def write(self, body, **kwargs):
async def write_with_http_info(self, body, **kwargs):
"""Add or delete tuples from the store

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\" } ] } } ```
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\" } } ```

>>> thread = api.write_with_http_info(body)

Expand Down
30 changes: 26 additions & 4 deletions openfga_sdk/client/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,17 @@ def options_to_transaction_info(
return WriteTransactionOpts()


def options_to_conflict_info(
options: dict[str, int | str | dict[str, int | str]] | None = None,
):
"""
Return the conflict info
"""
if options is not None and options.get("conflict"):
return options["conflict"]
return None


def _check_errored(response: ClientBatchCheckClientResponse):
"""
Helper function to return whether the response is errored
Expand Down Expand Up @@ -520,12 +531,23 @@ async def _write_with_transaction(
Write or deletes tuples
"""
kwargs = options_to_kwargs(options)
conflict_options = options_to_conflict_info(options)

# Extract conflict options to pass to the tuple key methods
on_duplicate = None
on_missing = None
if conflict_options:
if conflict_options.on_duplicate_writes:
on_duplicate = conflict_options.on_duplicate_writes.value
if conflict_options.on_missing_deletes:
on_missing = conflict_options.on_missing_deletes.value

writes_tuple_keys = None
deletes_tuple_keys = None
if body.writes_tuple_keys:
writes_tuple_keys = body.writes_tuple_keys
if body.deletes_tuple_keys:
deletes_tuple_keys = body.deletes_tuple_keys
if body.writes:
writes_tuple_keys = body.get_writes_tuple_keys(on_duplicate=on_duplicate)
if body.deletes:
deletes_tuple_keys = body.get_deletes_tuple_keys(on_missing=on_missing)

await self._api.write(
WriteRequest(
Expand Down
10 changes: 10 additions & 0 deletions openfga_sdk/client/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@
from openfga_sdk.client.models.list_relations_request import ClientListRelationsRequest
from openfga_sdk.client.models.read_changes_request import ClientReadChangesRequest
from openfga_sdk.client.models.tuple import ClientTuple
from openfga_sdk.client.models.write_conflict_opts import (
ClientWriteRequestOnDuplicateWrites,
ClientWriteRequestOnMissingDeletes,
ConflictOptions,
)
from openfga_sdk.client.models.write_options import ClientWriteOptions
from openfga_sdk.client.models.write_request import ClientWriteRequest
from openfga_sdk.client.models.write_response import ClientWriteResponse
from openfga_sdk.client.models.write_transaction_opts import WriteTransactionOpts
Expand All @@ -35,4 +41,8 @@
"ClientWriteRequest",
"ClientWriteResponse",
"WriteTransactionOpts",
"ClientWriteRequestOnDuplicateWrites",
"ClientWriteRequestOnMissingDeletes",
"ConflictOptions",
"ClientWriteOptions",
]
71 changes: 71 additions & 0 deletions openfga_sdk/client/models/write_conflict_opts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
"""
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 enum import Enum


class ClientWriteRequestOnDuplicateWrites(str, Enum):
ERROR = "error"
IGNORE = "ignore"


class ClientWriteRequestOnMissingDeletes(str, Enum):
ERROR = "error"
IGNORE = "ignore"


class ConflictOptions:
"""
OpenFGA client write conflict options
"""

def __init__(
self,
on_duplicate_writes: ClientWriteRequestOnDuplicateWrites | None = None,
on_missing_deletes: ClientWriteRequestOnMissingDeletes | None = None,
) -> None:
self._on_duplicate_writes = on_duplicate_writes
self._on_missing_deletes = on_missing_deletes

@property
def on_duplicate_writes(self) -> ClientWriteRequestOnDuplicateWrites | None:
"""
Return on_duplicate_writes
"""
return self._on_duplicate_writes

@on_duplicate_writes.setter
def on_duplicate_writes(
self,
value: ClientWriteRequestOnDuplicateWrites | None,
) -> None:
"""
Set on_duplicate_writes
"""
self._on_duplicate_writes = value

@property
def on_missing_deletes(self) -> ClientWriteRequestOnMissingDeletes | None:
"""
Return on_missing_deletes
"""
return self._on_missing_deletes

@on_missing_deletes.setter
def on_missing_deletes(
self,
value: ClientWriteRequestOnMissingDeletes | None,
) -> None:
"""
Set on_missing_deletes
"""
self._on_missing_deletes = value
Loading