From 22bba32db59aaa5e64c7460aec83a69d5c8f30b5 Mon Sep 17 00:00:00 2001 From: Ewan Harris Date: Fri, 2 Feb 2024 16:04:53 +0000 Subject: [PATCH 1/5] feat(js-sdk): support apiUrl configuration option --- .../js/template/README_initializing.mustache | 9 ++--- .../js/template/configuration.mustache | 39 ++++++++++++++++--- .../tests/helpers/default-config.ts.mustache | 4 +- .../js/template/tests/index.test.ts.mustache | 26 +++++++++---- 4 files changed, 58 insertions(+), 20 deletions(-) diff --git a/config/clients/js/template/README_initializing.mustache b/config/clients/js/template/README_initializing.mustache index b5b35c72..ae077063 100644 --- a/config/clients/js/template/README_initializing.mustache +++ b/config/clients/js/template/README_initializing.mustache @@ -8,8 +8,7 @@ The documentation below refers to the `{{appShortName}}Client`, to read the docu const { {{appShortName}}Client } = require('{{packageName}}'); // OR import { {{appShortName}}Client } from '{{packageName}}'; const fgaClient = new {{appShortName}}Client({ - apiScheme: process.env.FGA_API_SCHEME, // optional, defaults to "https" - apiHost: process.env.FGA_API_HOST, // required, define without the scheme (e.g. api.{{sampleApiDomain}} instead of https://api.{{sampleApiDomain}}) + apiUrl: process.env.FGA_API_URL, // required storeId: process.env.FGA_STORE_ID, // not needed when calling `CreateStore` or `ListStores` authorizationModelId: process.env.FGA_AUTHORIZATION_MODEL_ID, // Optional, can be overridden per request }); @@ -21,8 +20,7 @@ const fgaClient = new {{appShortName}}Client({ const { {{appShortName}}Client } = require('{{packageName}}'); // OR import { {{appShortName}}Client } from '{{packageName}}'; const fgaClient = new {{appShortName}}Client({ - apiScheme: process.env.FGA_API_SCHEME, // optional, defaults to "https" - apiHost: process.env.FGA_API_HOST, // required, define without the scheme (e.g. api.{{sampleApiDomain}} instead of https://api.{{sampleApiDomain}}) + apiUrl: process.env.FGA_API_URL, // required storeId: process.env.FGA_STORE_ID, // not needed when calling `CreateStore` or `ListStores` authorizationModelId: process.env.FGA_AUTHORIZATION_MODEL_ID, // Optional, can be overridden per request credentials: { @@ -40,8 +38,7 @@ const fgaClient = new {{appShortName}}Client({ const { {{appShortName}}Client } = require('{{packageName}}'); // OR import { {{appShortName}}Client } from '{{packageName}}'; const fgaClient = new {{appShortName}}Client({ - apiScheme: process.env.FGA_API_SCHEME, // optional, defaults to "https" - apiHost: process.env.FGA_API_HOST, // required, define without the scheme (e.g. api.{{sampleApiDomain}} instead of https://api.{{sampleApiDomain}}) + apiUrl: process.env.FGA_API_URL, // required storeId: process.env.FGA_STORE_ID, // not needed when calling `CreateStore` or `ListStores` authorizationModelId: process.env.FGA_AUTHORIZATION_MODEL_ID, // Optional, can be overridden per request credentials: { diff --git a/config/clients/js/template/configuration.mustache b/config/clients/js/template/configuration.mustache index 8de20786..b9c5c57c 100644 --- a/config/clients/js/template/configuration.mustache +++ b/config/clients/js/template/configuration.mustache @@ -18,8 +18,15 @@ export interface RetryParams { } export interface UserConfigurationParams { + apiUrl?: string; + /** + * @deprecated Replace usage of `apiScheme` + `apiHost` with `apiUrl` + */ apiScheme?: string; - apiHost: string; + /** + * @deprecated Replace usage of `apiScheme` + `apiHost` with `apiUrl` + */ + apiHost?: string; storeId?: string; credentials?: CredentialsConfig; baseOptions?: any; @@ -58,11 +65,20 @@ export class Configuration { */ private static sdkVersion = "{{packageVersion}}"; + /** + * provide the full api URL + * + * @type {string} + * @memberof Configuration + */ + apiUrl: string; + /** * provide scheme (e.g. `https`) * * @type {string} * @memberof Configuration + * @deprecated */ apiScheme = "https"; /** @@ -70,6 +86,7 @@ export class Configuration { * * @type {string} * @memberof Configuration + * @deprecated */ apiHost: string; /** @@ -104,6 +121,7 @@ export class Configuration { constructor(params: UserConfigurationParams = {} as unknown as UserConfigurationParams) { this.apiScheme = params.apiScheme || this.apiScheme; this.apiHost = params.apiHost!; + this.apiUrl = params.apiUrl!; this.storeId = params.storeId!; const credentialParams = params.credentials; @@ -156,12 +174,17 @@ export class Configuration { * @throws {FgaValidationError} */ public isValid(): boolean { - assertParamExists("Configuration", "apiScheme", this.apiScheme); - assertParamExists("Configuration", "apiHost", this.apiHost); + if (!this.apiUrl) { + assertParamExists("Configuration", "apiScheme", this.apiScheme); + assertParamExists("Configuration", "apiHost", this.apiHost); + } if (!isWellFormedUriString(this.getBasePath())) { throw new FgaValidationError( - `Configuration.apiScheme (${this.apiScheme}) and Configuration.apiHost (${this.apiHost}) do not form a valid URI (${this.getBasePath()})`); + this.apiUrl ? + `Configuration.apiUrl (${this.apiUrl}) is not a valid URI (${this.getBasePath()})` : + `Configuration.apiScheme (${this.apiScheme}) and Configuration.apiHost (${this.apiHost}) do not form a valid URI (${this.getBasePath()})` + ); } if (this.storeId && !isWellFormedUlidString(this.storeId)) { @@ -178,5 +201,11 @@ export class Configuration { /** * Returns the API base path (apiScheme+apiHost) */ - public getBasePath: () => string = () => `${this.apiScheme}://${this.apiHost}`; + public getBasePath: () => string = () => { + if (this.apiUrl) { + return this.apiUrl + } else { + return `${this.apiScheme}://${this.apiHost}` + } + }; } diff --git a/config/clients/js/template/tests/helpers/default-config.ts.mustache b/config/clients/js/template/tests/helpers/default-config.ts.mustache index db408ee5..b0c98ec0 100644 --- a/config/clients/js/template/tests/helpers/default-config.ts.mustache +++ b/config/clients/js/template/tests/helpers/default-config.ts.mustache @@ -4,7 +4,7 @@ import { Configuration, UserConfigurationParams } from "../../configuration"; import { CredentialsMethod } from "../../credentials"; export const {{appUpperCaseName}}_STORE_ID = "01H0H015178Y2V4CX10C2KGHF4"; -export const {{appUpperCaseName}}_API_HOST = "api.{{sampleApiDomain}}"; +export const {{appUpperCaseName}}_API_URL = "http://api.{{sampleApiDomain}}"; export const {{appUpperCaseName}}_API_TOKEN_ISSUER = "tokenissuer.{{sampleApiDomain}}"; export const {{appUpperCaseName}}_API_AUDIENCE = "https://api.{{sampleApiDomain}}/"; export const {{appUpperCaseName}}_CLIENT_ID = "01H0H3D8TD07EWAQHXY9BWJG3V"; @@ -13,7 +13,7 @@ export const {{appUpperCaseName}}_API_TOKEN = "fga_abcdef"; export const baseConfig: UserConfigurationParams = { storeId: {{appUpperCaseName}}_STORE_ID, - apiHost: {{appUpperCaseName}}_API_HOST, + apiUrl: {{appUpperCaseName}}_API_URL, credentials: { method: CredentialsMethod.ClientCredentials, config: { diff --git a/config/clients/js/template/tests/index.test.ts.mustache b/config/clients/js/template/tests/index.test.ts.mustache index 62fb5395..99079128 100644 --- a/config/clients/js/template/tests/index.test.ts.mustache +++ b/config/clients/js/template/tests/index.test.ts.mustache @@ -21,7 +21,7 @@ import { AuthCredentialsConfig } from "../credentials"; import { baseConfig, defaultConfiguration, - {{appUpperCaseName}}_API_HOST, + {{appUpperCaseName}}_API_URL, {{appUpperCaseName}}_API_TOKEN_ISSUER, {{appUpperCaseName}}_STORE_ID } from "./helpers/default-config"; @@ -53,16 +53,28 @@ describe("{{appTitleCaseName}} SDK", function () { it("should require host in configuration", () => { expect( - () => new {{appShortName}}Api({ ...baseConfig, apiHost: undefined! }) + () => new {{appShortName}}Api({ ...baseConfig, apiUrl: undefined! }) ).toThrowError(); }); it("should validate host in configuration (adding scheme as part of the host)", () => { expect( - () => new {{appShortName}}Api({ ...baseConfig, apiHost: "https://api.{{sampleApiDomain}}" }) + () => new {{appShortName}}Api({ ...baseConfig, apiUrl: "//api.{{sampleApiDomain}}" }) ).toThrowError(); }); + it("should allow using apiHost if apiUrl is not provided", () => { + expect( + () => new OpenFgaApi({ ...baseConfig, apiHost: "api.fga.example" }) + ).not.toThrowError(); + }); + + it("should still validate apiHost", () => { + expect( + () => new OpenFgaApi({ ...baseConfig, apiHost: "//api.{{sampleApiDomain}}" }) + ).not.toThrowError(); + }); + it("should validate apiTokenIssuer in configuration (should not allow scheme as part of the apiTokenIssuer)", () => { expect( () => new {{appShortName}}Api({ @@ -83,7 +95,7 @@ describe("{{appTitleCaseName}} SDK", function () { () => new {{appShortName}}Api({ storeId: baseConfig.storeId!, - apiHost: baseConfig.apiHost, + apiUrl: baseConfig.apiUrl, }) ).not.toThrowError(); }); @@ -93,7 +105,7 @@ describe("{{appTitleCaseName}} SDK", function () { () => new {{appShortName}}Api({ storeId: baseConfig.storeId!, - apiHost: baseConfig.apiHost, + apiUrl: baseConfig.apiUrl, credentials: { method: CredentialsMethod.ApiToken as any } @@ -229,7 +241,7 @@ describe("{{appTitleCaseName}} SDK", function () { const fgaApi = new {{appShortName}}Api({ storeId: baseConfig.storeId!, - apiHost: baseConfig.apiHost, + apiUrl: baseConfig.apiUrl, }); expect(scope.isDone()).toBe(false); @@ -247,7 +259,7 @@ describe("{{appTitleCaseName}} SDK", function () { it("should allow updating the storeId after initialization", async () => { const fgaApi = new {{appShortName}}Api({ - apiHost: {{appUpperCaseName}}_API_HOST + apiUrl: {{appUpperCaseName}}_API_URL }); expect(fgaApi.storeId).toBe(undefined); fgaApi.storeId = {{appUpperCaseName}}_STORE_ID; From a59a6226546e1863048ad8e99530fe1c58a566db Mon Sep 17 00:00:00 2001 From: Ewan Harris Date: Fri, 2 Feb 2024 17:46:31 +0000 Subject: [PATCH 2/5] feat(python-sdk): support api_url configuration option --- .../template/README_initializing.mustache | 12 ++-- .../python/template/api_client.mustache | 5 +- .../python/template/api_client_sync.mustache | 5 +- .../clients/python/template/api_test.mustache | 25 +++++++- .../python/template/api_test_sync.mustache | 25 +++++++- .../template/client/configuration.mustache | 3 +- .../template/client/test_client.mustache | 3 +- .../template/client/test_client_sync.mustache | 3 +- .../python/template/configuration.mustache | 57 ++++++++++++++----- .../template/example/example1/example1.py | 7 +-- 10 files changed, 107 insertions(+), 38 deletions(-) diff --git a/config/clients/python/template/README_initializing.mustache b/config/clients/python/template/README_initializing.mustache index d7fe46d7..9e8e067d 100644 --- a/config/clients/python/template/README_initializing.mustache +++ b/config/clients/python/template/README_initializing.mustache @@ -11,8 +11,7 @@ from {{packageName}}.client import OpenFgaClient async def main(): configuration = {{packageName}}.ClientConfiguration( - api_scheme = FGA_API_SCHEME, # optional, defaults to "https" - api_host = FGA_API_HOST, # required, define without the scheme (e.g. api.{{sampleApiDomain}} instead of https://api.{{sampleApiDomain}}) + api_url = FGA_API_URL, # required store_id = FGA_STORE_ID, # optional, not needed when calling `CreateStore` or `ListStores` authorization_model_id = FGA_AUTHORIZATION_MODEL_ID, # Optional, can be overridden per request ) @@ -33,8 +32,7 @@ from {{packageName}}.credentials import Credentials, CredentialConfiguration async def main(): configuration = {{packageName}}.ClientConfiguration( - api_scheme = FGA_API_SCHEME, # optional, defaults to "https" - api_host = FGA_API_HOST, # required, define without the scheme (e.g. api.{{sampleApiDomain}} instead of https://api.{{sampleApiDomain}}) + api_url = FGA_API_URL, # required store_id = FGA_STORE_ID, # optional, not needed when calling `CreateStore` or `ListStores` authorization_model_id = FGA_AUTHORIZATION_MODEL_ID, # Optional, can be overridden per request credentials = Credentials( @@ -61,8 +59,7 @@ from {{packageName}}.credentials import Credentials, CredentialConfiguration async def main(): configuration = {{packageName}}.ClientConfiguration( - api_scheme = FGA_API_SCHEME, # optional, defaults to "https" - api_host = FGA_API_HOST, # required, define without the scheme (e.g. api.{{sampleApiDomain}} instead of https://api.{{sampleApiDomain}}) + api_url = FGA_API_URL, # required store_id = FGA_STORE_ID, # optional, not needed when calling `CreateStore` or `ListStores` authorization_model_id = FGA_AUTHORIZATION_MODEL_ID, # Optional, can be overridden per request credentials = Credentials( @@ -95,8 +92,7 @@ from {{packageName}}.sync import OpenFgaClient def main(): configuration = {{packageName}}.ClientConfiguration( - api_scheme = FGA_API_SCHEME, # optional, defaults to "https" - api_host = FGA_API_HOST, # required, define without the scheme (e.g. api.{{sampleApiDomain}} instead of https://api.{{sampleApiDomain}}) + api_url = FGA_API_URL, # required store_id = FGA_STORE_ID, # optional, not needed when calling `CreateStore` or `ListStores` authorization_model_id = FGA_AUTHORIZATION_MODEL_ID, # optional, can be overridden per request ) diff --git a/config/clients/python/template/api_client.mustache b/config/clients/python/template/api_client.mustache index 03e97858..6818b1cf 100644 --- a/config/clients/python/template/api_client.mustache +++ b/config/clients/python/template/api_client.mustache @@ -203,7 +203,10 @@ class ApiClient(object): # request url if _host is None: - url = self.configuration.api_scheme + '://' + self.configuration.api_host + resource_path + if self.configuration.api_url is not None: + url = self.configuration.api_url + resource_path + else: + url = self.configuration.api_scheme + '://' + self.configuration.api_host + resource_path else: # use server/host defined in path or operation instead url = self.configuration.api_scheme + '://' + _host + resource_path diff --git a/config/clients/python/template/api_client_sync.mustache b/config/clients/python/template/api_client_sync.mustache index 26c2677d..4aefca70 100644 --- a/config/clients/python/template/api_client_sync.mustache +++ b/config/clients/python/template/api_client_sync.mustache @@ -186,7 +186,10 @@ class ApiClient(object): # request url if _host is None: - url = self.configuration.api_scheme + '://' + self.configuration.api_host + resource_path + if self.configuration.api_url is not None: + url = self.configuration.api_url + resource_path + else: + url = self.configuration.api_scheme + '://' + self.configuration.api_host + resource_path else: # use server/host defined in path or operation instead url = self.configuration.api_scheme + '://' + _host + resource_path diff --git a/config/clients/python/template/api_test.mustache b/config/clients/python/template/api_test.mustache index f735242a..e322dddc 100644 --- a/config/clients/python/template/api_test.mustache +++ b/config/clients/python/template/api_test.mustache @@ -86,8 +86,7 @@ class {{#operations}}Test{{classname}}(IsolatedAsyncioTestCase): def setUp(self): self.configuration = {{packageName}}.Configuration( - api_scheme='http', - api_host="api.{{sampleApiDomain}}", + api_url='http://api.{{sampleApiDomain}}', ) def tearDown(self): @@ -900,6 +899,28 @@ class {{#operations}}Test{{classname}}(IsolatedAsyncioTestCase): ) self.assertRaises(FgaValidationException, configuration.is_valid) + def test_url(self): + """ + Ensure that api_url is set and validated + """ + configuration = {{packageName}}.Configuration( + api_url='http://localhost:8080' + ) + self.assertEqual(configuration.api_url, 'http://localhost:8080') + configuration.is_valid() + + def test_url_with_scheme_and_host(self): + """ + Ensure that api_url takes precedence over api_host and scheme + """ + configuration = {{packageName}}.Configuration( + api_url='http://localhost:8080', + api_host='localhost:8080', + api_scheme='foo' + ) + self.assertEqual(configuration.api_url, 'http://localhost:8080') + configuration.is_valid() # Should not throw and complain about scheme being invalid + async def test_bad_configuration_read_authorization_model(self): """ Test whether FgaValidationException is raised for API (reading authorization models) diff --git a/config/clients/python/template/api_test_sync.mustache b/config/clients/python/template/api_test_sync.mustache index 30536f4b..201a74e5 100644 --- a/config/clients/python/template/api_test_sync.mustache +++ b/config/clients/python/template/api_test_sync.mustache @@ -87,8 +87,7 @@ class TestOpenFgaApiSync(IsolatedAsyncioTestCase): def setUp(self): self.configuration = Configuration( - api_scheme='http', - api_host="api.{{sampleApiDomain}}", + api_url='http://api.{{sampleApiDomain}}', ) def tearDown(self): @@ -901,6 +900,28 @@ class TestOpenFgaApiSync(IsolatedAsyncioTestCase): ) self.assertRaises(FgaValidationException, configuration.is_valid) + def test_url(self): + """ + Ensure that api_url is set and validated + """ + configuration = Configuration( + api_url='http://localhost:8080' + ) + self.assertEqual(configuration.api_url, 'http://localhost:8080') + configuration.is_valid() + + def test_url_with_scheme_and_host(self): + """ + Ensure that api_url takes precedence over api_host and scheme + """ + configuration = Configuration( + api_url='http://localhost:8080', + api_host='localhost:8080', + api_scheme='foo' + ) + self.assertEqual(configuration.api_url, 'http://localhost:8080') + configuration.is_valid() # Should not throw and complain about scheme being invalid + async def test_bad_configuration_read_authorization_model(self): """ Test whether FgaValidationException is raised for API (reading authorization models) diff --git a/config/clients/python/template/client/configuration.mustache b/config/clients/python/template/client/configuration.mustache index 8675b50a..08df3de2 100644 --- a/config/clients/python/template/client/configuration.mustache +++ b/config/clients/python/template/client/configuration.mustache @@ -14,13 +14,14 @@ class ClientConfiguration(Configuration): self, api_scheme="https", api_host=None, + api_url=None, store_id=None, credentials=None, retry_params=None, authorization_model_id=None, ssl_ca_cert=None, ): - super().__init__(api_scheme, api_host, store_id, credentials, retry_params, ssl_ca_cert=ssl_ca_cert) + super().__init__(api_scheme, api_host, api_url, store_id, credentials, retry_params, ssl_ca_cert=ssl_ca_cert) self._authorization_model_id = authorization_model_id def is_valid(self): diff --git a/config/clients/python/template/client/test_client.mustache b/config/clients/python/template/client/test_client.mustache index 37391f0d..587853a8 100644 --- a/config/clients/python/template/client/test_client.mustache +++ b/config/clients/python/template/client/test_client.mustache @@ -81,8 +81,7 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): def setUp(self): self.configuration = ClientConfiguration( - api_scheme='http', - api_host="api.{{sampleApiDomain}}", + api_url='http://api.{{sampleApiDomain}}', ) def tearDown(self): diff --git a/config/clients/python/template/client/test_client_sync.mustache b/config/clients/python/template/client/test_client_sync.mustache index 7219403e..d31204f7 100644 --- a/config/clients/python/template/client/test_client_sync.mustache +++ b/config/clients/python/template/client/test_client_sync.mustache @@ -81,8 +81,7 @@ class TestOpenFgaClient(IsolatedAsyncioTestCase): def setUp(self): self.configuration = ClientConfiguration( - api_scheme='http', - api_host="api.{{sampleApiDomain}}", + api_url='http://api.{{sampleApiDomain}}', ) def tearDown(self): diff --git a/config/clients/python/template/configuration.mustache b/config/clients/python/template/configuration.mustache index 23cdbdc7..9b98f52f 100644 --- a/config/clients/python/template/configuration.mustache +++ b/config/clients/python/template/configuration.mustache @@ -75,7 +75,12 @@ class Configuration(object): Do not edit the class manually. :param api_scheme: Whether connection is 'https' or 'http'. Default as 'https' + .. deprecated:: 0.4.1 + Use `api_url` instead. :param api_host: Base url + .. deprecated:: 0.4.1 + Use `api_url` instead. + :param api_url: str - the URL of the FGA server :param store_id: ID of store for API :param credentials: Configuration for obtaining authentication credential :param retry_params: Retry parameters upon HTTP too many request @@ -127,7 +132,7 @@ class Configuration(object): _default = None - def __init__(self, api_scheme="https", api_host=None, + def __init__(self, api_scheme="https", api_host=None, api_url=None, store_id=None, credentials=None, retry_params=None, @@ -141,6 +146,7 @@ class Configuration(object): ): """Constructor """ + self._url = api_url self._scheme = api_scheme self._base_path = api_host self._store_id = store_id @@ -524,24 +530,35 @@ class Configuration(object): Verify the configuration is valid. Note that we are only doing basic validation to ensure input is sane. """ - if self.api_host is None or self.api_host == '': - raise FgaValidationException('api_host is required but not configured.') - if self.api_scheme is None or self.api_scheme == '': - raise FgaValidationException('api_scheme is required but not configured.') - combined_url = self.api_scheme + '://' + self.api_host + combined_url = self.api_url + if self.api_url is None: + if self.api_host is None or self.api_host == '': + raise FgaValidationException('api_host is required but not configured.') + if self.api_scheme is None or self.api_scheme == '': + raise FgaValidationException('api_scheme is required but not configured.') + combined_url = self.api_scheme + '://' + self.api_host parsed_url = None try: parsed_url = urlparse(combined_url) except ValueError: - raise ApiValueError('Either api_scheme `{}` or api_host `{}` is invalid'.format(self.api_scheme, self.api_host)) - if (parsed_url.scheme != 'http' and parsed_url.scheme != 'https'): - raise ApiValueError('api_scheme `{}` must be either `http` or `https`'.format(self.api_scheme)) - if (parsed_url.netloc == ''): - raise ApiValueError('api_host `{}` is invalid'.format(self.api_host)) - if (parsed_url.path != ''): - raise ApiValueError('api_host `{}` is not expected to have path specified'.format(self.api_scheme)) - if (parsed_url.query != ''): - raise ApiValueError('api_host `{}` is not expected to have query specified'.format(self.api_scheme)) + if self.api_url is None: + raise ApiValueError('Either api_scheme `{}` or api_host `{}` is invalid'.format( + self.api_scheme, self.api_host)) + else: + raise ApiValueError('api_url `{}` is invalid'.format( + self.api_url)) + if self.api_url is None: + if (parsed_url.scheme != 'http' and parsed_url.scheme != 'https'): + raise ApiValueError( + 'api_scheme `{}` must be either `http` or `https`'.format(self.api_scheme)) + if (parsed_url.netloc == ''): + raise ApiValueError('api_host `{}` is invalid'.format(self.api_host)) + if (parsed_url.path != ''): + raise ApiValueError( + 'api_host `{}` is not expected to have path specified'.format(self.api_scheme)) + if (parsed_url.query != ''): + raise ApiValueError( + 'api_host `{}` is not expected to have query specified'.format(self.api_scheme)) if self.store_id is not None and self.store_id != "" and is_well_formed_ulid_string(self.store_id) is False: raise FgaValidationException( @@ -570,6 +587,16 @@ class Configuration(object): """Update configured host""" self._base_path = value + @property + def api_url(self): + """Return api_url""" + return self._url + + @api_url.setter + def api_url(self, value): + """Update configured api_url""" + self._url = value + @property def store_id(self): """Return store id.""" diff --git a/config/clients/python/template/example/example1/example1.py b/config/clients/python/template/example/example1/example1.py index c06f3c5e..9d5b35ab 100644 --- a/config/clients/python/template/example/example1/example1.py +++ b/config/clients/python/template/example/example1/example1.py @@ -22,15 +22,14 @@ async def main(): ) ) - if os.getenv('FGA_API_HOST') is not None: + if os.getenv('FGA_API_URL') is not None: configuration = ClientConfiguration( - api_host=os.getenv('FGA_API_HOST'), + api_url=os.getenv('FGA_API_URL'), credentials=credentials ) else: configuration = ClientConfiguration( - api_scheme='http', - api_host='localhost:8080', + api_url='http://localhost:8080', credentials=credentials ) From 7b17ca8880ce07bac381fbad69bb255e73c8cb95 Mon Sep 17 00:00:00 2001 From: Ewan Harris Date: Fri, 2 Feb 2024 18:27:06 +0000 Subject: [PATCH 3/5] chore(python-sdk): clean up and fix imports and initialization in README --- .../template/README_calling_api.mustache | 169 ++++++++++++++++-- .../template/README_initializing.mustache | 73 ++++---- 2 files changed, 189 insertions(+), 53 deletions(-) diff --git a/config/clients/python/template/README_calling_api.mustache b/config/clients/python/template/README_calling_api.mustache index a00e7fe0..866feb0b 100644 --- a/config/clients/python/template/README_calling_api.mustache +++ b/config/clients/python/template/README_calling_api.mustache @@ -7,6 +7,11 @@ Get a paginated list of stores. [API Documentation]({{apiDocsUrl}}/docs/api#/Stores/ListStores) ```python +# from openfga_sdk.sync import OpenFgaClient + +# Initialize the fga_client +# fga_client = OpenFgaClient(configuration) + options = {"page_size": 25, "continuation_token": "eyJwayI6IkxBVEVTVF9OU0NPTkZJR19hdXRoMHN0b3JlIiwic2siOiIxem1qbXF3MWZLZExTcUoyN01MdTdqTjh0cWgifQ=="} response = await fga_client.list_stores(options) # response = ListStoresResponse(...) @@ -21,6 +26,11 @@ Create and initialize a store. [API Documentation]({{apiDocsUrl}}/docs/api#/Stores/CreateStore) ```python +# from openfga_sdk import CreateStoreRequest, OpenFgaClient + +# Initialize the fga_client +# fga_client = OpenFgaClient(configuration) + body = CreateStoreRequest( name = "FGA Demo Store", ) @@ -38,6 +48,11 @@ Get information about the current store. > Requires a client initialized with a storeId ```python +# from openfga_sdk import OpenFgaClient + +# Initialize the fga_client +# fga_client = OpenFgaClient(configuration) + response = await fga_client.get_store() # response = Store({"id": "01FQH7V8BEG3GPQW93KTRFR8JB", "name": "FGA Demo Store", "created_at": "2022-01-01T00:00:00.000Z", "updated_at": "2022-01-01T00:00:00.000Z"}) ``` @@ -52,6 +67,11 @@ Delete a store. > Requires a client initialized with a storeId ```python +# from openfga_sdk import OpenFgaClient + +# Initialize the fga_client +# fga_client = OpenFgaClient(configuration) + response = await fga_client.delete_store() ``` @@ -65,6 +85,11 @@ Read all authorization models in the store. [API Documentation]({{apiDocsUrl}}#/Authorization%20Models/ReadAuthorizationModels) ```python +# from openfga_sdk import OpenFgaClient + +# Initialize the fga_client +# fga_client = OpenFgaClient(configuration) + options = {"page_size": 25, "continuation_token": "eyJwayI6IkxBVEVTVF9OU0NPTkZJR19hdXRoMHN0b3JlIiwic2siOiIxem1qbXF3MWZLZExTcUoyN01MdTdqTjh0cWgifQ=="} response = await fga_client.read_authorization_models(options) # response.authorization_models = [AuthorizationModel(id='01GXSA8YR785C4FYS3C0RTG7B1', schema_version = '1.1', type_definitions=type_definitions[...], AuthorizationModel(id='01GXSBM5PVYHCJNRNKXMB4QZTW', schema_version = '1.1', type_definitions=type_definitions[...])] @@ -84,6 +109,14 @@ Create a new authorization model. > You can use the [{{appTitleCaseName}} Syntax Transformer](https://github.com/openfga/syntax-transformer) to convert between the friendly DSL and the JSON authorization model. ```python +# from openfga_sdk import ( +# Condition, ConditionParamTypeRef, Metadata, ObjectRelation, OpenFgaClient, RelationMetadata, +# RelationReference, TypeDefinition, Userset, Usersets, WriteAuthorizationModelRequest +# ) + +# Initialize the fga_client +# fga_client = OpenFgaClient(configuration) + body = WriteAuthorizationModelRequest( schema_version="1.1", type_definitions=[ @@ -156,6 +189,11 @@ Read a particular authorization model. [API Documentation]({{apiDocsUrl}}#/Authorization%20Models/ReadAuthorizationModel) ```python +# from openfga_sdk import OpenFgaClient + +# Initialize the fga_client +# fga_client = OpenFgaClient(configuration) + options = { # You can rely on the model id set in the configuration or override it for this specific request "authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1" @@ -172,6 +210,18 @@ Reads the latest authorization model (note: this ignores the model id in configu [API Documentation]({{apiDocsUrl}}#/Authorization%20Models/ReadAuthorizationModel) ```python +# from openfga_sdk import ClientConfiguration, OpenFgaClient + +# Create the cofiguration object +# configuration = ClientConfiguration( +# ... +# authorization_model_id = FGA_AUTHORIZATION_MODEL_ID, +# ... +# ) + +# Initialize the fga_client +# fga_client = OpenFgaClient(configuration) + response = await fga_client.read_latest_authorization_model() # response.authorization_model = AuthorizationModel(id='01GXSA8YR785C4FYS3C0RTG7B1', schema_version = '1.1', type_definitions=type_definitions[...]) ``` @@ -186,6 +236,12 @@ Reads the list of historical relationship tuple writes and deletes. [API Documentation]({{apiDocsUrl}}#/Relationship%20Tuples/ReadChanges) ```python +# from openfga_sdk import OpenFgaClient +# from openfga_sdk.client.models import ClientReadChangesRequest + +# Initialize the fga_client +# fga_client = OpenFgaClient(configuration) + options = { # You can rely on the model id set in the configuration or override it for this specific request "page_size": "25", @@ -205,6 +261,11 @@ Reads the relationship tuples stored in the database. It does not evaluate nor e [API Documentation]({{apiDocsUrl}}#/Relationship%20Tuples/Read) ```python +# from openfga_sdk import OpenFgaClient, ReadRequestTupleKey + +# Initialize the fga_client +# fga_client = OpenFgaClient(configuration) + # Find if a relationship tuple stating that a certain user is a viewer of certain document body = ReadRequestTupleKey( user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", @@ -217,6 +278,11 @@ response = await fga_client.read(body) ``` ```python +# from openfga_sdk import OpenFgaClient, ReadRequestTupleKey + +# Initialize the fga_client +# fga_client = OpenFgaClient(configuration) + # Find all relationship tuples where a certain user has a relationship as any relation to a certain document body = ReadRequestTupleKey( user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", @@ -229,6 +295,11 @@ response = await fga_client.read(body) ``` ```python +# from openfga_sdk import OpenFgaClient, ReadRequestTupleKey + +# Initialize the fga_client +# fga_client = OpenFgaClient(configuration) + # Find all relationship tuples where a certain user is a viewer of any document body = ReadRequestTupleKey( user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", @@ -241,6 +312,11 @@ response = await fga_client.read(body) ``` ```python +# from openfga_sdk import OpenFgaClient, ReadRequestTupleKey + +# Initialize the fga_client +# fga_client = OpenFgaClient(configuration) + # Find all relationship tuples where any user has a relationship as any relation with a particular document body = ReadRequestTupleKey( object="document:roadmap", @@ -251,6 +327,11 @@ response = await fga_client.read(body) ``` ```python +# from openfga_sdk import OpenFgaClient, ReadRequestTupleKey + +# Initialize the fga_client +# fga_client = OpenFgaClient(configuration) + # Read all stored relationship tuples body = ReadRequestTupleKey() @@ -269,6 +350,12 @@ Create and/or delete relationship tuples to update the system state. By default, write runs in a transaction mode where any invalid operation (deleting a non-existing tuple, creating an existing tuple, one of the tuples was invalid) or a server error will fail the entire operation. ```python +# from openfga_sdk import OpenFgaClient, RelationshipCondition +# from openfga_sdk.client.models import ClientTuple, ClientWriteRequest + +# Initialize the fga_client +# fga_client = OpenFgaClient(configuration) + options = { # You can rely on the model id set in the configuration or override it for this specific request "authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1" @@ -312,6 +399,12 @@ Convenience `write_tuples` and `delete_tuples` methods are also available. The SDK will split the writes into separate requests and send them sequentially to avoid violating rate limits. ```python +# from openfga_sdk import OpenFgaClient, RelationshipCondition +# from openfga_sdk.client.models import ClientTuple, ClientWriteRequest, WriteTransactionOpts + +# Initialize the fga_client +# fga_client = OpenFgaClient(configuration) + options = { # You can rely on the model id set in the configuration or override it for this specific request "authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1", @@ -362,6 +455,12 @@ Check if a user has a particular relation with an object. [API Documentation]({{apiDocsUrl}}#/Relationship%20Queries/Check) ```python +# from openfga_sdk import OpenFgaClient +# from openfga_sdk.client import ClientCheckRequest + +# Initialize the fga_client +# fga_client = OpenFgaClient(configuration) + options = { # You can rely on the model id set in the configuration or override it for this specific request "authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1" @@ -386,6 +485,13 @@ Run a set of [checks](#check). Batch Check will return `allowed: false` if it en If 429s or 5xxs are encountered, the underlying check will retry up to {{defaultMaxRetry}} times before giving up. ```python +# from openfga_sdk import OpenFgaClient +# from openfga_sdk.client import ClientCheckRequest +# from openfga_sdk.client.models import ClientTuple + +# Initialize the fga_client +# fga_client = OpenFgaClient(configuration) + options = { # You can rely on the model id set in the configuration or override it for this specific request "authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1" @@ -394,7 +500,7 @@ body = [ClientCheckRequest( user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", relation="viewer", object="document:roadmap", - contextual_tuples=[ # optional + contextual_tuples=[ # optional ClientTuple( user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", relation="editor", @@ -408,7 +514,7 @@ body = [ClientCheckRequest( user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", relation="admin", object="document:roadmap", - contextual_tuples=[ # optional + contextual_tuples=[ # optional ClientTuple( user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", relation="editor", @@ -478,6 +584,12 @@ Expands the relationships in userset tree format. [API Documentation]({{apiDocsUrl}}#/Relationship%20Queries/Expand) ```python +# from openfga_sdk import OpenFgaClient +# from openfga_sdk.client.models import ClientExpandRequest + +# Initialize the fga_client +# fga_client = OpenFgaClient(configuration) + options = { # You can rely on the model id set in the configuration or override it for this specific request "authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1" @@ -499,6 +611,12 @@ List the objects of a particular type a user has access to. [API Documentation]({{apiDocsUrl}}#/Relationship%20Queries/ListObjects) ```python +# from openfga_sdk import OpenFgaClient +# from openfga_sdk.client.models import ClientListObjectsRequest, ClientTuple + +# Initialize the fga_client +# fga_client = OpenFgaClient(configuration) + options = { # You can rely on the model id set in the configuration or override it for this specific request "authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1" @@ -507,7 +625,7 @@ body = ClientListObjectsRequest( user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", relation="viewer", type="document", - contextual_tuples=[ # optional + contextual_tuples=[ # optional ClientTuple( user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", relation="writer", @@ -528,15 +646,21 @@ response = await fga_client.list_objects(body) List the relations a user has on an object. ```python +# from openfga_sdk import OpenFgaClient +# from openfga_sdk.client.models import ClientListRelationsRequest + +# Initialize the fga_client +# fga_client = OpenFgaClient(configuration) + options = { # You can rely on the model id set in the configuration or override it for this specific request "authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1" } body = ClientListRelationsRequest( - user = "user:81684243-9356-4421-8fbf-a4f8d36aa31b", - object = "document:roadmap", - relations = ["can_view", "can_edit", "can_delete", "can_rename"], - contextual_tuples=[ # optional + user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", + object="document:roadmap", + relations=["can_view", "can_edit", "can_delete", "can_rename"], + contextual_tuples=[ # optional ClientTuple( user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", relation="writer", @@ -547,7 +671,8 @@ body = ClientListRelationsRequest( ViewCount=100 ) ) -var response = await fga_client.list_relations(body, options); + +response = await fga_client.list_relations(body, options) # response.relations = ["can_view", "can_edit"] ``` @@ -561,11 +686,17 @@ Read assertions for a particular authorization model. [API Documentation]({{apiDocsUrl}}#/Assertions/Read%20Assertions) ```python +# from openfga_sdk import OpenFgaClient + +# Initialize the fga_client +# fga_client = OpenFgaClient(configuration) + options = { # You can rely on the model id set in the configuration or override it for this specific request "authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1" } -response = await fga_client.read_assertions(options); + +response = await fga_client.read_assertions(options) ``` ##### Write Assertions @@ -575,15 +706,23 @@ Update the assertions for a particular authorization model. [API Documentation]({{apiDocsUrl}}#/Assertions/Write%20Assertions) ```python +# from openfga_sdk import OpenFgaClient +# from openfga_sdk.client.models import ClientAssertion + +# Initialize the fga_client +# fga_client = OpenFgaClient(configuration) + options = { # You can rely on the model id set in the configuration or override it for this specific request "authorization_model_id": "01GXSA8YR785C4FYS3C0RTG7B1" } + body = [ClientAssertion( - user = "user:81684243-9356-4421-8fbf-a4f8d36aa31b", - relation = "viewer", - object = "document:roadmap", - expectation = true, -)]; -response = await fga_client.write_assertions(body, options); + user="user:81684243-9356-4421-8fbf-a4f8d36aa31b", + relation="viewer", + object="document:roadmap", + expectation=True, +)] + +response = await fga_client.write_assertions(body, options) ``` diff --git a/config/clients/python/template/README_initializing.mustache b/config/clients/python/template/README_initializing.mustache index 9e8e067d..6652df98 100644 --- a/config/clients/python/template/README_initializing.mustache +++ b/config/clients/python/template/README_initializing.mustache @@ -5,40 +5,38 @@ The documentation below refers to the `{{appShortName}}Client`, to read the docu #### No Credentials ```python -import {{packageName}} -from {{packageName}}.client import OpenFgaClient +from {{packageName}} import ClientConfiguration, OpenFgaClient async def main(): - configuration = {{packageName}}.ClientConfiguration( - api_url = FGA_API_URL, # required - store_id = FGA_STORE_ID, # optional, not needed when calling `CreateStore` or `ListStores` - authorization_model_id = FGA_AUTHORIZATION_MODEL_ID, # Optional, can be overridden per request + configuration = ClientConfiguration( + api_url=FGA_API_URL, # required + store_id=FGA_STORE_ID, # optional, not needed when calling `CreateStore` or `ListStores` + authorization_model_id=FGA_AUTHORIZATION_MODEL_ID, # Optional, can be overridden per request ) # Enter a context with an instance of the OpenFgaClient async with OpenFgaClient(configuration) as fga_client: api_response = await fga_client.read_authorization_models() await fga_client.close() - + return api_response ``` #### API Token ```python -import {{packageName}} -from {{packageName}}.client import OpenFgaClient -from {{packageName}}.credentials import Credentials, CredentialConfiguration +from {{packageName}} import ClientConfiguration, OpenFgaClient +from {{packageName}}.credentials import CredentialConfiguration, Credentials async def main(): - configuration = {{packageName}}.ClientConfiguration( - api_url = FGA_API_URL, # required - store_id = FGA_STORE_ID, # optional, not needed when calling `CreateStore` or `ListStores` - authorization_model_id = FGA_AUTHORIZATION_MODEL_ID, # Optional, can be overridden per request - credentials = Credentials( + configuration = ClientConfiguration( + api_url=FGA_API_URL, # required + store_id=FGA_STORE_ID, # optional, not needed when calling `CreateStore` or `ListStores` + authorization_model_id=FGA_AUTHORIZATION_MODEL_ID, # Optional, can be overridden per request + credentials=Credentials( method='api_token', - configuration = CredentialConfiguration( - api_token = FGA_API_TOKEN, + configuration=CredentialConfiguration( + api_token=FGA_API_TOKEN, ) ) ) @@ -46,29 +44,28 @@ async def main(): async with OpenFgaClient(configuration) as fga_client: api_response = await fga_client.read_authorization_models() await fga_client.close() - + return api_response ``` #### Client Credentials ```python -import {{packageName}} -from {{packageName}}.client import OpenFgaClient +from {{packageName}} import ClientConfiguration, OpenFgaClient from {{packageName}}.credentials import Credentials, CredentialConfiguration async def main(): - configuration = {{packageName}}.ClientConfiguration( - api_url = FGA_API_URL, # required - store_id = FGA_STORE_ID, # optional, not needed when calling `CreateStore` or `ListStores` - authorization_model_id = FGA_AUTHORIZATION_MODEL_ID, # Optional, can be overridden per request - credentials = Credentials( - method = 'client_credentials', - configuration = CredentialConfiguration( - api_issuer = FGA_API_TOKEN_ISSUER, - api_audience = FGA_API_AUDIENCE, - client_id = FGA_CLIENT_ID, - client_secret = FGA_CLIENT_SECRET, + configuration = ClientConfiguration( + api_url=FGA_API_URL, # required + store_id=FGA_STORE_ID, # optional, not needed when calling `CreateStore` or `ListStores` + authorization_model_id=FGA_AUTHORIZATION_MODEL_ID, # Optional, can be overridden per request + credentials=Credentials( + method='client_credentials', + configuration=CredentialConfiguration( + api_issuer=FGA_API_TOKEN_ISSUER, + api_audience=FGA_API_AUDIENCE, + client_id=FGA_CLIENT_ID, + client_secret=FGA_CLIENT_SECRET, ) ) ) @@ -76,7 +73,7 @@ async def main(): async with OpenFgaClient(configuration) as fga_client: api_response = await fga_client.read_authorization_models() await fga_client.close() - + return api_response ``` #### Synchronous Client @@ -86,17 +83,17 @@ from `openfga_sdk.sync` that supports all the credential types and calls, without requiring async/await. ```python -import {{packageName}} -from {{packageName}}.sync import OpenFgaClient +from {{packageName}}.sync import ClientConfiguration, OpenFgaClient def main(): - configuration = {{packageName}}.ClientConfiguration( - api_url = FGA_API_URL, # required - store_id = FGA_STORE_ID, # optional, not needed when calling `CreateStore` or `ListStores` - authorization_model_id = FGA_AUTHORIZATION_MODEL_ID, # optional, can be overridden per request + configuration = ClientConfiguration( + api_url=FGA_API_URL, # required + store_id=FGA_STORE_ID, # optional, not needed when calling `CreateStore` or `ListStores` + authorization_model_id=FGA_AUTHORIZATION_MODEL_ID, # optional, can be overridden per request ) # Enter a context with an instance of the OpenFgaClient with OpenFgaClient(configuration) as fga_client: api_response = fga_client.read_authorization_models() + return api_response ``` From 9ad45dc73c895d6f47060852bc42994ff3ce025e Mon Sep 17 00:00:00 2001 From: Ewan Harris Date: Tue, 6 Feb 2024 12:06:28 +0000 Subject: [PATCH 4/5] docs(js-sdk): improve docstring --- config/clients/js/template/configuration.mustache | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/clients/js/template/configuration.mustache b/config/clients/js/template/configuration.mustache index b9c5c57c..3e677465 100644 --- a/config/clients/js/template/configuration.mustache +++ b/config/clients/js/template/configuration.mustache @@ -66,7 +66,7 @@ export class Configuration { private static sdkVersion = "{{packageVersion}}"; /** - * provide the full api URL + * provide the full api URL (e.g. `https://api.{{sampleApiDomain}}`) * * @type {string} * @memberof Configuration From 87c5fa862915ed2851cd877658fc280ab1e370a4 Mon Sep 17 00:00:00 2001 From: Ewan Harris Date: Tue, 6 Feb 2024 12:08:43 +0000 Subject: [PATCH 5/5] fix(python-sdk): correct position for new argument --- config/clients/python/template/client/configuration.mustache | 4 ++-- config/clients/python/template/configuration.mustache | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/config/clients/python/template/client/configuration.mustache b/config/clients/python/template/client/configuration.mustache index 08df3de2..155cb9d8 100644 --- a/config/clients/python/template/client/configuration.mustache +++ b/config/clients/python/template/client/configuration.mustache @@ -14,14 +14,14 @@ class ClientConfiguration(Configuration): self, api_scheme="https", api_host=None, - api_url=None, store_id=None, credentials=None, retry_params=None, authorization_model_id=None, ssl_ca_cert=None, + api_url=None, # TODO: restructure when removing api_scheme/api_host ): - super().__init__(api_scheme, api_host, api_url, store_id, credentials, retry_params, ssl_ca_cert=ssl_ca_cert) + super().__init__(api_scheme, api_host, store_id, credentials, retry_params, ssl_ca_cert=ssl_ca_cert, api_url=api_url) self._authorization_model_id = authorization_model_id def is_valid(self): diff --git a/config/clients/python/template/configuration.mustache b/config/clients/python/template/configuration.mustache index 9b98f52f..e8150f7f 100644 --- a/config/clients/python/template/configuration.mustache +++ b/config/clients/python/template/configuration.mustache @@ -80,7 +80,6 @@ class Configuration(object): :param api_host: Base url .. deprecated:: 0.4.1 Use `api_url` instead. - :param api_url: str - the URL of the FGA server :param store_id: ID of store for API :param credentials: Configuration for obtaining authentication credential :param retry_params: Retry parameters upon HTTP too many request @@ -128,11 +127,12 @@ class Configuration(object): The validation of enums is performed for variables with defined enum values before. :param ssl_ca_cert: str - the path to a file of concatenated CA certificates in PEM format + :param api_url: str - the URL of the FGA server """ _default = None - def __init__(self, api_scheme="https", api_host=None, api_url=None, + def __init__(self, api_scheme="https", api_host=None, store_id=None, credentials=None, retry_params=None, @@ -143,6 +143,7 @@ class Configuration(object): server_index=None, server_variables=None, server_operation_index=None, server_operation_variables=None, ssl_ca_cert=None, + api_url=None, # TODO: restructure when removing api_scheme/api_host ): """Constructor """