diff --git a/.openapi-generator/FILES b/.openapi-generator/FILES index 09c39d0b..0305274d 100644 --- a/.openapi-generator/FILES +++ b/.openapi-generator/FILES @@ -116,14 +116,9 @@ example/opentelemetry/main.py example/opentelemetry/requirements.txt example/opentelemetry/setup.cfg example/opentelemetry/setup.py -example/streamed-list-objects/.env.example -example/streamed-list-objects/.gitignore example/streamed-list-objects/README.md -example/streamed-list-objects/asynchronous.py -example/streamed-list-objects/requirements.txt -example/streamed-list-objects/setup.cfg -example/streamed-list-objects/setup.py -example/streamed-list-objects/synchronous.py +example/streamed-list-objects/example.py +example/streamed-list-objects/model.json openfga_sdk/__init__.py openfga_sdk/api/__init__.py openfga_sdk/api/open_fga_api.py diff --git a/example/streamed-list-objects/.env.example b/example/streamed-list-objects/.env.example deleted file mode 100644 index 2bc57571..00000000 --- a/example/streamed-list-objects/.env.example +++ /dev/null @@ -1 +0,0 @@ -FGA_API_URL="http://localhost:8080" diff --git a/example/streamed-list-objects/.gitignore b/example/streamed-list-objects/.gitignore deleted file mode 100644 index 4c49bd78..00000000 --- a/example/streamed-list-objects/.gitignore +++ /dev/null @@ -1 +0,0 @@ -.env diff --git a/example/streamed-list-objects/README.md b/example/streamed-list-objects/README.md index 97508484..2b1dff39 100644 --- a/example/streamed-list-objects/README.md +++ b/example/streamed-list-objects/README.md @@ -1,39 +1,22 @@ -# Streamed List Objects example for OpenFGA's Python SDK +# Streamed List Objects Example -This example demonstrates working with the `POST` `/stores/:id/streamed-list-objects` endpoint in OpenFGA using the Python SDK. +This example demonstrates working with [OpenFGA's `/streamed-list-objects` endpoint](https://openfga.dev/api/service#/Relationship%20Queries/StreamedListObjects) using the Python SDK's `streamed_list_objects()` method. ## Prerequisites -If you do not already have an OpenFGA instance running, you can start one using the following command: +- Python 3.10+ +- OpenFGA running on `localhost:8080` -```bash -docker run -d -p 8080:8080 openfga/openfga -``` - -## Configure the example - -You may need to configure the example for your environment: +You can start OpenFGA with Docker by running the following command: ```bash -cp .env.example .env +docker pull openfga/openfga && docker run -it --rm -p 8080:8080 openfga/openfga run ``` -Now edit the `.env` file and set the values as appropriate. - ## Running the example -Begin by installing the required dependencies: - -```bash -pip install -r requirements.txt -``` - -Next, run the example. You can use either the synchronous or asynchronous client: - -```bash -python asynchronous.py -``` +No additional setup is required to run this example. Simply run the following command: ```bash -python synchronous.py +python example.py ``` diff --git a/example/streamed-list-objects/asynchronous.py b/example/streamed-list-objects/asynchronous.py deleted file mode 100644 index ec89bb13..00000000 --- a/example/streamed-list-objects/asynchronous.py +++ /dev/null @@ -1,137 +0,0 @@ -# ruff: noqa: E402 - -""" -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. -""" - -import asyncio -import json -import os -import sys - -from operator import attrgetter -from typing import Any - -from dotenv import load_dotenv - - -sdk_path = os.path.realpath(os.path.join(os.path.abspath(__file__), "..", "..", "..")) -sys.path.insert(0, sdk_path) - -from openfga_sdk import ( - ClientConfiguration, - OpenFgaClient, -) -from openfga_sdk.client.models import ( - ClientListObjectsRequest, - ClientTuple, - ClientWriteRequest, -) -from openfga_sdk.models import CreateStoreRequest - - -class app: - def __init__( - self, - client: OpenFgaClient = None, - configuration: ClientConfiguration = None, - ): - self._client = client - self._configuration = configuration - - async def fga_client(self, env: dict[str, str] = {}) -> OpenFgaClient: - if not self._client or not self._configuration: - load_dotenv() - - if not self._configuration: - self._configuration = ClientConfiguration( - api_url=os.getenv("FGA_API_URL"), - ) - - self._client = OpenFgaClient(self._configuration) - return self._client - - -def unpack( - response, - attr: str, -) -> Any: - return attrgetter(attr)(response) - - -async def main(): - async with await app().fga_client() as fga_client: - # Create a temporary store - store = unpack( - await fga_client.create_store(CreateStoreRequest(name="Test Store")), - "id", - ) - print(f"Created temporary store ({store})") - fga_client.set_store_id(store) - - # Create a temporary authorization model - model = unpack( - await fga_client.write_authorization_model( - json.loads( - '{"schema_version":"1.1","type_definitions":[{"type":"user","relations":{}},{"type":"group","relations":{"member":{"this":{}}},"metadata":{"relations":{"member":{"directly_related_user_types":[{"type":"user"}]}}}},{"type":"folder","relations":{"can_create_file":{"computedUserset":{"object":"","relation":"owner"}},"owner":{"this":{}},"parent":{"this":{}},"viewer":{"union":{"child":[{"this":{}},{"computedUserset":{"object":"","relation":"owner"}},{"tupleToUserset":{"tupleset":{"object":"","relation":"parent"},"computedUserset":{"object":"","relation":"viewer"}}}]}}},"metadata":{"relations":{"can_create_file":{"directly_related_user_types":[]},"owner":{"directly_related_user_types":[{"type":"user"}]},"parent":{"directly_related_user_types":[{"type":"folder"}]},"viewer":{"directly_related_user_types":[{"type":"user"},{"type":"user","wildcard":{}},{"type":"group","relation":"member"}]}}}},{"type":"document","relations":{"can_change_owner":{"computedUserset":{"object":"","relation":"owner"}},"owner":{"this":{}},"parent":{"this":{}},"can_read":{"union":{"child":[{"computedUserset":{"object":"","relation":"viewer"}},{"computedUserset":{"object":"","relation":"owner"}},{"tupleToUserset":{"tupleset":{"object":"","relation":"parent"},"computedUserset":{"object":"","relation":"viewer"}}}]}},"can_share":{"union":{"child":[{"computedUserset":{"object":"","relation":"owner"}},{"tupleToUserset":{"tupleset":{"object":"","relation":"parent"},"computedUserset":{"object":"","relation":"owner"}}}]}},"viewer":{"this":{}},"can_write":{"union":{"child":[{"computedUserset":{"object":"","relation":"owner"}},{"tupleToUserset":{"tupleset":{"object":"","relation":"parent"},"computedUserset":{"object":"","relation":"owner"}}}]}}},"metadata":{"relations":{"can_change_owner":{"directly_related_user_types":[]},"owner":{"directly_related_user_types":[{"type":"user"}]},"parent":{"directly_related_user_types":[{"type":"folder"}]},"can_read":{"directly_related_user_types":[]},"can_share":{"directly_related_user_types":[]},"viewer":{"directly_related_user_types":[{"type":"user"},{"type":"user","wildcard":{}},{"type":"group","relation":"member"}]},"can_write":{"directly_related_user_types":[]}}}}]}' - ) - ), - "authorization_model_id", - ) - print(f"Created temporary authorization model ({model})") - - print("Writing 100 mock tuples to store.") - - # Write mock data - writes = [] - for x in range(0, 100): - writes.append( - ClientTuple( - user="user:anne", - relation="owner", - object=f"document:{x}", - ) - ) - - await fga_client.write( - ClientWriteRequest(writes), - { - "authorization_model_id": model, - }, - ) - - print("Listing objects using streaming endpoint:") - results = [] - - request = ClientListObjectsRequest( - type="document", - relation="owner", - user="user:anne", - ) - - async for response in fga_client.streamed_list_objects(request): - print(f" {response}") - results.append(response) - - print(f"API returned {results.__len__()} objects.") - - # Delete the temporary store - try: - await fga_client.delete_store() - print(f"Deleted temporary store ({store})") - except Exception: - pass - - print("Finished.") - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/example/streamed-list-objects/example.py b/example/streamed-list-objects/example.py new file mode 100644 index 00000000..15268844 --- /dev/null +++ b/example/streamed-list-objects/example.py @@ -0,0 +1,158 @@ +# ruff: noqa: E402 + +""" +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. +""" + +import asyncio +import json +import os +import sys + + +### +# The following two lines are just a convenience for SDK development and testing, +# and should not be used in your code. They allow you to run this example using the +# local SDK code from the parent directory, rather than using the installed package. +### +sdk_path = os.path.realpath(os.path.join(os.path.abspath(__file__), "..", "..", "..")) +sys.path.insert(0, sdk_path) + +from openfga_sdk.client import OpenFgaClient +from openfga_sdk.client.configuration import ClientConfiguration +from openfga_sdk.client.models.list_objects_request import ClientListObjectsRequest +from openfga_sdk.client.models.tuple import ClientTuple +from openfga_sdk.models.create_store_request import CreateStoreRequest + + +async def create_store(openfga: OpenFgaClient) -> str: + """ + Create a temporary store. The store will be deleted at the end of the example. + """ + + response = await openfga.create_store(CreateStoreRequest(name="Demo Store")) + return response.id + + +async def write_model(openfga: OpenFgaClient) -> str: + """ + Load the authentication model from a file and write it to the server. + """ + + with open("model.json") as model: + response = await openfga.write_authorization_model(json.loads(model.read())) + return response.authorization_model_id + + +async def write_tuples(openfga: OpenFgaClient, quantity: int) -> int: + """ + Write a variable number of tuples to the temporary store. + """ + + chunks = quantity // 100 + + for chunk in range(0, chunks): + await openfga.write_tuples( + [ + ClientTuple( + user="user:anne", + relation="owner", + object=f"document:{chunk * 100 + t}", + ) + for t in range(0, 100) + ], + ) + + return quantity + + +async def streamed_list_objects( + openfga: OpenFgaClient, request: ClientListObjectsRequest +): + """ + Send our request to the streaming endpoint, and iterate over the streamed responses. + """ + results = [] + + # Note that streamed_list_objects() is an asynchronous generator, so we could yield results as they come in. + # For the sake of this example, we'll just collect all the results into a list and yield them all at once. + + async for response in openfga.streamed_list_objects(request): + results.append(response.object) + + return results + + +async def list_objects(openfga: OpenFgaClient, request: ClientListObjectsRequest): + """ + For comparison sake, here is the non-streamed version of the same call, using list_objects(). + Note that in the non-streamed version, the server will return a maximum of 1000 results. + """ + results = await openfga.list_objects(request) + return results.objects + + +async def main(): + configure = ClientConfiguration( + api_url="http://localhost:8080", + ) + + async with OpenFgaClient(configure) as openfga: + # Create our temporary store + store = await create_store(openfga) + print(f"Created temporary store ({store})") + + # Configure the SDK to use the temporary store for the rest of the example + openfga.set_store_id(store) + + # Load the authorization model from a file and write it to the server + model = await write_model(openfga) + print(f"Created temporary authorization model ({model})\n") + + # Configure the SDK to use this authorization model for the rest of the example + openfga.set_authorization_model_id(model) + + # Write a bunch of example tuples to the temporary store + wrote = await write_tuples(openfga, 2000) + print(f"Wrote {wrote} tuples to the store.\n") + + ################################ + + # Craft a request to list all `documents` owned by `user:anne`` + request = ClientListObjectsRequest( + type="document", + relation="owner", + user="user:anne", + ) + + # Send a single request to the server using both the streamed and standard endpoints + streamed_results = await streamed_list_objects(openfga, request) + standard_results = await list_objects(openfga, request) + + print( + f"/streamed-list-objects returned {streamed_results.__len__()} objects in a single request.", + ) + # print([r for r in streamed_results]) + + print( + f"/list-objects returned {standard_results.__len__()} objects in a single request.", + ) + # print([r for r in standard_results]) + + ################################ + + # Finally, delete the temporary store. + await openfga.delete_store() + print(f"\nDeleted temporary store ({store})") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/example/streamed-list-objects/model.json b/example/streamed-list-objects/model.json new file mode 100644 index 00000000..8ec4685f --- /dev/null +++ b/example/streamed-list-objects/model.json @@ -0,0 +1,121 @@ +{ + "schema_version": "1.1", + "type_definitions": [ + { "type": "user", "relations": {} }, + { + "type": "group", + "relations": { "member": { "this": {} } }, + "metadata": { + "relations": { + "member": { "directly_related_user_types": [{ "type": "user" }] } + } + } + }, + { + "type": "folder", + "relations": { + "can_create_file": { + "computedUserset": { "object": "", "relation": "owner" } + }, + "owner": { "this": {} }, + "parent": { "this": {} }, + "viewer": { + "union": { + "child": [ + { "this": {} }, + { "computedUserset": { "object": "", "relation": "owner" } }, + { + "tupleToUserset": { + "tupleset": { "object": "", "relation": "parent" }, + "computedUserset": { "object": "", "relation": "viewer" } + } + } + ] + } + } + }, + "metadata": { + "relations": { + "can_create_file": { "directly_related_user_types": [] }, + "owner": { "directly_related_user_types": [{ "type": "user" }] }, + "parent": { "directly_related_user_types": [{ "type": "folder" }] }, + "viewer": { + "directly_related_user_types": [ + { "type": "user" }, + { "type": "user", "wildcard": {} }, + { "type": "group", "relation": "member" } + ] + } + } + } + }, + { + "type": "document", + "relations": { + "can_change_owner": { + "computedUserset": { "object": "", "relation": "owner" } + }, + "owner": { "this": {} }, + "parent": { "this": {} }, + "can_read": { + "union": { + "child": [ + { "computedUserset": { "object": "", "relation": "viewer" } }, + { "computedUserset": { "object": "", "relation": "owner" } }, + { + "tupleToUserset": { + "tupleset": { "object": "", "relation": "parent" }, + "computedUserset": { "object": "", "relation": "viewer" } + } + } + ] + } + }, + "can_share": { + "union": { + "child": [ + { "computedUserset": { "object": "", "relation": "owner" } }, + { + "tupleToUserset": { + "tupleset": { "object": "", "relation": "parent" }, + "computedUserset": { "object": "", "relation": "owner" } + } + } + ] + } + }, + "viewer": { "this": {} }, + "can_write": { + "union": { + "child": [ + { "computedUserset": { "object": "", "relation": "owner" } }, + { + "tupleToUserset": { + "tupleset": { "object": "", "relation": "parent" }, + "computedUserset": { "object": "", "relation": "owner" } + } + } + ] + } + } + }, + "metadata": { + "relations": { + "can_change_owner": { "directly_related_user_types": [] }, + "owner": { "directly_related_user_types": [{ "type": "user" }] }, + "parent": { "directly_related_user_types": [{ "type": "folder" }] }, + "can_read": { "directly_related_user_types": [] }, + "can_share": { "directly_related_user_types": [] }, + "viewer": { + "directly_related_user_types": [ + { "type": "user" }, + { "type": "user", "wildcard": {} }, + { "type": "group", "relation": "member" } + ] + }, + "can_write": { "directly_related_user_types": [] } + } + } + } + ] +} diff --git a/example/streamed-list-objects/requirements.txt b/example/streamed-list-objects/requirements.txt deleted file mode 100644 index 78af7ab4..00000000 --- a/example/streamed-list-objects/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -python-dotenv >= 1, <2 diff --git a/example/streamed-list-objects/setup.cfg b/example/streamed-list-objects/setup.cfg deleted file mode 100644 index 11433ee8..00000000 --- a/example/streamed-list-objects/setup.cfg +++ /dev/null @@ -1,2 +0,0 @@ -[flake8] -max-line-length=99 diff --git a/example/streamed-list-objects/setup.py b/example/streamed-list-objects/setup.py deleted file mode 100644 index f8e36701..00000000 --- a/example/streamed-list-objects/setup.py +++ /dev/null @@ -1,31 +0,0 @@ -""" -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 setuptools import find_packages, setup - - -NAME = "openfga-streamed-list-objects-example" -VERSION = "0.0.1" -REQUIRES = [""] - -setup( - name=NAME, - version=VERSION, - description="An example of using the OpenFGA Python SDK with the Streamed List Objects endpoint.", - author="OpenFGA (https://openfga.dev)", - author_email="community@openfga.dev", - url="https://github.com/openfga/python-sdk", - python_requires=">=3.10", - packages=find_packages(exclude=["test", "tests"]), - include_package_data=True, - license="Apache-2.0", -) diff --git a/example/streamed-list-objects/synchronous.py b/example/streamed-list-objects/synchronous.py deleted file mode 100644 index 58161e70..00000000 --- a/example/streamed-list-objects/synchronous.py +++ /dev/null @@ -1,134 +0,0 @@ -# ruff: noqa: E402 - -""" -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. -""" - -import json -import os -import sys - -from operator import attrgetter -from typing import Any - -from dotenv import load_dotenv - - -sdk_path = os.path.realpath(os.path.join(os.path.abspath(__file__), "..", "..", "..")) -sys.path.insert(0, sdk_path) - -from openfga_sdk import ClientConfiguration -from openfga_sdk.client.models import ( - ClientListObjectsRequest, - ClientTuple, - ClientWriteRequest, -) -from openfga_sdk.models.create_store_request import CreateStoreRequest -from openfga_sdk.sync import OpenFgaClient - - -class app: - def __init__( - self, - client: OpenFgaClient = None, - configuration: ClientConfiguration = None, - ): - self._client = client - self._configuration = configuration - - def fga_client(self, env: dict[str, str] = {}) -> OpenFgaClient: - if not self._client or not self._configuration: - load_dotenv() - - if not self._configuration: - self._configuration = ClientConfiguration( - api_url=os.getenv("FGA_API_URL"), - ) - - self._client = OpenFgaClient(self._configuration) - return self._client - - -def unpack( - response, - attr: str, -) -> Any: - return attrgetter(attr)(response) - - -def main(): - with app().fga_client() as fga_client: - # Create a temporary store - store = unpack( - fga_client.create_store(CreateStoreRequest(name="Test Store")), - "id", - ) - print(f"Created temporary store ({store})") - fga_client.set_store_id(store) - - # Create a temporary authorization model - model = unpack( - fga_client.write_authorization_model( - json.loads( - '{"schema_version":"1.1","type_definitions":[{"type":"user","relations":{}},{"type":"group","relations":{"member":{"this":{}}},"metadata":{"relations":{"member":{"directly_related_user_types":[{"type":"user"}]}}}},{"type":"folder","relations":{"can_create_file":{"computedUserset":{"object":"","relation":"owner"}},"owner":{"this":{}},"parent":{"this":{}},"viewer":{"union":{"child":[{"this":{}},{"computedUserset":{"object":"","relation":"owner"}},{"tupleToUserset":{"tupleset":{"object":"","relation":"parent"},"computedUserset":{"object":"","relation":"viewer"}}}]}}},"metadata":{"relations":{"can_create_file":{"directly_related_user_types":[]},"owner":{"directly_related_user_types":[{"type":"user"}]},"parent":{"directly_related_user_types":[{"type":"folder"}]},"viewer":{"directly_related_user_types":[{"type":"user"},{"type":"user","wildcard":{}},{"type":"group","relation":"member"}]}}}},{"type":"document","relations":{"can_change_owner":{"computedUserset":{"object":"","relation":"owner"}},"owner":{"this":{}},"parent":{"this":{}},"can_read":{"union":{"child":[{"computedUserset":{"object":"","relation":"viewer"}},{"computedUserset":{"object":"","relation":"owner"}},{"tupleToUserset":{"tupleset":{"object":"","relation":"parent"},"computedUserset":{"object":"","relation":"viewer"}}}]}},"can_share":{"union":{"child":[{"computedUserset":{"object":"","relation":"owner"}},{"tupleToUserset":{"tupleset":{"object":"","relation":"parent"},"computedUserset":{"object":"","relation":"owner"}}}]}},"viewer":{"this":{}},"can_write":{"union":{"child":[{"computedUserset":{"object":"","relation":"owner"}},{"tupleToUserset":{"tupleset":{"object":"","relation":"parent"},"computedUserset":{"object":"","relation":"owner"}}}]}}},"metadata":{"relations":{"can_change_owner":{"directly_related_user_types":[]},"owner":{"directly_related_user_types":[{"type":"user"}]},"parent":{"directly_related_user_types":[{"type":"folder"}]},"can_read":{"directly_related_user_types":[]},"can_share":{"directly_related_user_types":[]},"viewer":{"directly_related_user_types":[{"type":"user"},{"type":"user","wildcard":{}},{"type":"group","relation":"member"}]},"can_write":{"directly_related_user_types":[]}}}}]}' - ) - ), - "authorization_model_id", - ) - print(f"Created temporary authorization model ({model})") - - print("Writing 100 mock tuples to store.") - - # Write mock data - writes = [] - for x in range(0, 100): - writes.append( - ClientTuple( - user="user:anne", - relation="owner", - object=f"document:{x}", - ) - ) - - fga_client.write( - ClientWriteRequest(writes), - { - "authorization_model_id": model, - }, - ) - - print("Listing objects using streaming endpoint:") - results = [] - - request = ClientListObjectsRequest( - type="document", - relation="owner", - user="user:anne", - ) - - for response in fga_client.streamed_list_objects(request): - print(f" {response}") - results.append(response) - - print(f"API returned {results.__len__()} objects.") - - # Delete the temporary store - try: - fga_client.delete_store() - print(f"Deleted temporary store ({store})") - except Exception: - pass - - print("Finished.") - - -if __name__ == "__main__": - main()