diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 61ca8d55c..5ffee1f58 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -136,7 +136,7 @@ jobs: - name: Run All Tests run: make test-client-python env: - OPEN_API_REF: c0b62b28b14d0d164d37a1f6bf19dc9d39e5769b + OPEN_API_REF: 0ac19aac54f21f3c78970126b84b4c69c6e3b9a2 - name: Check for SDK changes run: | diff --git a/Makefile b/Makefile index 2bdb907a6..ed80677ed 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ # Main config OPENFGA_DOCKER_TAG = v1 -OPEN_API_REF ?= e53c69cc55317404d02a6d8e418d626268f28a59 +OPEN_API_REF ?= 0ac19aac54f21f3c78970126b84b4c69c6e3b9a2 OPEN_API_URL = https://raw.githubusercontent.com/openfga/api/${OPEN_API_REF}/docs/openapiv2/apidocs.swagger.json OPENAPI_GENERATOR_CLI_DOCKER_TAG ?= v6.4.0 NODE_DOCKER_TAG = 20-alpine diff --git a/config/clients/python/CHANGELOG.md.mustache b/config/clients/python/CHANGELOG.md.mustache index af05b7739..dc7d99495 100644 --- a/config/clients/python/CHANGELOG.md.mustache +++ b/config/clients/python/CHANGELOG.md.mustache @@ -1,6 +1,12 @@ # Changelog ## [Unreleased](https://{{gitHost}}/{{gitUserId}}/{{gitRepoId}}/compare/v{{packageVersion}}...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 ### [{{packageVersion}}](https://{{gitHost}}/{{gitUserId}}/{{gitRepoId}}/compare/v0.9.6...{{packageVersion}}) (2025-10-06) diff --git a/config/clients/python/config.overrides.json b/config/clients/python/config.overrides.json index 8311a0ca0..8b80511d8 100644 --- a/config/clients/python/config.overrides.json +++ b/config/clients/python/config.overrides.json @@ -153,6 +153,14 @@ "destinationFilename": "openfga_sdk/client/models/write_transaction_opts.py", "templateType": "SupportingFiles" }, + "src/client/models/write_conflict_opts.py.mustache": { + "destinationFilename": "openfga_sdk/client/models/write_conflict_opts.py", + "templateType": "SupportingFiles" + }, + "src/client/models/write_options.py.mustache": { + "destinationFilename": "openfga_sdk/client/models/write_options.py", + "templateType": "SupportingFiles" + }, "/src/models/__init__.py.mustache": { "destinationFilename": "openfga_sdk/models/__init__.py", "templateType": "SupportingFiles" diff --git a/config/clients/python/template/README_calling_api.mustache b/config/clients/python/template/README_calling_api.mustache index 83ddf5e3d..e7348c67e 100644 --- a/config/clients/python/template/README_calling_api.mustache +++ b/config/clients/python/template/README_calling_api.mustache @@ -452,6 +452,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 diff --git a/config/clients/python/template/model.mustache b/config/clients/python/template/model.mustache index 734690595..6eec067f9 100644 --- a/config/clients/python/template/model.mustache +++ b/config/clients/python/template/model.mustache @@ -117,19 +117,19 @@ class {{classname}}: {{#isArray}} if (self.local_vars_configuration.client_side_validation and not set({{{name}}}).issubset(set(allowed_values))): + invalid_values = ", ".join(map(str, set({{{name}}}) - set(allowed_values))) + valid_values = ", ".join(map(str, allowed_values)) raise ValueError( - "Invalid values for `{{{name}}}` [{0}], must be a subset of [{1}]" - .format(", ".join(map(str, set({{{name}}}) - set(allowed_values))), - ", ".join(map(str, allowed_values))) + f"Invalid values for `{{{name}}}` [{invalid_values}], must be a subset of [{valid_values}]" ) {{/isArray}} {{#isMap}} if (self.local_vars_configuration.client_side_validation and not set({{{name}}}.keys()).issubset(set(allowed_values))): + invalid_keys = ", ".join(map(str, set({{{name}}}.keys()) - set(allowed_values))) + valid_values = ", ".join(map(str, allowed_values)) raise ValueError( - "Invalid keys in `{{{name}}}` [{0}], must be a subset of [{1}]" - .format(", ".join(map(str, set({{{name}}}.keys()) - set(allowed_values))), - ", ".join(map(str, allowed_values))) + f"Invalid keys in `{{{name}}}` [{invalid_keys}], must be a subset of [{valid_values}]" ) {{/isMap}} {{/isContainer}} @@ -137,8 +137,7 @@ class {{classname}}: allowed_values = [{{#isNullable}}None,{{/isNullable}}{{#allowableValues}}{{#values}}{{#isString}}"{{/isString}}{{{this}}}{{#isString}}"{{/isString}}{{^-last}}, {{/-last}}{{/values}}{{/allowableValues}}] if self.local_vars_configuration.client_side_validation and {{{name}}} not in allowed_values: raise ValueError( - "Invalid value for `{{{name}}}` ({0}), must be one of {1}" - .format({{{name}}}, allowed_values) + f"Invalid value for `{{name}}` ({{{{{name}}}}}), must be one of {allowed_values}" ) {{/isContainer}} {{/isEnum}} diff --git a/config/clients/python/template/src/client/client.py.mustache b/config/clients/python/template/src/client/client.py.mustache index d7551d929..e7a6e15a2 100644 --- a/config/clients/python/template/src/client/client.py.mustache +++ b/config/clients/python/template/src/client/client.py.mustache @@ -112,6 +112,14 @@ def options_to_transaction_info(options: dict[str, int | str | dict[str, int | s return options["transaction"] 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 @@ -448,17 +456,32 @@ class OpenFgaClient: return batch_write_responses - {{#asyncio}}async {{/asyncio}}def _write_with_transaction(self, body: ClientWriteRequest, options: dict[str, int | str | dict[str, int | str]] | None = None): + {{#asyncio}}async {{/asyncio}}def _write_with_transaction( + self, + body: ClientWriteRequest, + options: dict[str, int | str | dict[str, int | str]] | None = None, + ): """ 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) {{#asyncio}}await {{/asyncio}}self._api.write( WriteRequest( @@ -471,10 +494,14 @@ class OpenFgaClient: # any error will result in exception being thrown and not reached below code writes_response = None if body.writes: - writes_response = [construct_write_single_response(i, True, None) for i in body.writes] + writes_response = [ + construct_write_single_response(i, True, None) for i in body.writes + ] deletes_response = None if body.deletes: - deletes_response = [construct_write_single_response(i, True, None) for i in body.deletes] + deletes_response = [ + construct_write_single_response(i, True, None) for i in body.deletes + ] return ClientWriteResponse(writes=writes_response, deletes=deletes_response) {{#asyncio}}async {{/asyncio}}def write(self, body: ClientWriteRequest, options: dict[str, int | str | dict[str, int | str]] | None = None): @@ -945,4 +972,4 @@ class OpenFgaClient: api_request_body = WriteAssertionsRequest([map_to_assertion(client_assertion) for client_assertion in body]) api_response = {{#asyncio}}await {{/asyncio}}self._api.write_assertions(authorization_model_id, api_request_body, **kwargs) - return api_response + return api_response \ No newline at end of file diff --git a/config/clients/python/template/src/client/models/__init__.py.mustache b/config/clients/python/template/src/client/models/__init__.py.mustache index 7edc3e503..7ece718f3 100644 --- a/config/clients/python/template/src/client/models/__init__.py.mustache +++ b/config/clients/python/template/src/client/models/__init__.py.mustache @@ -15,6 +15,12 @@ from {{packageName}}.client.models.tuple import ClientTuple from {{packageName}}.client.models.write_request import ClientWriteRequest from {{packageName}}.client.models.write_response import ClientWriteResponse from {{packageName}}.client.models.write_transaction_opts import WriteTransactionOpts +from {{packageName}}.client.models.write_conflict_opts import ( + ClientWriteRequestOnDuplicateWrites, + ClientWriteRequestOnMissingDeletes, + ConflictOptions, +) +from {{packageName}}.client.models.write_options import ClientWriteOptions __all__ = [ "ClientAssertion", @@ -32,4 +38,8 @@ __all__ = [ "ClientWriteRequest", "ClientWriteResponse", "WriteTransactionOpts", -] + "ClientWriteRequestOnDuplicateWrites", + "ClientWriteRequestOnMissingDeletes", + "ConflictOptions", + "ClientWriteOptions", +] \ No newline at end of file diff --git a/config/clients/python/template/src/client/models/write_conflict_opts.py.mustache b/config/clients/python/template/src/client/models/write_conflict_opts.py.mustache new file mode 100644 index 000000000..20b32734e --- /dev/null +++ b/config/clients/python/template/src/client/models/write_conflict_opts.py.mustache @@ -0,0 +1,61 @@ +{{>partial_header}} + +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 \ No newline at end of file diff --git a/config/clients/python/template/src/client/models/write_options.py.mustache b/config/clients/python/template/src/client/models/write_options.py.mustache new file mode 100644 index 000000000..06cf9378c --- /dev/null +++ b/config/clients/python/template/src/client/models/write_options.py.mustache @@ -0,0 +1,71 @@ +{{>partial_header}} + +from {{packageName}}.client.models.write_conflict_opts import ConflictOptions +from {{packageName}}.client.models.write_transaction_opts import WriteTransactionOpts + + +class ClientWriteOptions: + """ + OpenFGA client write options + """ + + def __init__( + self, + authorization_model_id: str | None = None, + transaction: WriteTransactionOpts | None = None, + conflict: ConflictOptions | None = None, + ) -> None: + self._authorization_model_id = authorization_model_id + self._transaction = transaction + self._conflict = conflict + + @property + def authorization_model_id(self) -> str | None: + """ + Return authorization_model_id + """ + return self._authorization_model_id + + @authorization_model_id.setter + def authorization_model_id( + self, + value: str | None, + ) -> None: + """ + Set authorization_model_id + """ + self._authorization_model_id = value + + @property + def transaction(self) -> WriteTransactionOpts | None: + """ + Return transaction + """ + return self._transaction + + @transaction.setter + def transaction( + self, + value: WriteTransactionOpts | None, + ) -> None: + """ + Set transaction + """ + self._transaction = value + + @property + def conflict(self) -> ConflictOptions | None: + """ + Return conflict + """ + return self._conflict + + @conflict.setter + def conflict( + self, + value: ConflictOptions | None, + ) -> None: + """ + Set conflict + """ + self._conflict = value \ No newline at end of file diff --git a/config/clients/python/template/src/client/models/write_request.py.mustache b/config/clients/python/template/src/client/models/write_request.py.mustache index bfba1e970..cc4311f2e 100644 --- a/config/clients/python/template/src/client/models/write_request.py.mustache +++ b/config/clients/python/template/src/client/models/write_request.py.mustache @@ -49,7 +49,28 @@ class ClientWriteRequest: @property def writes_tuple_keys(self) -> WriteRequestWrites | None: """ - Return the writes as tuple keys + Return the writes as tuple keys (backward compatibility property) + """ + return self.get_writes_tuple_keys() + + @property + def deletes_tuple_keys(self) -> WriteRequestDeletes | None: + """ + Return the deletes as tuple keys (backward compatibility property) + """ + return self.get_deletes_tuple_keys() + + def get_writes_tuple_keys( + self, on_duplicate: str | None = None + ) -> WriteRequestWrites | None: + """ + Return the writes as tuple keys with optional conflict handling + + Args: + on_duplicate: Optional conflict resolution strategy for duplicate writes + + Returns: + WriteRequestWrites object with tuple keys and optional on_duplicate setting """ if self._writes is None: return None @@ -59,12 +80,21 @@ class ClientWriteRequest: if keys is None: return None + if on_duplicate is not None: + return WriteRequestWrites(tuple_keys=keys, on_duplicate=on_duplicate) return WriteRequestWrites(tuple_keys=keys) - @property - def deletes_tuple_keys(self) -> WriteRequestDeletes | None: + def get_deletes_tuple_keys( + self, on_missing: str | None = None + ) -> WriteRequestDeletes | None: """ - Return the delete as tuple keys + Return the deletes as tuple keys with optional conflict handling + + Args: + on_missing: Optional conflict resolution strategy for missing deletes + + Returns: + WriteRequestDeletes object with tuple keys and optional on_missing setting """ if self._deletes is None: return None @@ -74,4 +104,6 @@ class ClientWriteRequest: if keys is None: return None - return WriteRequestDeletes(tuple_keys=keys) + if on_missing is not None: + return WriteRequestDeletes(tuple_keys=keys, on_missing=on_missing) + return WriteRequestDeletes(tuple_keys=keys) \ No newline at end of file diff --git a/config/clients/python/template/src/sync/client/client.py.mustache b/config/clients/python/template/src/sync/client/client.py.mustache index cc285cf07..a9e170a0c 100644 --- a/config/clients/python/template/src/sync/client/client.py.mustache +++ b/config/clients/python/template/src/sync/client/client.py.mustache @@ -112,6 +112,14 @@ def options_to_transaction_info(options: dict[str, int | str | dict[str, int | s return options["transaction"] 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 @@ -433,17 +441,32 @@ class OpenFgaClient: return batch_write_responses - def _write_with_transaction(self, body: ClientWriteRequest, options: dict[str, int | str | dict[str, int | str]] | None = None): + def _write_with_transaction( + self, + body: ClientWriteRequest, + options: dict[str, int | str | dict[str, int | str]] | None = None, + ): """ 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) self._api.write( WriteRequest( @@ -456,10 +479,14 @@ class OpenFgaClient: # any error will result in exception being thrown and not reached below code writes_response = None if body.writes: - writes_response = [construct_write_single_response(i, True, None) for i in body.writes] + writes_response = [ + construct_write_single_response(i, True, None) for i in body.writes + ] deletes_response = None if body.deletes: - deletes_response = [construct_write_single_response(i, True, None) for i in body.deletes] + deletes_response = [ + construct_write_single_response(i, True, None) for i in body.deletes + ] return ClientWriteResponse(writes=writes_response, deletes=deletes_response) def write(self, body: ClientWriteRequest, options: dict[str, int | str | dict[str, int | str]] | None = None): @@ -919,4 +946,4 @@ class OpenFgaClient: api_request_body = WriteAssertionsRequest([map_to_assertion(client_assertion) for client_assertion in body]) api_response = self._api.write_assertions(authorization_model_id, api_request_body, **kwargs) - return api_response + return api_response \ No newline at end of file diff --git a/config/clients/python/template/test/api_test.py.mustache b/config/clients/python/template/test/api_test.py.mustache index a7d424552..efe6dec0f 100644 --- a/config/clients/python/template/test/api_test.py.mustache +++ b/config/clients/python/template/test/api_test.py.mustache @@ -873,7 +873,8 @@ class TestOpenFgaApi(IsolatedAsyncioTestCase): "relation": "reader", "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31b", } - ] + ], + "on_duplicate": "error", }, "authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J", }, @@ -927,7 +928,8 @@ class TestOpenFgaApi(IsolatedAsyncioTestCase): "relation": "reader", "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31b", } - ] + ], + "on_missing": "error", }, "authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J", }, diff --git a/config/clients/python/template/test/client/client_test.py.mustache b/config/clients/python/template/test/client/client_test.py.mustache index b7de74e97..8de35455e 100644 --- a/config/clients/python/template/test/client/client_test.py.mustache +++ b/config/clients/python/template/test/client/client_test.py.mustache @@ -1016,7 +1016,8 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): "relation": "reader", "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31d", }, - ] + ], + "on_duplicate": "error" }, "authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J", }, @@ -1061,7 +1062,8 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): "relation": "reader", "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31b", } - ] + ], + "on_missing": "error", }, "authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J", }, @@ -1161,7 +1163,8 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): "relation": "reader", "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31b", } - ] + ], + "on_duplicate": "error" }, "authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J", }, @@ -1182,7 +1185,8 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): "relation": "reader", "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31c", } - ] + ], + "on_duplicate": "error" }, "authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J", }, @@ -1203,7 +1207,8 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): "relation": "reader", "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31d", } - ] + ], + "on_duplicate": "error" }, "authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J", }, @@ -1303,7 +1308,8 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): "relation": "reader", "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31b", } - ] + ], + "on_duplicate": "error" }, "authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J", }, @@ -1324,7 +1330,8 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): "relation": "reader", "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31c", } - ] + ], + "on_duplicate": "error" }, "authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J", }, @@ -1345,7 +1352,8 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): "relation": "reader", "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31d", } - ] + ], + "on_duplicate": "error" }, "authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J", }, @@ -1449,7 +1457,8 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): "relation": "reader", "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31c", }, - ] + ], + "on_duplicate": "error" }, "authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J", }, @@ -1470,7 +1479,8 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): "relation": "reader", "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31d", } - ] + ], + "on_duplicate": "error" }, "authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J", }, @@ -1584,7 +1594,8 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): "relation": "reader", "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31b", } - ] + ], + "on_duplicate": "error" }, "authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J", }, @@ -1605,7 +1616,8 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): "relation": "reader", "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31c", } - ] + ], + "on_duplicate": "error" }, "authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J", }, @@ -1626,7 +1638,8 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): "relation": "reader", "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31d", } - ] + ], + "on_duplicate": "error" }, "authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J", }, @@ -1680,7 +1693,8 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): "relation": "reader", "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31b", } - ] + ], + "on_missing": "error", }, "authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J", }, @@ -1743,7 +1757,8 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): "relation": "reader", "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31d", }, - ] + ], + "on_duplicate": "error" }, "authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J", }, @@ -1806,7 +1821,8 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): "relation": "reader", "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31d", }, - ] + ], + "on_missing": "error", }, "authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J", }, @@ -1862,7 +1878,8 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): "relation": "reader", "object": "document:2021-budget", } - ] + ], + "on_duplicate": "error", }, "authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J", }, @@ -2173,7 +2190,9 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): if user == "user:81684243-9356-4421-8fbf-a4f8d36aa31b": return mock_response('{"allowed": true, "resolution": "1234"}', 200) elif user == "user:81684243-9356-4421-8fbf-a4f8d36aa31c": - raise ValidationException(http_resp=http_mock_response(response_body, 400)) + raise ValidationException( + http_resp=http_mock_response(response_body, 400) + ) elif user == "user:81684243-9356-4421-8fbf-a4f8d36aa31d": return mock_response('{"allowed": false, "resolution": "1234"}', 200) return mock_response('{"allowed": false, "resolution": "1234"}', 200) @@ -3395,8 +3414,12 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): # Verify all four headers exist with correct values self.assertEqual(result["headers"]["X-SDK-Version"], "1.0.0") # Default was set - self.assertEqual(result["headers"]["X-Request-ID"], "my-custom-request-id") # Custom overrode default - self.assertEqual(result["headers"]["X-Tenant-ID"], "tenant-123") # Custom preserved + self.assertEqual( + result["headers"]["X-Request-ID"], "my-custom-request-id" + ) # Custom overrode default + self.assertEqual( + result["headers"]["X-Tenant-ID"], "tenant-123" + ) # Custom preserved self.assertEqual(len(result["headers"]), 3) # Exactly 3 headers def test_set_heading_if_not_set_with_empty_string_value(self): @@ -3419,7 +3442,9 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): """Test that headers with special characters are handled correctly.""" options = {"headers": {"X-Custom-Header": "value-with-special!@#$%^&*()_+"}} result = set_heading_if_not_set(options, "X-Custom-Header", "default") - self.assertEqual(result["headers"]["X-Custom-Header"], "value-with-special!@#$%^&*()_+") + self.assertEqual( + result["headers"]["X-Custom-Header"], "value-with-special!@#$%^&*()_+" + ) def test_set_heading_if_not_set_case_sensitivity(self): """Test that header names are treated as case-sensitive by the helper function.""" @@ -3555,7 +3580,9 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): async with OpenFgaClient(configuration) as api_client: custom_options = {"headers": {"X-List-Objects-Id": "list-obj-999"}} - api_response = await api_client.list_objects(body=body, options=custom_options) + api_response = await api_client.list_objects( + body=body, options=custom_options + ) call_args = mock_request.call_args headers = call_args[1]["headers"] @@ -3580,7 +3607,9 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): async with OpenFgaClient(configuration) as api_client: custom_options = {"headers": {"X-List-Users-Id": "list-users-777"}} - api_response = await api_client.list_users(body=body, options=custom_options) + api_response = await api_client.list_users( + body=body, options=custom_options + ) call_args = mock_request.call_args headers = call_args[1]["headers"] @@ -3622,11 +3651,15 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): async with OpenFgaClient(configuration) as api_client: # First call with custom header 1 check_options = {"headers": {"X-Request-Id": "check-request-111"}} - check_response = await api_client.check(body=check_body, options=check_options) + check_response = await api_client.check( + body=check_body, options=check_options + ) # Second call with custom header 2 expand_options = {"headers": {"X-Request-Id": "expand-request-222"}} - expand_response = await api_client.expand(body=expand_body, options=expand_options) + expand_response = await api_client.expand( + body=expand_body, options=expand_options + ) # Verify first call had correct header first_call_args = mock_request.call_args_list[0] @@ -3939,3 +3972,198 @@ class TestClientConfigurationHeaders: config.headers["X-New"] = "new-value" assert "X-New" not in copied_config.headers + + @patch.object(rest.RESTClientObject, "request") + @pytest.mark.asyncio + async def test_write_with_conflict_options_ignore_duplicates(self, mock_request): + """Test case for write with conflict options - ignore duplicates""" + from {{packageName}}.client.models.write_conflict_opts import ( + ClientWriteRequestOnDuplicateWrites, + ConflictOptions, + ) + + response_body = "{}" + mock_request.return_value = mock_response(response_body, 200) + self.configuration = ClientConfiguration( + api_url="http://api.fga.example", + ) + configuration = self.configuration + configuration.store_id = store_id + async with OpenFgaClient(configuration) as api_client: + body = ClientWriteRequest( + writes=[ + ClientTuple( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", + ), + ], + ) + await api_client.write( + body, + options={ + "authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J", + "conflict": ConflictOptions( + on_duplicate_writes=ClientWriteRequestOnDuplicateWrites.IGNORE + ), + }, + ) + mock_request.assert_called_once_with( + "POST", + "http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/write", + headers=ANY, + query_params=[], + post_params=[], + body={ + "writes": { + "tuple_keys": [ + { + "object": "document:2021-budget", + "relation": "reader", + "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31b", + }, + ], + "on_duplicate": "ignore", + }, + "authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J", + }, + _preload_content=ANY, + _request_timeout=None, + ) + + @patch.object(rest.RESTClientObject, "request") + @pytest.mark.asyncio + async def test_write_with_conflict_options_ignore_missing_deletes( + self, mock_request + ): + """Test case for write with conflict options - ignore missing deletes""" + from {{packageName}}.client.models.write_conflict_opts import ( + ClientWriteRequestOnMissingDeletes, + ConflictOptions, + ) + + response_body = "{}" + mock_request.return_value = mock_response(response_body, 200) + self.configuration = ClientConfiguration( + api_url="http://api.fga.example", + ) + configuration = self.configuration + configuration.store_id = store_id + async with OpenFgaClient(configuration) as api_client: + body = ClientWriteRequest( + deletes=[ + ClientTuple( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", + ) + ], + ) + await api_client.write( + body, + options={ + "authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J", + "conflict": ConflictOptions( + on_missing_deletes=ClientWriteRequestOnMissingDeletes.IGNORE + ), + }, + ) + mock_request.assert_called_once_with( + "POST", + "http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/write", + headers=ANY, + query_params=[], + post_params=[], + body={ + "deletes": { + "tuple_keys": [ + { + "object": "document:2021-budget", + "relation": "reader", + "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31b", + }, + ], + "on_missing": "ignore", + }, + "authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J", + }, + _preload_content=ANY, + _request_timeout=None, + ) + + @patch.object(rest.RESTClientObject, "request") + @pytest.mark.asyncio + async def test_write_with_conflict_options_both(self, mock_request): + """Test case for write with both conflict options""" + from {{packageName}}.client.models.write_conflict_opts import ( + ClientWriteRequestOnDuplicateWrites, + ClientWriteRequestOnMissingDeletes, + ConflictOptions, + ) + + response_body = "{}" + mock_request.return_value = mock_response(response_body, 200) + self.configuration = ClientConfiguration( + api_url="http://api.fga.example", + ) + configuration = self.configuration + configuration.store_id = store_id + async with OpenFgaClient(configuration) as api_client: + body = ClientWriteRequest( + writes=[ + ClientTuple( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", + ), + ], + deletes=[ + ClientTuple( + object="document:2021-report", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31c", + ) + ], + ) + await api_client.write( + body, + options={ + "authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J", + "conflict": ConflictOptions( + on_duplicate_writes=ClientWriteRequestOnDuplicateWrites.IGNORE, + on_missing_deletes=ClientWriteRequestOnMissingDeletes.IGNORE, + ), + }, + ) + mock_request.assert_called_once_with( + "POST", + "http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/write", + headers=ANY, + query_params=[], + post_params=[], + body={ + "writes": { + "tuple_keys": [ + { + "object": "document:2021-budget", + "relation": "reader", + "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31b", + }, + ], + "on_duplicate": "ignore", + }, + "deletes": { + "tuple_keys": [ + { + "object": "document:2021-report", + "relation": "reader", + "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31c", + }, + ], + "on_missing": "ignore", + }, + "authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J", + }, + _preload_content=ANY, + _request_timeout=None, + ) diff --git a/config/clients/python/template/test/sync/api_test.py.mustache b/config/clients/python/template/test/sync/api_test.py.mustache index 7030f61b6..24c670150 100644 --- a/config/clients/python/template/test/sync/api_test.py.mustache +++ b/config/clients/python/template/test/sync/api_test.py.mustache @@ -940,7 +940,8 @@ class TestOpenFgaApiSync(IsolatedAsyncioTestCase): "relation": "reader", "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31b", } - ] + ], + "on_duplicate": "error", }, "authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J", }, @@ -994,7 +995,8 @@ class TestOpenFgaApiSync(IsolatedAsyncioTestCase): "relation": "reader", "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31b", } - ] + ], + "on_missing": "error", }, "authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J", }, diff --git a/config/clients/python/template/test/sync/client/client_test.py.mustache b/config/clients/python/template/test/sync/client/client_test.py.mustache index 814637469..a3ab59811 100644 --- a/config/clients/python/template/test/sync/client/client_test.py.mustache +++ b/config/clients/python/template/test/sync/client/client_test.py.mustache @@ -1014,7 +1014,8 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): "relation": "reader", "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31d", }, - ] + ], + "on_duplicate": "error" }, "authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J", }, @@ -1059,7 +1060,8 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): "relation": "reader", "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31b", } - ] + ], + "on_missing": "error" }, "authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J", }, @@ -1159,7 +1161,8 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): "relation": "reader", "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31b", } - ] + ], + "on_duplicate": "error" }, "authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J", }, @@ -1180,7 +1183,8 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): "relation": "reader", "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31c", } - ] + ], + "on_duplicate": "error" }, "authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J", }, @@ -1201,7 +1205,8 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): "relation": "reader", "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31d", } - ] + ], + "on_duplicate": "error" }, "authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J", }, @@ -1301,7 +1306,8 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): "relation": "reader", "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31b", } - ] + ], + "on_duplicate": "error" }, "authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J", }, @@ -1322,7 +1328,8 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): "relation": "reader", "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31c", } - ] + ], + "on_duplicate": "error" }, "authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J", }, @@ -1343,7 +1350,8 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): "relation": "reader", "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31d", } - ] + ], + "on_duplicate": "error" }, "authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J", }, @@ -1448,7 +1456,8 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): "relation": "reader", "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31c", }, - ] + ], + "on_duplicate": "error" }, "authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J", }, @@ -1469,7 +1478,8 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): "relation": "reader", "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31d", } - ] + ], + "on_duplicate": "error" }, "authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J", }, @@ -1583,7 +1593,8 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): "relation": "reader", "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31b", } - ] + ], + "on_duplicate": "error" }, "authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J", }, @@ -1604,7 +1615,8 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): "relation": "reader", "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31c", } - ] + ], + "on_duplicate": "error" }, "authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J", }, @@ -1625,7 +1637,8 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): "relation": "reader", "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31d", } - ] + ], + "on_duplicate": "error" }, "authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J", }, @@ -1679,7 +1692,8 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): "relation": "reader", "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31b", } - ] + ], + "on_missing": "error" }, "authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J", }, @@ -1742,7 +1756,8 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): "relation": "reader", "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31d", }, - ] + ], + "on_duplicate": "error" }, "authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J", }, @@ -1805,7 +1820,8 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): "relation": "reader", "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31d", }, - ] + ], + "on_missing": "error" }, "authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J", }, @@ -1861,7 +1877,8 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): "relation": "reader", "object": "document:2021-budget", } - ] + ], + "on_duplicate": "error" }, "authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J", }, @@ -2176,7 +2193,9 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): if user == "user:81684243-9356-4421-8fbf-a4f8d36aa31b": return mock_response('{"allowed": true, "resolution": "1234"}', 200) elif user == "user:81684243-9356-4421-8fbf-a4f8d36aa31c": - raise ValidationException(http_resp=http_mock_response(response_body, 400)) + raise ValidationException( + http_resp=http_mock_response(response_body, 400) + ) elif user == "user:81684243-9356-4421-8fbf-a4f8d36aa31d": return mock_response('{"allowed": false, "resolution": "1234"}', 200) return mock_response('{"allowed": false, "resolution": "1234"}', 200) @@ -3397,8 +3416,12 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): # Verify all four headers exist with correct values self.assertEqual(result["headers"]["X-SDK-Version"], "1.0.0") # Default was set - self.assertEqual(result["headers"]["X-Request-ID"], "my-custom-request-id") # Custom overrode default - self.assertEqual(result["headers"]["X-Tenant-ID"], "tenant-123") # Custom preserved + self.assertEqual( + result["headers"]["X-Request-ID"], "my-custom-request-id" + ) # Custom overrode default + self.assertEqual( + result["headers"]["X-Tenant-ID"], "tenant-123" + ) # Custom preserved self.assertEqual(len(result["headers"]), 3) # Exactly 3 headers def test_set_heading_if_not_set_with_empty_string_value(self): @@ -3481,9 +3504,7 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): with OpenFgaClient(configuration) as api_client: # Add custom header that should override any defaults - custom_options = { - "headers": {"X-Custom-Request-Id": "custom-request-123"} - } + custom_options = {"headers": {"X-Custom-Request-Id": "custom-request-123"}} api_response = api_client.check(body=body, options=custom_options) # Verify the API was called and extract the headers @@ -3938,3 +3959,192 @@ class TestSyncClientConfigurationHeaders: config.headers["X-New"] = "new-value" assert "X-New" not in copied_config.headers + + @patch.object(rest.RESTClientObject, "request") + def test_sync_write_with_conflict_options_ignore_duplicates(self, mock_request): + """Test case for write with conflict options - ignore duplicates""" + from {{packageName}}.client.models.write_conflict_opts import ( + ClientWriteRequestOnDuplicateWrites, + ConflictOptions, + ) + + response_body = "{}" + mock_request.return_value = mock_response(response_body, 200) + configuration = ClientConfiguration( + api_url="http://api.fga.example", + ) + configuration.store_id = store_id + with OpenFgaClient(configuration) as api_client: + body = ClientWriteRequest( + writes=[ + ClientTuple( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", + ), + ], + ) + api_client.write( + body, + options={ + "authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J", + "conflict": ConflictOptions( + on_duplicate_writes=ClientWriteRequestOnDuplicateWrites.IGNORE + ), + }, + ) + mock_request.assert_called_once_with( + "POST", + "http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/write", + headers=ANY, + query_params=[], + post_params=[], + body={ + "writes": { + "tuple_keys": [ + { + "object": "document:2021-budget", + "relation": "reader", + "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31b", + }, + ], + "on_duplicate": "ignore", + }, + "authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J", + }, + _preload_content=ANY, + _request_timeout=None, + ) + + @patch.object(rest.RESTClientObject, "request") + def test_sync_write_with_conflict_options_ignore_missing_deletes( + self, mock_request + ): + """Test case for write with conflict options - ignore missing deletes""" + from {{packageName}}.client.models.write_conflict_opts import ( + ClientWriteRequestOnMissingDeletes, + ConflictOptions, + ) + + response_body = "{}" + mock_request.return_value = mock_response(response_body, 200) + configuration = ClientConfiguration( + api_url="http://api.fga.example", + ) + configuration.store_id = store_id + with OpenFgaClient(configuration) as api_client: + body = ClientWriteRequest( + deletes=[ + ClientTuple( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", + ) + ], + ) + api_client.write( + body, + options={ + "authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J", + "conflict": ConflictOptions( + on_missing_deletes=ClientWriteRequestOnMissingDeletes.IGNORE + ), + }, + ) + mock_request.assert_called_once_with( + "POST", + "http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/write", + headers=ANY, + query_params=[], + post_params=[], + body={ + "deletes": { + "tuple_keys": [ + { + "object": "document:2021-budget", + "relation": "reader", + "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31b", + }, + ], + "on_missing": "ignore", + }, + "authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J", + }, + _preload_content=ANY, + _request_timeout=None, + ) + + @patch.object(rest.RESTClientObject, "request") + def test_sync_write_with_conflict_options_both(self, mock_request): + """Test case for write with both conflict options""" + from {{packageName}}.client.models.write_conflict_opts import ( + ClientWriteRequestOnDuplicateWrites, + ClientWriteRequestOnMissingDeletes, + ConflictOptions, + ) + + response_body = "{}" + mock_request.return_value = mock_response(response_body, 200) + configuration = ClientConfiguration( + api_url="http://api.fga.example", + ) + configuration.store_id = store_id + with OpenFgaClient(configuration) as api_client: + body = ClientWriteRequest( + writes=[ + ClientTuple( + object="document:2021-budget", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", + ), + ], + deletes=[ + ClientTuple( + object="document:2021-report", + relation="reader", + user="user:81684243-9356-4421-8fbf-a4f8d36aa31c", + ) + ], + ) + api_client.write( + body, + options={ + "authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J", + "conflict": ConflictOptions( + on_duplicate_writes=ClientWriteRequestOnDuplicateWrites.IGNORE, + on_missing_deletes=ClientWriteRequestOnMissingDeletes.IGNORE, + ), + }, + ) + mock_request.assert_called_once_with( + "POST", + "http://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/write", + headers=ANY, + query_params=[], + post_params=[], + body={ + "writes": { + "tuple_keys": [ + { + "object": "document:2021-budget", + "relation": "reader", + "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31b", + }, + ], + "on_duplicate": "ignore", + }, + "deletes": { + "tuple_keys": [ + { + "object": "document:2021-report", + "relation": "reader", + "user": "user:81684243-9356-4421-8fbf-a4f8d36aa31c", + }, + ], + "on_missing": "ignore", + }, + "authorization_model_id": "01G5JAVJ41T49E9TT3SKVS7X1J", + }, + _preload_content=ANY, + _request_timeout=None, + )