From 8010bef48ff3892e71ec2f79c0ba33737e92ed43 Mon Sep 17 00:00:00 2001 From: Andrew Helsby Date: Thu, 12 May 2022 16:11:40 +0400 Subject: [PATCH] python-sdk: Work in progress initial commit of a python sdk --- .gitignore | 2 +- LICENSE | 201 +++++++++ requirements-dev.in | 11 + requirements-dev.txt | 124 ++++++ requirements.in | 1 + requirements.txt | 18 + src/__init__.py | 0 src/client.py | 383 ++++++++++++++++++ src/evaluation-context/__init__.py | 0 src/evaluation-context/evaluation_context.py | 0 src/flag-evaluation/__init__.py | 0 src/flag-evaluation/flag_evaluation.py | 0 src/flag-evaluation/hooks.py | 0 src/open_feature.py | 20 + src/provider/__init__.py | 0 src/provider/flagsmith/__init__.py | 0 src/provider/flagsmith/flagsmith_provider.py | 31 ++ src/provider/provider.py | 92 +++++ tests/__init__.py | 0 tests/provider/__init__.py | 0 tests/provider/flagsmith/__init__.py | 0 .../flagsmith/test_flagsmith_provider.py | 19 + 22 files changed, 901 insertions(+), 1 deletion(-) create mode 100644 LICENSE create mode 100644 requirements-dev.in create mode 100644 requirements-dev.txt create mode 100644 requirements.in create mode 100644 requirements.txt create mode 100644 src/__init__.py create mode 100644 src/client.py create mode 100644 src/evaluation-context/__init__.py create mode 100644 src/evaluation-context/evaluation_context.py create mode 100644 src/flag-evaluation/__init__.py create mode 100644 src/flag-evaluation/flag_evaluation.py create mode 100644 src/flag-evaluation/hooks.py create mode 100644 src/open_feature.py create mode 100644 src/provider/__init__.py create mode 100644 src/provider/flagsmith/__init__.py create mode 100644 src/provider/flagsmith/flagsmith_provider.py create mode 100644 src/provider/provider.py create mode 100644 tests/__init__.py create mode 100644 tests/provider/__init__.py create mode 100644 tests/provider/flagsmith/__init__.py create mode 100644 tests/provider/flagsmith/test_flagsmith_provider.py diff --git a/.gitignore b/.gitignore index 88315da..5abdfe0 100644 --- a/.gitignore +++ b/.gitignore @@ -11,7 +11,6 @@ build/ develop-eggs/ dist/ eggs/ -lib/ lib64/ parts/ sdist/ @@ -38,6 +37,7 @@ coverage.xml .mr.developer.cfg .project .pydevproject +.idea # Rope .ropeproject diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..989e2c5 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/requirements-dev.in b/requirements-dev.in new file mode 100644 index 0000000..741af96 --- /dev/null +++ b/requirements-dev.in @@ -0,0 +1,11 @@ +pylint +pep8 +autopep8 +pytest +pytest-mock +black +pip-tools +responses +pre-commit +flake8 +pytest-mock \ No newline at end of file diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..9a10979 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,124 @@ +# +# This file is autogenerated by pip-compile with python 3.9 +# To update, run: +# +# pip-compile requirements-dev.in +# +astroid==2.11.5 + # via pylint +attrs==21.4.0 + # via pytest +autopep8==1.6.0 + # via -r requirements-dev.in +black==22.3.0 + # via -r requirements-dev.in +certifi==2021.10.8 + # via requests +cfgv==3.3.1 + # via pre-commit +charset-normalizer==2.0.12 + # via requests +click==8.1.3 + # via + # black + # pip-tools +dill==0.3.4 + # via pylint +distlib==0.3.4 + # via virtualenv +filelock==3.6.0 + # via virtualenv +flake8==4.0.1 + # via -r requirements-dev.in +identify==2.5.0 + # via pre-commit +idna==3.3 + # via requests +iniconfig==1.1.1 + # via pytest +isort==5.10.1 + # via pylint +lazy-object-proxy==1.7.1 + # via astroid +mccabe==0.6.1 + # via + # flake8 + # pylint +mypy-extensions==0.4.3 + # via black +nodeenv==1.6.0 + # via pre-commit +packaging==21.3 + # via pytest +pathspec==0.9.0 + # via black +pep517==0.12.0 + # via pip-tools +pep8==1.7.1 + # via -r requirements-dev.in +pip-tools==6.6.0 + # via -r requirements-dev.in +platformdirs==2.5.2 + # via + # black + # pylint + # virtualenv +pluggy==1.0.0 + # via pytest +pre-commit==2.19.0 + # via -r requirements-dev.in +py==1.11.0 + # via pytest +pycodestyle==2.8.0 + # via + # autopep8 + # flake8 +pyflakes==2.4.0 + # via flake8 +pylint==2.13.8 + # via -r requirements-dev.in +pyparsing==3.0.9 + # via packaging +pytest==7.1.2 + # via + # -r requirements-dev.in + # pytest-mock +pytest-mock==3.7.0 + # via -r requirements-dev.in +pyyaml==6.0 + # via pre-commit +requests==2.27.1 + # via responses +responses==0.20.0 + # via -r requirements-dev.in +six==1.16.0 + # via virtualenv +toml==0.10.2 + # via + # autopep8 + # pre-commit +tomli==2.0.1 + # via + # black + # pep517 + # pylint + # pytest +typing-extensions==4.2.0 + # via + # astroid + # black + # pylint +urllib3==1.26.9 + # via + # requests + # responses +virtualenv==20.14.1 + # via pre-commit +wheel==0.37.1 + # via pip-tools +wrapt==1.14.1 + # via astroid + +# The following packages are considered to be unsafe in a requirements file: +# pip +# setuptools diff --git a/requirements.in b/requirements.in new file mode 100644 index 0000000..acdd9ad --- /dev/null +++ b/requirements.in @@ -0,0 +1 @@ +flagsmith \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..614dfa8 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,18 @@ +# +# This file is autogenerated by pip-compile with python 3.9 +# To update, run: +# +# pip-compile +# +certifi==2021.10.8 + # via requests +charset-normalizer==2.0.12 + # via requests +flagsmith==2.0.1 + # via -r requirements.in +idna==3.3 + # via requests +requests==2.27.1 + # via flagsmith +urllib3==1.26.9 + # via requests diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/client.py b/src/client.py new file mode 100644 index 0000000..c60e7c6 --- /dev/null +++ b/src/client.py @@ -0,0 +1,383 @@ +from enum import Enum +from numbers import Number + +from src.provider.provider import AbstractProvider +from open_feature import OpenFeatureProvider, OpenFeature + + +class FlagType(Enum): + BOOLEAN = 1 + STRING = 2 + NUMBER = 3 + + +class OpenFeatureClient: + def __init__(self, name: str, version: str, context: Context, hooks: list = []): + self.name = name + self.version = version + self.context = context + self.hooks = hooks + + def get_boolean_value( + self, key: str, defaultValue: bool, evaluationContext, flagEvaluationOptions + ) -> bool: + return self.evaluate_flag( + FlagType.BOOLEAN, + key, + defaultValue, + evaluationContext, + flagEvaluationOptions, + ) + + def get_boolean_details( + self, key: str, defaultValue: bool, evaluationContext, flagEvaluationOptions + ): + return self.get_boolean_details( + key, defaultValue, evaluationContext, flagEvaluationOptions + ) + + def get_string_value( + self, key: str, defaultValue: bool, evaluationContext, flagEvaluationOptions + ) -> str: + return self.evaluate_flag( + FlagType.BOOLEAN, + key, + defaultValue, + evaluationContext, + flagEvaluationOptions, + ) + + def get_string_details( + self, key: str, defaultValue: bool, evaluationContext, flagEvaluationOptions + ): + return self.get_string_details( + key, defaultValue, evaluationContext, flagEvaluationOptions + ) + + def get_number_value( + self, key: str, defaultValue: bool, evaluationContext, flagEvaluationOptions + ) -> Number: + return self.evaluate_flag( + FlagType.BOOLEAN, + key, + defaultValue, + evaluationContext, + flagEvaluationOptions, + ) + + def get_number_details( + self, key: str, defaultValue: bool, evaluationContext, flagEvaluationOptions + ): + return self.get_number_details( + key, defaultValue, evaluationContext, flagEvaluationOptions + ) + + def evaluate_flag( + self, + flag_type: FlagType, + key: str, + defaultValue: bool, + evaluationContext, + flagEvaluationOptions, + ): + provider = OpenFeature.get_provider() + if flag_type is FlagType.BOOLEAN: + return provider.getBooleanEvaluation( + key, defaultValue, evaluationContext, flagEvaluationOptions + ) + if flag_type is FlagType.NUMBER: + return provider.getNumberEvaluation( + key, defaultValue, evaluationContext, flagEvaluationOptions + ) + if flag_type is FlagType.STRING: + return provider.getStringEvaluation( + key, defaultValue, evaluationContext, flagEvaluationOptions + ) + + +# async getBooleanValue( +# flagKey: string, +# defaultValue: boolean, +# context?: Context, +# options?: FlagEvaluationOptions +# ): Promise { +# return ( +# await this.getBooleanDetails(flagKey, defaultValue, context, options) +# ).value; +# } +# +# getBooleanDetails( +# flagKey: string, +# defaultValue: boolean, +# context?: Context, +# options?: FlagEvaluationOptions +# ): Promise> { +# return this.evaluateFlag( +# 'boolean', +# flagKey, +# defaultValue, +# context, +# options +# ); +# } +# +# async getStringValue( +# flagKey: string, +# defaultValue: string, +# context?: Context, +# options?: FlagEvaluationOptions +# ): Promise { +# return ( +# await this.getStringDetails(flagKey, defaultValue, context, options) +# ).value; +# } +# +# getStringDetails( +# flagKey: string, +# defaultValue: string, +# context?: Context, +# options?: FlagEvaluationOptions +# ): Promise> { +# return this.evaluateFlag('string', flagKey, defaultValue, context, options); +# } +# +# async getNumberValue( +# flagKey: string, +# defaultValue: number, +# context?: Context, +# options?: FlagEvaluationOptions +# ): Promise { +# return ( +# await this.getNumberDetails(flagKey, defaultValue, context, options) +# ).value; +# } +# +# getNumberDetails( +# flagKey: string, +# defaultValue: number, +# context?: Context, +# options?: FlagEvaluationOptions +# ): Promise> { +# return this.evaluateFlag('number', flagKey, defaultValue, context, options); +# } +# +# async getObjectValue( +# flagKey: string, +# defaultValue: T, +# context?: Context, +# options?: FlagEvaluationOptions +# ): Promise { +# return ( +# await this.getObjectDetails(flagKey, defaultValue, context, options) +# ).value; +# } +# +# getObjectDetails( +# flagKey: string, +# defaultValue: T, +# context?: Context, +# options?: FlagEvaluationOptions +# ): Promise> { +# return this.evaluateFlag('json', flagKey, defaultValue, context, options); +# } +# +# private async evaluateFlag( +# flagValueType: FlagValueType, +# flagKey: string, +# defaultValue: T, +# context: Context | undefined, +# options?: FlagEvaluationOptions +# ): Promise> { +# const provider = this.getProvider(); +# const flagHooks = options?.hooks ?? []; +# const allHooks: Hook[] = [ +# ...OpenFeatureAPI.getInstance().hooks, +# ...this.hooks, +# ...flagHooks, +# ]; +# // merge client context with evaluation context +# const mergedContext = { +# ...this.context, +# ...this.getTransactionContext(), +# ...context, +# }; +# +# // this object reference must not change over the course of flag evaluation +# const hookContext: HookContext = { +# flagKey, +# flagValueType, +# defaultValue, +# context: mergedContext, +# client: this, +# provider, +# executedHooks: { +# after: [], +# before: [], +# error: [], +# finally: [], +# }, +# }; +# let evaluationDetailsPromise: Promise>; +# +# try { +# this.beforeEvaluation(allHooks, hookContext); +# +# // if a transformer is defined, run it to prepare the context. +# const transformedContext = +# typeof provider.contextTransformer === 'function' +# ? await provider.contextTransformer(mergedContext) +# : mergedContext; +# switch (flagValueType) { +# case 'boolean': { +# evaluationDetailsPromise = provider.getBooleanEvaluation( +# flagKey, +# defaultValue as boolean, +# transformedContext, +# options +# ); +# break; +# } +# case 'string': { +# evaluationDetailsPromise = provider.getStringEvaluation( +# flagKey, +# defaultValue as string, +# transformedContext, +# options +# ); +# break; +# } +# case 'number': { +# evaluationDetailsPromise = provider.getNumberEvaluation( +# flagKey, +# defaultValue as number, +# transformedContext, +# options +# ); +# break; +# } +# case 'json': { +# evaluationDetailsPromise = provider.getObjectEvaluation( +# flagKey, +# defaultValue as object, +# transformedContext, +# options +# ); +# break; +# } +# default: { +# throw new GeneralError('Unknown flag type'); +# } +# } +# +# const evaluationDetails = await evaluationDetailsPromise; +# return { +# ...evaluationDetails, +# value: this.afterEvaluation( +# allHooks, +# hookContext, +# evaluationDetails +# ) as T, +# flagKey, +# executedHooks: hookContext.executedHooks, +# }; +# } catch (err) { +# if (this.isError(err)) { +# this.errorEvaluation(allHooks, hookContext, err); +# } +# return { +# flagKey, +# executedHooks: hookContext.executedHooks, +# value: defaultValue, +# reason: Reason.ERROR, +# }; +# } finally { +# this.finallyEvaluation(allHooks, hookContext); +# } +# } +# +# getTransactionContext(): Context { +# return openfeature.getTransactionContext(); +# } +# +# private beforeEvaluation(allHooks: Hook[], hookContext: HookContext) { +# const mergedContext = allHooks.reduce( +# (accumulated: Context, hook: Hook): Context => { +# if (typeof hook?.before === 'function') { +# hookContext.executedHooks.before.push(hook.name); +# return { +# ...accumulated, +# ...hook.before(hookContext), +# }; +# } +# return accumulated; +# }, +# hookContext.context +# ); +# hookContext.context = mergedContext; +# } +# +# private afterEvaluation( +# allHooks: Hook[], +# hookContext: HookContext, +# evaluationDetails: ProviderEvaluation +# ): FlagValue { +# return allHooks.reduce((accumulated: FlagValue, hook) => { +# if (typeof hook?.after === 'function') { +# hookContext.executedHooks.after.push(hook.name); +# return ( +# hook.after(hookContext, { +# ...evaluationDetails, +# flagKey: hookContext.flagKey, +# executedHooks: hookContext.executedHooks, +# }) ?? accumulated +# ); +# } +# return accumulated; +# }, evaluationDetails.value); +# } +# +# private finallyEvaluation(allHooks: Hook[], hookContext: HookContext): void { +# allHooks.forEach((hook) => { +# if (typeof hook?.finally === 'function') { +# hookContext.executedHooks.finally.push(hook.name); +# return hook.finally(hookContext); +# } +# }); +# } +# +# private errorEvaluation( +# allHooks: Hook[], +# hookContext: HookContext, +# err: Error +# ): void { +# // Workaround for error scoping issue +# const error = err; +# allHooks.forEach((hook) => { +# if (typeof hook?.error === 'function') { +# hookContext.executedHooks.error.push(hook.name); +# return hook.error(hookContext, error); +# } +# }); +# } +# +# private getAllHooks(options: FlagEvaluationOptions | undefined) { +# const flagHooks = options?.hooks ?? []; +# const allHooks: Hook[] = [ +# ...OpenFeatureAPI.getInstance().hooks, +# ...this.hooks, +# ...flagHooks, +# ]; +# return allHooks; +# } +# +# private getProvider(): FeatureProvider { +# return (this.api.getProvider() ?? +# NOOP_FEATURE_PROVIDER) as FeatureProvider; +# } +# +# private isError(err: unknown): err is Error { +# return ( +# (err as Error).stack !== undefined && (err as Error).message !== undefined +# ); +# } +# } diff --git a/src/evaluation-context/__init__.py b/src/evaluation-context/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/evaluation-context/evaluation_context.py b/src/evaluation-context/evaluation_context.py new file mode 100644 index 0000000..e69de29 diff --git a/src/flag-evaluation/__init__.py b/src/flag-evaluation/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/flag-evaluation/flag_evaluation.py b/src/flag-evaluation/flag_evaluation.py new file mode 100644 index 0000000..e69de29 diff --git a/src/flag-evaluation/hooks.py b/src/flag-evaluation/hooks.py new file mode 100644 index 0000000..e69de29 diff --git a/src/open_feature.py b/src/open_feature.py new file mode 100644 index 0000000..32dc2e3 --- /dev/null +++ b/src/open_feature.py @@ -0,0 +1,20 @@ +from src.provider.provider import AbstractProvider + +global provider + + +class OpenFeature: + def __init__(self): + pass + + @staticmethod + def set_provider(provider_type: AbstractProvider): + if provider_type is None: + print("No provider") + global provider + provider = provider_type + + @staticmethod + def get_provider() -> AbstractProvider: + global provider + return provider diff --git a/src/provider/__init__.py b/src/provider/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/provider/flagsmith/__init__.py b/src/provider/flagsmith/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/provider/flagsmith/flagsmith_provider.py b/src/provider/flagsmith/flagsmith_provider.py new file mode 100644 index 0000000..2296ea3 --- /dev/null +++ b/src/provider/flagsmith/flagsmith_provider.py @@ -0,0 +1,31 @@ +from numbers import Number + +import flagsmith as Flagsmith + +from src.provider.provider import AbstractProvider + + +class FlagsmithProvider(AbstractProvider): + def __init__(self, flagsmith: Flagsmith = None): + if flagsmith is None: + self.flagsmith_provider = Flagsmith(environment_id="") + else: + self.flagsmith_provider = flagsmith + + def get_boolean_value(self, key: str, defaultValue: bool) -> bool: + value = self.flagsmith_provider.get_value(key) + + def get_boolean_details(self, key: str, defaultValue: bool): + pass + + def get_string_value(self, key: str, defaultValue: bool) -> str: + value = self.flagsmith_provider.get_value(key) + + def get_string_details(self, key: str, defaultValue: bool): + pass + + def get_number_value(self, key: str, defaultValue: bool) -> Number: + value = self.flagsmith_provider.get_value(key) + + def get_number_details(self, key: str, defaultValue: bool): + pass diff --git a/src/provider/provider.py b/src/provider/provider.py new file mode 100644 index 0000000..88c6726 --- /dev/null +++ b/src/provider/provider.py @@ -0,0 +1,92 @@ +from abc import abstractmethod +from numbers import Number + + +class AbstractProvider: + @abstractmethod + def get_boolean_value(self, key: str, defaultValue: bool) -> bool: + pass + + @abstractmethod + def get_boolean_value( + self, key: str, defaultValue: bool, evaluationContext + ) -> bool: + pass + + @abstractmethod + def get_boolean_value( + self, key: str, defaultValue: bool, evaluationContext, flagEvaluationOptions + ) -> bool: + pass + + @abstractmethod + def get_boolean_details(self, key: str, defaultValue: bool): + pass + + @abstractmethod + def get_boolean_details(self, key: str, defaultValue: bool, evaluationContext): + pass + + @abstractmethod + def get_boolean_details( + self, key: str, defaultValue: bool, evaluationContext, flagEvaluationOptions + ): + pass + + @abstractmethod + def get_string_value(self, key: str, defaultValue: bool) -> str: + pass + + @abstractmethod + def get_string_value(self, key: str, defaultValue: bool, evaluationContext) -> str: + pass + + @abstractmethod + def get_string_value( + self, key: str, defaultValue: bool, evaluationContext, flagEvaluationOptions + ) -> str: + pass + + @abstractmethod + def get_string_details(self, key: str, defaultValue: bool): + pass + + @abstractmethod + def get_string_details(self, key: str, defaultValue: bool, evaluationContext): + pass + + @abstractmethod + def get_string_details( + self, key: str, defaultValue: bool, evaluationContext, flagEvaluationOptions + ): + pass + + @abstractmethod + def get_number_value(self, key: str, defaultValue: bool) -> Number: + pass + + @abstractmethod + def get_number_value( + self, key: str, defaultValue: bool, evaluationContext + ) -> Number: + pass + + @abstractmethod + def get_number_value( + self, key: str, defaultValue: bool, evaluationContext, flagEvaluationOptions + ) -> Number: + pass + + @abstractmethod + def get_number_details(self, key: str, defaultValue: bool): + pass + + @abstractmethod + def get_number_details(self, key: str, defaultValue: bool, evaluationContext): + pass + + @abstractmethod + def get_number_details( + self, key: str, defaultValue: bool, evaluationContext, flagEvaluationOptions + ): + pass diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/provider/__init__.py b/tests/provider/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/provider/flagsmith/__init__.py b/tests/provider/flagsmith/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/provider/flagsmith/test_flagsmith_provider.py b/tests/provider/flagsmith/test_flagsmith_provider.py new file mode 100644 index 0000000..e216bed --- /dev/null +++ b/tests/provider/flagsmith/test_flagsmith_provider.py @@ -0,0 +1,19 @@ +from unittest.mock import Mock + +from src.open_feature import OpenFeatureProvider +from src.provider.flagsmith.flagsmith_provider import FlagsmithProvider + + +def setUp(): + OpenFeatureProvider.set_provider(FlagsmithProvider(flagsmith=Mock())) + provider = OpenFeatureProvider.get_provider() + assert isinstance(provider, FlagsmithProvider) + + +def test_should_get_boolean_flag_from_flagsmith(): + # Given + provider = OpenFeatureProvider.get_provider() + # When + flag = provider.get_boolean_value("Key", False) + # Then + assert flag