From ec0943b586ab811b079842757481927e3ad7a9ff Mon Sep 17 00:00:00 2001 From: parvez alam Date: Sun, 21 Feb 2021 21:00:13 +0530 Subject: [PATCH 01/22] Add singleton base class --- UnleashClient/__init__.py | 131 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 127 insertions(+), 4 deletions(-) diff --git a/UnleashClient/__init__.py b/UnleashClient/__init__.py index efca580..c175e92 100644 --- a/UnleashClient/__init__.py +++ b/UnleashClient/__init__.py @@ -15,6 +15,129 @@ from .utils import LOGGER from .deprecation_warnings import strategy_v2xx_deprecation_check, default_value_warning + +class FeatureTogglesFromConst: + def __init__(self): + self.feature_toggles_dict = consts.FEATURE_TOGGLES_API_RESPONSE + + def is_enabled(self, feature_name, app_context: Optional[Dict] = {}) -> bool: + """ + Check if certain feature is enabled in const + + Args: + feature_name(str): Name of the feature + app_context(dict): App context to check when certain feature is enabled for given entity + eg: { + "partner_names": "" + } + + Returns(bool): True if feature is enabled else False + """ + is_feature_enabled = feature_name in self.feature_toggles_dict + + if not is_feature_enabled: # If Feature is not enabled then return is_feature_enabled Value + return is_feature_enabled + + if not app_context: # If there's not any app_context then return is_feature_enabled value + return is_feature_enabled + + app_context_parameter_key = list(app_context.keys())[0] + app_context_parameter_value = list(app_context.values())[0] + + feature_data = self.feature_toggles_dict[feature_name] + return app_context_parameter_value in feature_data.get(app_context_parameter_key, []) + + def fetch_feature_toggles(self) -> Dict[str, Any]: + """ + Return Feature toggles from const + """ + return self.feature_toggles_dict + + +class FeatureToggles: + __instance = None + __client = None + + __url = None + __app_name = None + __redis_host = None + __redis_port = None + __redis_db = None + + + def __init__(self): + """ Virtually private constructor. """ + if FeatureToggles.__instance is None: + LOGGER.info("FeatureFlag class not initialized!") + else: + return FeatureToggles.__instance + + + @staticmethod + def __get_unleash_client(): + """ Static access method. """ + if FeatureToggles.__client is None: + FeatureToggles.__client = UnleashClient(FeatureToggles.__url, FeatureToggles.__app_name, FeatureToggles.__redis_host, + FeatureToggles.__redis_port,FeatureToggles.__redis_db) + FeatureToggles.__client.initialize_client() + + return FeatureToggles.__client + + @staticmethod + def initialize(url: str, + app_name: str, + redis_host: str, + redis_port: str, + redis_db: str): + """ Static access method. """ + if FeatureToggles.__instance is None: + FeatureToggles.__instance = FeatureToggles() + + FeatureToggles.__url = url + FeatureToggles.__app_name = app_name + FeatureToggles.__redis_host = redis_host + FeatureToggles.__redis_port = redis_port + FeatureToggles.__redis_db = redis_db + + @staticmethod + def is_enabled_for_domain(feature_name: str, domain_name: str): + """ Static access method. """ + + context = { + 'domainNames': domain_name + } + return FeatureToggles.__get_unleash_client().is_enabled(feature_name, + context) + + @staticmethod + def is_enabled_for_business(feature_name: str, business_via_name: str): + """ Static access method. """ + + context = { + 'businessViaNames': business_via_name + } + return FeatureToggles.__get_unleash_client().is_enabled(feature_name, + context) + + @staticmethod + def is_enabled_for_partner(feature_name: str, partner_name: str): + """ Static access method. """ + + context = { + 'partnerNames': partner_name + } + return FeatureToggles.__get_unleash_client().is_enabled(feature_name, + context) + + @staticmethod + def is_enabled_for_expert(feature_name: str, expert_email: str): + """ Static access method. """ + + context = { + 'expertEmails': expert_email + } + return FeatureToggles.__get_unleash_client().is_enabled(feature_name, + context) # pylint: disable=dangerous-default-value class UnleashClient(): """ @@ -24,6 +147,9 @@ class UnleashClient(): def __init__(self, url: str, app_name: str, + redis_host: str, + redis_port: str, + redis_db: str, environment: str = "default", instance_id: str = "unleash-client-python", refresh_interval: int = 15, @@ -33,10 +159,7 @@ def __init__(self, custom_headers: dict = {}, custom_options: dict = {}, custom_strategies: dict = {}, - cache_directory: str = None, - redis_host: str, - redis_port: str, - redis_db: str) -> None: + cache_directory: str = None) -> None: """ A client for the Unleash feature toggle system. From 3969757cbc8b50dd4eb127fce8418db089a13712 Mon Sep 17 00:00:00 2001 From: parvez alam Date: Sun, 21 Feb 2021 21:00:42 +0530 Subject: [PATCH 02/22] Add consts --- UnleashClient/constants.py | 32 +++++++++++++++++++++++++++++--- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/UnleashClient/constants.py b/UnleashClient/constants.py index 83a08bf..50d3a7d 100644 --- a/UnleashClient/constants.py +++ b/UnleashClient/constants.py @@ -16,6 +16,32 @@ FEATURES_URL = "/client/features" METRICS_URL = "/client/metrics" -REDIS_HOST = "127.0.0.1" -REDIS_PORT = 6379 -REDIS_DB = 8 + +FEATURE_TOGGLES_BASE_URL = "http://128.199.29.137:4242/api" +FEATURE_TOGGLES_APP_NAME = "feature-toggles-poc" +FEATURE_TOGGLES_INSTANCE_ID = "haptik-development-dev-parvez-vm-1" +FEATURE_TOGGLES_ENABLED = False +FEATURE_TOGGLES_CACHE_KEY = "/client/features" +FEATURE_TOGGLES_API_RESPONSE = { + "haptik.development.enable_smart_skills": { + "domain_names": ["test_pvz_superman", "priyanshisupermandefault"], + "business_via_names": ["testpvzsupermanchannel", "priyanshisupermandefaultchannel"], + "partner_names": ["Platform Demo"] + }, + "prestaging.staging.enable_smart_skills": { + "domain_names": ["test_pvz_superman", "priyanshisupermandefault"], + "business_via_names": ["testpvzsupermanchannel", "priyanshisupermandefaultchannel"], + "partner_names": ["Platform Demo"] + }, + "haptik.staging.enable_smart_skills": { + "domain_names": ["test_pvz_superman", "priyanshisupermandefault"], + "business_via_names": ["testpvzsupermanchannel", "priyanshisupermandefaultchannel"], + "partner_names": ["Platform Demo"] + }, + "haptik.production.enable_smart_skills": { + "domain_names": ["test_pvz_superman", "priyanshisupermandefault"], + "business_via_names": ["testpvzsupermanchannel", "priyanshisupermandefaultchannel"], + "partner_names": ["Platform Demo"] + } +} + From 734aecbbb81b51ce9bfb2f2768549a3597cc5e39 Mon Sep 17 00:00:00 2001 From: parvez alam Date: Sun, 21 Feb 2021 21:00:57 +0530 Subject: [PATCH 03/22] Change domain check strategy --- UnleashClient/strategies/EnableForDomainStrategy.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/UnleashClient/strategies/EnableForDomainStrategy.py b/UnleashClient/strategies/EnableForDomainStrategy.py index 9b43842..227be7b 100644 --- a/UnleashClient/strategies/EnableForDomainStrategy.py +++ b/UnleashClient/strategies/EnableForDomainStrategy.py @@ -2,7 +2,7 @@ class EnableForDomains(Strategy): def load_provisioning(self) -> list: - return [x.strip() for x in self.parameters["domainIds"].split(',')] + return [x.strip() for x in self.parameters["domainNames"].split(',')] def apply(self, context: dict = None) -> bool: """ @@ -12,7 +12,7 @@ def apply(self, context: dict = None) -> bool: """ default_value = False - if "domainIds" in context.keys(): - default_value = context["domainIds"] in self.parsed_provisioning + if "domainNames" in context.keys(): + default_value = context["domainNames"] in self.parsed_provisioning return default_value From 7d51b6049df093a565adc02ca0b9429305cf6da3 Mon Sep 17 00:00:00 2001 From: parvez alam Date: Sun, 21 Feb 2021 21:03:04 +0530 Subject: [PATCH 04/22] Add new check to enable or disable features for businesses --- .../strategies/EnableForBusinessStrategy.py | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 UnleashClient/strategies/EnableForBusinessStrategy.py diff --git a/UnleashClient/strategies/EnableForBusinessStrategy.py b/UnleashClient/strategies/EnableForBusinessStrategy.py new file mode 100644 index 0000000..9ca12cc --- /dev/null +++ b/UnleashClient/strategies/EnableForBusinessStrategy.py @@ -0,0 +1,21 @@ +from UnleashClient.strategies import Strategy + +class EnableForBusinesses(Strategy): + def load_provisioning(self) -> list: + return [ + x.strip() for x in self.parameters["businessViaNames"].split(',') + ] + + def apply(self, context: dict = None) -> bool: + """ + Turn on if I'm a cat. + + :return: + """ + default_value = False + + if "businessViaNames" in context.keys(): + default_value = context["businessViaNames"] in self.parsed_provisioning + + return default_value + From 48a504f8e496fbecbdb6252fec914defbd1148a2 Mon Sep 17 00:00:00 2001 From: parvez alam Date: Sun, 21 Feb 2021 21:05:23 +0530 Subject: [PATCH 05/22] Add new strategy to check whether feature is enabled for partner or not --- .../strategies/EnableForPartnerStrategy.py | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 UnleashClient/strategies/EnableForPartnerStrategy.py diff --git a/UnleashClient/strategies/EnableForPartnerStrategy.py b/UnleashClient/strategies/EnableForPartnerStrategy.py new file mode 100644 index 0000000..17d7a84 --- /dev/null +++ b/UnleashClient/strategies/EnableForPartnerStrategy.py @@ -0,0 +1,22 @@ +from UnleashClient.strategies import Strategy + +class EnableForBusinesses(Strategy): + def load_provisioning(self) -> list: + return [ + x.strip() for x in self.parameters["partnerNames"].split(',') + ] + + def apply(self, context: dict = None) -> bool: + """ + Check if feature is enabled for given partner or not + + Args: + context(dict): partner name provided as context + """ + default_value = False + + if "partnerNames" in context.keys(): + default_value = context["partnerNames"] in self.parsed_provisioning + + return default_value + From e5c00b9e8ec1672ef9d07bbb6cf2f07e6b1b9830 Mon Sep 17 00:00:00 2001 From: parvez alam Date: Sun, 21 Feb 2021 21:05:34 +0530 Subject: [PATCH 06/22] Update doc strings --- UnleashClient/strategies/EnableForBusinessStrategy.py | 7 ++++--- UnleashClient/strategies/EnableForDomainStrategy.py | 7 ++++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/UnleashClient/strategies/EnableForBusinessStrategy.py b/UnleashClient/strategies/EnableForBusinessStrategy.py index 9ca12cc..0e5da4e 100644 --- a/UnleashClient/strategies/EnableForBusinessStrategy.py +++ b/UnleashClient/strategies/EnableForBusinessStrategy.py @@ -8,9 +8,10 @@ def load_provisioning(self) -> list: def apply(self, context: dict = None) -> bool: """ - Turn on if I'm a cat. - - :return: + Check if feature is enabled for given business or not + + Args: + context(dict): business-via-name provided as context """ default_value = False diff --git a/UnleashClient/strategies/EnableForDomainStrategy.py b/UnleashClient/strategies/EnableForDomainStrategy.py index 227be7b..1cb14c5 100644 --- a/UnleashClient/strategies/EnableForDomainStrategy.py +++ b/UnleashClient/strategies/EnableForDomainStrategy.py @@ -6,9 +6,10 @@ def load_provisioning(self) -> list: def apply(self, context: dict = None) -> bool: """ - Turn on if I'm a cat. - - :return: + Check if feature is enabled for domain or not + + Args: + context(dict): domain name provided as context """ default_value = False From e9958088e4555543d018d587717b91d20e5acbb5 Mon Sep 17 00:00:00 2001 From: parvez alam Date: Sun, 21 Feb 2021 21:06:24 +0530 Subject: [PATCH 07/22] Rename the class name --- UnleashClient/strategies/EnableForPartnerStrategy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/UnleashClient/strategies/EnableForPartnerStrategy.py b/UnleashClient/strategies/EnableForPartnerStrategy.py index 17d7a84..4e03ec2 100644 --- a/UnleashClient/strategies/EnableForPartnerStrategy.py +++ b/UnleashClient/strategies/EnableForPartnerStrategy.py @@ -1,6 +1,6 @@ from UnleashClient.strategies import Strategy -class EnableForBusinesses(Strategy): +class EnableForPartners(Strategy): def load_provisioning(self) -> list: return [ x.strip() for x in self.parameters["partnerNames"].split(',') From e53ce83cfbb973b77fcda4aa5967d43f6766a330 Mon Sep 17 00:00:00 2001 From: parvez alam Date: Sun, 21 Feb 2021 21:07:30 +0530 Subject: [PATCH 08/22] Add Strateg to enable disable feature for expert --- .../strategies/EnableForExpertStrategy.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 UnleashClient/strategies/EnableForExpertStrategy.py diff --git a/UnleashClient/strategies/EnableForExpertStrategy.py b/UnleashClient/strategies/EnableForExpertStrategy.py new file mode 100644 index 0000000..43b52b4 --- /dev/null +++ b/UnleashClient/strategies/EnableForExpertStrategy.py @@ -0,0 +1,19 @@ +from UnleashClient.strategies import Strategy + +class EnableForExperts(Strategy): + def load_provisioning(self) -> list: + return [x.strip() for x in self.parameters["expertEmails"].split(',')] + + def apply(self, context: dict = None) -> bool: + """ + Check if feature is enabled for expert or not + + Args: + context(dict): expert email provided as context + """ + default_value = False + + if "expertEmails" in context.keys(): + default_value = context["expertEmails"] in self.parsed_provisioning + + return default_value From fdb2b78e6603ba1d3fdff063aa831d447c2fce33 Mon Sep 17 00:00:00 2001 From: parvez alam Date: Sun, 21 Feb 2021 21:08:34 +0530 Subject: [PATCH 09/22] Add extra line before starting new class --- UnleashClient/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/UnleashClient/__init__.py b/UnleashClient/__init__.py index c175e92..222692a 100644 --- a/UnleashClient/__init__.py +++ b/UnleashClient/__init__.py @@ -138,6 +138,7 @@ def is_enabled_for_expert(feature_name: str, expert_email: str): } return FeatureToggles.__get_unleash_client().is_enabled(feature_name, context) + # pylint: disable=dangerous-default-value class UnleashClient(): """ From b40bb7dac468d3335822e2155126622780316bc7 Mon Sep 17 00:00:00 2001 From: parvez alam Date: Sun, 21 Feb 2021 23:26:32 +0530 Subject: [PATCH 10/22] refactor --- UnleashClient/__init__.py | 38 +++++++++++++++++++++++--------------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/UnleashClient/__init__.py b/UnleashClient/__init__.py index 222692a..7ccb12c 100644 --- a/UnleashClient/__init__.py +++ b/UnleashClient/__init__.py @@ -60,31 +60,38 @@ class FeatureToggles: __url = None __app_name = None + __instance_id = None + __custom_strategies = {} + __redis_host = None __redis_port = None __redis_db = None def __init__(self): - """ Virtually private constructor. """ + """Initialize a class""" if FeatureToggles.__instance is None: - LOGGER.info("FeatureFlag class not initialized!") + print("FeatureFlag class not initialized!") else: return FeatureToggles.__instance - @staticmethod - def __get_unleash_client(): + @classmethod + def __get_unleash_client(cls): """ Static access method. """ if FeatureToggles.__client is None: - FeatureToggles.__client = UnleashClient(FeatureToggles.__url, FeatureToggles.__app_name, FeatureToggles.__redis_host, + FeatureToggles.__client = UnleashClient(FeatureToggles.__url, FeatureToggles.__app_name, + FeatureToggles.__instance_id, + FeatureToggles.__custom_strategies, + FeatureToggles.__redis_host, FeatureToggles.__redis_port,FeatureToggles.__redis_db) FeatureToggles.__client.initialize_client() return FeatureToggles.__client - @staticmethod - def initialize(url: str, + @classmethod + def initialize(cls, + url: str, app_name: str, redis_host: str, redis_port: str, @@ -98,6 +105,7 @@ def initialize(url: str, FeatureToggles.__redis_host = redis_host FeatureToggles.__redis_port = redis_port FeatureToggles.__redis_db = redis_db + FeatureToggles.__client = cls.__get_unleash_client() @staticmethod def is_enabled_for_domain(feature_name: str, domain_name: str): @@ -106,8 +114,8 @@ def is_enabled_for_domain(feature_name: str, domain_name: str): context = { 'domainNames': domain_name } - return FeatureToggles.__get_unleash_client().is_enabled(feature_name, - context) + return FeatureToggles.__client.is_enabled(feature_name, + context) @staticmethod def is_enabled_for_business(feature_name: str, business_via_name: str): @@ -116,8 +124,8 @@ def is_enabled_for_business(feature_name: str, business_via_name: str): context = { 'businessViaNames': business_via_name } - return FeatureToggles.__get_unleash_client().is_enabled(feature_name, - context) + return FeatureToggles.__client.is_enabled(feature_name, + context) @staticmethod def is_enabled_for_partner(feature_name: str, partner_name: str): @@ -126,8 +134,8 @@ def is_enabled_for_partner(feature_name: str, partner_name: str): context = { 'partnerNames': partner_name } - return FeatureToggles.__get_unleash_client().is_enabled(feature_name, - context) + return FeatureToggles.__client.is_enabled(feature_name, + context) @staticmethod def is_enabled_for_expert(feature_name: str, expert_email: str): @@ -136,8 +144,8 @@ def is_enabled_for_expert(feature_name: str, expert_email: str): context = { 'expertEmails': expert_email } - return FeatureToggles.__get_unleash_client().is_enabled(feature_name, - context) + return FeatureToggles.__client.is_enabled(feature_name, + context) # pylint: disable=dangerous-default-value class UnleashClient(): From dd468a86982aa533275752ab3a8071e1aa65493e Mon Sep 17 00:00:00 2001 From: parvez alam Date: Mon, 22 Feb 2021 18:40:03 +0530 Subject: [PATCH 11/22] Add context support --- UnleashClient/__init__.py | 83 +++++++++++++++++++++++++-------------- 1 file changed, 54 insertions(+), 29 deletions(-) diff --git a/UnleashClient/__init__.py b/UnleashClient/__init__.py index 7ccb12c..7f38b95 100644 --- a/UnleashClient/__init__.py +++ b/UnleashClient/__init__.py @@ -20,6 +20,7 @@ class FeatureTogglesFromConst: def __init__(self): self.feature_toggles_dict = consts.FEATURE_TOGGLES_API_RESPONSE + @classmethod def is_enabled(self, feature_name, app_context: Optional[Dict] = {}) -> bool: """ Check if certain feature is enabled in const @@ -53,6 +54,24 @@ def fetch_feature_toggles(self) -> Dict[str, Any]: """ return self.feature_toggles_dict + @staticmethod + def is_enabled_for_partner(feature_name: str, + partner_name: Optional[str] = ''): + context = {} + if partner_name: + context['partnerNames'] = partner_name + + return FeatureTogglesFromConst().is_enabled(feature_name, context) + + @staticmethod + def is_enabled_for_expert(feature_name: str, + expert_email: Optional[str] = ''): + context = {} + if expert_email: + context['expertEmails'] = expert_email + + return FeatureTogglesFromConst().is_enabled(feature_name, context) + class FeatureToggles: __instance = None @@ -93,57 +112,63 @@ def __get_unleash_client(cls): def initialize(cls, url: str, app_name: str, + isstance_id: str, redis_host: str, redis_port: str, - redis_db: str): + redis_db: str, + custom_strategies: Optional[Dict] = {},): """ Static access method. """ - if FeatureToggles.__instance is None: - FeatureToggles.__instance = FeatureToggles() - - FeatureToggles.__url = url - FeatureToggles.__app_name = app_name - FeatureToggles.__redis_host = redis_host - FeatureToggles.__redis_port = redis_port - FeatureToggles.__redis_db = redis_db - FeatureToggles.__client = cls.__get_unleash_client() + is_unleash_available = True if consts.FEATURE_TOGGLES_ENABLED else False + if is_unleash_available: + if FeatureToggles.__instance is None: + FeatureToggles.__instance = FeatureToggles() + + FeatureToggles.__url = url + FeatureToggles.__app_name = app_name + FeatureToggles.__redis_host = redis_host + FeatureToggles.__redis_port = redis_port + FeatureToggles.__redis_db = redis_db + FeatureToggles.__client = cls.__get_unleash_client() + else: + FeatureToggles.__client = FeatureTogglesFromConst() @staticmethod - def is_enabled_for_domain(feature_name: str, domain_name: str): + def is_enabled_for_domain(feature_name: str, + domain_name: Optional[str] = ''): """ Static access method. """ + context = {} + if domain_name: + context['domainNames'] = domain_name - context = { - 'domainNames': domain_name - } return FeatureToggles.__client.is_enabled(feature_name, context) @staticmethod - def is_enabled_for_business(feature_name: str, business_via_name: str): - """ Static access method. """ + def is_enabled_for_business(feature_name: str, + business_via_name: Optional[str] = ''): + context = {} + if business_via_name: + context['businessViaNames'] = business_via_name - context = { - 'businessViaNames': business_via_name - } return FeatureToggles.__client.is_enabled(feature_name, context) @staticmethod - def is_enabled_for_partner(feature_name: str, partner_name: str): - """ Static access method. """ + def is_enabled_for_partner(feature_name: str, + partner_name: Optional[str]=''): + if partner_name: + context['partner_name'] = partner_name - context = { - 'partnerNames': partner_name - } return FeatureToggles.__client.is_enabled(feature_name, context) @staticmethod - def is_enabled_for_expert(feature_name: str, expert_email: str): - """ Static access method. """ + def is_enabled_for_expert(feature_name: str, + expert_email: Optional[str] = ''): + context = {} + if expert_email: + context['expertEmails'] = expert_email - context = { - 'expertEmails': expert_email - } return FeatureToggles.__client.is_enabled(feature_name, context) From abc8299cee1066011b5a19d10cabb779d4ab2968 Mon Sep 17 00:00:00 2001 From: parvez alam Date: Wed, 28 Apr 2021 15:15:47 +0530 Subject: [PATCH 12/22] Add repo checks --- .github/PULL_REQUEST_TEMPLATE.md | 28 ++++++---------------------- .github/codecov.yml | 29 +++++++++++++++++++++++++++++ .github/mergeable.yml | 21 +++++++++++++++++++++ .github/release-drafter.yml | 28 ++++++++++++++++++++++++++++ 4 files changed, 84 insertions(+), 22 deletions(-) create mode 100644 .github/codecov.yml create mode 100644 .github/mergeable.yml create mode 100644 .github/release-drafter.yml diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 0292c9d..ddfdf1a 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,27 +1,11 @@ -# Description +## JIRA Ticket Number -Please include a summary of the change and which issue is fixed (if any). +JIRA TICKET: -Fixes # (issue) +## Description of change +(REMOVE ME) Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change. -## Type of change - -Please delete options that are not relevant. - -- [ ] Bug fix (non-breaking change which fixes an issue) -- [ ] New feature (non-breaking change which adds functionality) -- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) -- [ ] This change requires a documentation update - -# How Has This Been Tested? - -Please describe the tests that you ran to verify your changes. - -- [ ] Unit tests -- [ ] Spec Tests -- [ ] Integration tests / Manual Tests - -# Checklist: +## Checklist (OPTIONAL): - [ ] My code follows the style guidelines of this project - [ ] I have performed a self-review of my own code @@ -30,4 +14,4 @@ Please describe the tests that you ran to verify your changes. - [ ] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [ ] New and existing unit tests pass locally with my changes -- [ ] Any dependent changes have been merged and published in downstream modules \ No newline at end of file +- [ ] Any dependent changes have been merged and published in downstream modules diff --git a/.github/codecov.yml b/.github/codecov.yml new file mode 100644 index 0000000..d8d5ad2 --- /dev/null +++ b/.github/codecov.yml @@ -0,0 +1,29 @@ +codecov: + notify: + require_ci_to_pass: yes + +coverage: + precision: 2 + round: down + range: "40...100" + + status: + project: yes + patch: + default: + target: 80% + threshold: 1% + changes: no + +parsers: + gcov: + branch_detection: + conditional: yes + loop: yes + method: no + macro: no + +comment: + layout: "header, diff" + behavior: default + require_changes: no diff --git a/.github/mergeable.yml b/.github/mergeable.yml new file mode 100644 index 0000000..682fb99 --- /dev/null +++ b/.github/mergeable.yml @@ -0,0 +1,21 @@ +mergeable: + pull_requests: + stale: + days: 14 + message: 'This PR is stale. Please follow up!' + + label: + must_include: + regex: '(new-feature)|(documentation)|(bug-fixes)|(enhancement)|(needs-migration)|(packages-updated)|(miscellaneous)|(superman)' + message: 'Can you please add a valid label! [One of (new-feature) / (documentation) / (bug-fixes) / (enhancement) / (needs-migration) / (packages-updated) / (miscellaneous)]' + must_exclude: + regex: '(do-not-merge)' + message: 'This PR is work in progress. Cannot be merged yet.' + + description: + no_empty: + enabled: true + message: 'Can you please add a description!' + must_exclude: + regex: 'do not merge' + message: 'This PR is work in progress. Cannot be merged yet.' diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml new file mode 100644 index 0000000..460eaa9 --- /dev/null +++ b/.github/release-drafter.yml @@ -0,0 +1,28 @@ +name-template: $NEXT_PATCH_VERSION +tag-template: $NEXT_PATCH_VERSION +branches: master +template: | + # What's Changed + + $CHANGES +categories: + - title: 🚀 Features + label: new-feature + - title: 🐛 Bug Fixes + label: bug-fixes + - title: 📖 Documentation + label: documentation + - title: 💯 Enhancements + label: enhancement + - title: 🚒 Migrations + label: needs-migration + - title: 📦 Packages Updated + label: packages-updated + - title: 👺 Miscellaneous + label: miscellaneous + - title: 💪 Superman Release + label: superman + + +# exclude-labels: +# - miscellaneous From 455715be4033789b9e28cffc4aa83aefcb5dfea6 Mon Sep 17 00:00:00 2001 From: parvez alam Date: Sun, 16 May 2021 18:16:25 +0530 Subject: [PATCH 13/22] Refactor feature toggle module --- FeatureToggles/__init__.py | 353 +++++++++++++++++++++++++++++++++++++ 1 file changed, 353 insertions(+) create mode 100644 FeatureToggles/__init__.py diff --git a/FeatureToggles/__init__.py b/FeatureToggles/__init__.py new file mode 100644 index 0000000..99ee8d6 --- /dev/null +++ b/FeatureToggles/__init__.py @@ -0,0 +1,353 @@ +# Python Imports +import redis +import pickle +from typing import Dict, Any, Optional + +# Unleash Imports +from UnleashClient import constants as consts +from UnleashClient import UnleashClient +from UnleashClient.utils import LOGGER + + +class FeatureTogglesFromConst: + def __init__(self): + self.feature_toggles_dict = consts.FEATURE_TOGGLES_API_RESPONSE + + def is_enabled(self, feature_name, + app_context: Optional[Dict] = {}) -> bool: + """ + Check if certain feature is enabled in const + Args: + feature_name(str): Name of the feature + app_context(dict): App context to check when certain feature is enabled for given entity + eg: { + "partner_names": "" + } + Returns(bool): True if feature is enabled else False + """ + is_feature_enabled = feature_name in self.feature_toggles_dict + + # If Feature is not enabled then return is_feature_enabled Value + if not is_feature_enabled: + return is_feature_enabled + + if not app_context: # If there's not any app_context then return is_feature_enabled value + return is_feature_enabled + + app_context_parameter_key = list(app_context.keys())[0] + app_context_parameter_value = list(app_context.values())[0] + + feature_data = self.feature_toggles_dict[feature_name] + return app_context_parameter_value in feature_data.get(app_context_parameter_key, []) + + @staticmethod + def fetch_feature_toggles() -> Dict[str, Any]: + """ + Return Feature toggles from const + """ + return consts.FEATURE_TOGGLES_API_RESPONSE + + @staticmethod + def is_enabled_for_partner(feature_name: str, + partner_name: Optional[str] = ''): + context = {} + if partner_name: + context['partner_names'] = partner_name + + return FeatureTogglesFromConst().is_enabled(feature_name, context) + + @staticmethod + def is_enabled_for_expert(feature_name: str, + expert_email: Optional[str] = ''): + context = {} + if expert_email: + context['expert_emails'] = expert_email + + return FeatureTogglesFromConst().is_enabled(feature_name, context) + + @staticmethod + def is_enabled_for_business(feature_name: str, + business_via_name: Optional[str] = ''): + context = {} + if business_via_name: + context['business_via_names'] = business_via_name + + return FeatureTogglesFromConst().is_enabled(feature_name, context) + + @staticmethod + def is_enabled_for_domain(feature_name: str, + domain_name: Optional[str] = ''): + context = {} + if domain_name: + context['domain_names'] = domain_name + + return FeatureTogglesFromConst().is_enabled(feature_name, context) + + +class FeatureToggles: + __client = None + __url = None + __app_name = None + __instance_id = None + __redis_host = None + __redis_port = None + __redis_db = None + __cas_name = None + __environment = None + __cache = None + __enable_toggle_service = True + + @staticmethod + def initialize(url: str, + app_name: str, + instance_id: str, + cas_name: str, + environment: str, + redis_host: str, + redis_port: str, + redis_db: str, + enable_toggle_service: bool = True) -> None: + """ Static access method. """ + if FeatureToggles.__client is None: + FeatureToggles.__url = url + FeatureToggles.__app_name = app_name + FeatureToggles.__instance_id = instance_id + FeatureToggles.__cas_name = cas_name + FeatureToggles.__environment = environment + FeatureToggles.__redis_host = redis_host + FeatureToggles.__redis_port = redis_port + FeatureToggles.__redis_db = redis_db + FeatureToggles.__enable_toggle_service = enable_toggle_service + FeatureToggles.__cache = FeatureToggles.__get_cache() + else: + raise Exception("Client has been already initialized") + + @staticmethod + def __get_cache(): + """ + Create redis connection + """ + if FeatureToggles.__cache is None: + FeatureToggles.__cache = redis.Redis( + host=FeatureToggles.__redis_host, + port=FeatureToggles.__redis_port, + db=FeatureToggles.__redis_db + ) + + return FeatureToggles.__cache + + @staticmethod + def update_cache(data: Dict[str, Any]) -> None: + """ + Update cache data + Args: + data(dict): Feature toggles Data + Returns: + None + """ + if FeatureToggles.__cache is None: + raise Exception( + 'To update cache Feature Toggles class needs to be initialised' + ) + + LOGGER.info(f'Updating the cache data: {data}') + try: + FeatureToggles.__cache.set( + consts.FEATURES_URL, pickle.dumps(data) + ) + except Exception as err: + raise Exception( + f'Exception occured while updating the redis cache: {str(err)}' + ) + LOGGER.info(f'Cache Updatation is Done') + + @staticmethod + def __get_unleash_client(): + """ + Initialize the client if client is None Else Return the established client + """ + if FeatureToggles.__enable_toggle_service: + if FeatureToggles.__client is None: + FeatureToggles.__client = UnleashClient( + url=FeatureToggles.__url, + app_name=FeatureToggles.__app_name, + instance_id=FeatureToggles.__instance_id, + cas_name=FeatureToggles.__cas_name, + environment=FeatureToggles.__environment, + redis_host=FeatureToggles.__redis_host, + redis_port=FeatureToggles.__redis_port, + redis_db=FeatureToggles.__redis_db + ) + FeatureToggles.__client.initialize_client() + else: + FeatureToggles.__client = FeatureTogglesFromConst() + + return FeatureToggles.__client + + @staticmethod + def __get_full_feature_name(feature_name: str): + """ + construct full feature name + Args: + feature_name(str): Feature Name + eg: `enable_language_support` + Returns: + (str): fully constructed feature name including cas and env name + format => '{cas_name}.{environment}.{feature_name}' + eg => 'haptik.production.enable_language_support' + """ + try: + full_feature_name = ( + f'{FeatureToggles.__cas_name}.' + f'{FeatureToggles.__environment}.' + f'{feature_name}' + ) + return full_feature_name + except Exception as err: + raise Exception(f'Error while forming the feature name: {str(err)}') + + @staticmethod + def is_enabled_for_domain(feature_name: str, + domain_name: Optional[str] = ''): + """ + Util method to check whether given feature is enabled or not + Args: + feature_name(str): Name of the feature + domain_name(Optional[str]): Name of the domain + Returns: + (bool): True if Feature is enabled else False + """ + feature_name = FeatureToggles.__get_full_feature_name(feature_name) + + context = {} + if domain_name: + context['domain_names'] = domain_name + + return FeatureToggles.__get_unleash_client().is_enabled(feature_name, + context) + + @staticmethod + def is_enabled_for_partner(feature_name: str, + partner_name: Optional[str] = ''): + """ + Util method to check whether given feature is enabled or not + Args: + feature_name(str): Name of the feature + partner_name(Optional[str]): Name of the Partner + Returns: + (bool): True if Feature is enabled else False + """ + feature_name = FeatureToggles.__get_full_feature_name(feature_name) + + context = {} + if partner_name: + context['partner_names'] = partner_name + + return FeatureToggles.__get_unleash_client().is_enabled(feature_name, + context) + + @staticmethod + def is_enabled_for_business(feature_name: str, + business_via_name: Optional[str] = ''): + """ + Util method to check whether given feature is enabled or not + Args: + feature_name(str): Name of the feature + business_via_name(Optional[str]): Business Via Name + Returns: + (bool): True if Feature is enabled else False + """ + feature_name = FeatureToggles.__get_full_feature_name(feature_name) + + context = {} + if business_via_name: + context['business_via_names'] = business_via_name + + return FeatureToggles.__get_unleash_client().is_enabled(feature_name, + context) + + @staticmethod + def is_enabled_for_expert(feature_name: str, + expert_email: Optional[str] = ''): + """ + Util method to check whether given feature is enabled or not + Args: + feature_name(str): Name of the feature + expert_email(Optional[str]): Expert Emails + Returns: + (bool): True if Feature is enabled else False + """ + feature_name = FeatureToggles.__get_full_feature_name(feature_name) + + context = {} + if expert_email: + context['expert_emails'] = expert_email + + return FeatureToggles.__get_unleash_client().is_enabled(feature_name, + context) + + @staticmethod + def fetch_feature_toggles(): + """ + Returns(Dict): + Feature toggles data + Eg: { + "..": { + "domain_names": [], + "business_via_names": [], + "partner_names": [] + } + } + """ + # TODO: Remove the cas and environment name from the feature toggles while returning the response + feature_toggles = pickle.loads( + FeatureToggles.__cache.get(consts.FEATURES_URL) + ) + response = {} + try: + if feature_toggles: + for feature_toggle in feature_toggles: + full_feature_name = feature_toggle['name'] + # split the feature and get compare the cas and environment name + feature = full_feature_name.split('.') + cas_name = feature[0] + environment = feature[1] + + # Define empty list for empty values + domain_names = [] + partner_names = [] + business_via_names = [] + expert_emails = [] + + if cas_name == FeatureToggles.__cas_name and environment == FeatureToggles.__environment: + # Strip CAS and ENV name from feature name + active_cas_env_name = f'{cas_name}.{environment}.' + full_feature_name = full_feature_name.replace(active_cas_env_name, '') + if full_feature_name not in response: + response[full_feature_name] = {} + strategies = feature_toggle.get('strategies', []) + for strategy in strategies: + strategy_name = strategy.get('name', '') + parameters = strategy.get('parameters', {}) + if strategy_name == 'EnableForPartners': + partner_names = parameters.get('partner_names', '').replace(', ', ',').split(',') + + elif strategy_name == 'EnableForBusinesses': + business_via_names = parameters.get('business_via_names', '').replace(', ', ',').split(',') + elif strategy_name == 'EnableForDomains': + domain_names = parameters.get('domain_names', '').replace(', ', ',').split(',') + elif strategy_name == 'EnableForExperts': + expert_emails = parameters.get('expert_emails', '').replace(', ', ',').split(',') + + # Keep updating this list for new strategies which gets added + + # Assign the strategies data to feature name + response[full_feature_name]['partner_names'] = partner_names + response[full_feature_name]['business_via_names'] = business_via_names + response[full_feature_name]['domain_names'] = domain_names + response[full_feature_name]['expert_emails'] = expert_emails + except Exception as err: + # Handle this exception from where this util gets called + raise Exception(f'Error occured while parsing the response: {str(err)}') + + return response From 90bdd28cb9300e2976b2120af73d9b1b054aa564 Mon Sep 17 00:00:00 2001 From: parvez alam Date: Mon, 17 May 2021 11:23:58 +0530 Subject: [PATCH 14/22] Add readme file --- README.md | 113 ++++++++++++++---------------------------------------- 1 file changed, 29 insertions(+), 84 deletions(-) diff --git a/README.md b/README.md index 6b6876c..85ac655 100644 --- a/README.md +++ b/README.md @@ -1,105 +1,50 @@ # unleash-client-python -![](https://github.com/unleash/unleash-client-python/workflows/CI/badge.svg?branch=master) [![Coverage Status](https://coveralls.io/repos/github/Unleash/unleash-client-python/badge.svg?branch=master)](https://coveralls.io/github/Unleash/unleash-client-python?branch=master) [![PyPI version](https://badge.fury.io/py/UnleashClient.svg)](https://badge.fury.io/py/UnleashClient) ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/UnleashClient.svg) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) - - -This is the Python client for [Unleash](https://github.com/unleash/unleash). It implements [Client Specifications 1.0](https://github.com/Unleash/unleash/blob/master/docs/client-specification.md) and checks compliance based on spec in [unleash/client-specifications](https://github.com/Unleash/client-specification) - -What it supports: -* Default activation strategies using 32-bit [Murmerhash3](https://en.wikipedia.org/wiki/MurmurHash) -* Custom strategies -* Full client lifecycle: - * Client registers with Unleash server - * Client periodically fetches feature toggles and stores to on-disk cache - * Client periodically sends metrics to Unleash Server -* Tested on Linux (Ubuntu), OSX, and Windows - -Check out the [project documentation](https://unleash.github.io/unleash-client-python/) and the [changelog](https://unleash.github.io/unleash-client-python/changelog/). - -## Installation - -Check out the package on [Pypi](https://pypi.org/project/UnleashClient/)! +This is the Python client for [Unleash](https://github.com/unleash/unleash). It implements [Client Specifications 1.0](https://github.com/Unleash/unleash/blob/master/docs/client-specification.md) and checks compliance based on spec in [unleash/client-specifications](https://github.com/Unleash/client-specification). +## Params Required to initialise the FeatureToggles ``` -pip install UnleashClient -``` +url -> Unleash Service Client URL -## Usage +app_name -> Unleash server URL -### Initialization +environment -> Get from ENV variable -``` -from UnleashClient import UnleashClient -client = UnleashClient("https://unleash.herokuapp.com/api", "My Program") -client.initialize_client() -``` +cas_name -> Get from ENV variable -To clean up gracefully: -``` -client.destroy() -``` +redis_host -> Get from ENV variable -#### Arguments -Argument | Description | Required? | Type | Default Value| ----------|-------------|-----------|-------|---------------| -url | Unleash server URL | Y | String | N/A | -app_name | Name of your program | Y | String | N/A | -environment | Name of current environment | N | String | default | -instance_id | Unique ID for your program | N | String | unleash-client-python | -refresh_interval | How often the unleash client should check for configuration changes. | N | Integer | 15 | -metrics_interval | How often the unleash client should send metrics to server. | N | Integer | 60 | -disable_metrics | Disables sending metrics to Unleash server. | N | Boolean | F | -disable_registration | Disables registration with Unleash server. | N | Boolean | F | -custom_headers | Custom headers to send to Unleash. | N | Dictionary | {} | -custom_strategies | Custom strategies you'd like UnleashClient to support. | N | Dictionary | {} | +redis_port -> Get from ENV variable -### Checking if a feature is enabled +redis_db -> Get from ENV variable -A check of a simple toggle: -```Python -client.is_enabled("My Toggle") +enable_feature_oggle_service -> Get from ENV variable ``` -Specifying a default value: -```Python -client.is_enabled("My Toggle", default_value=True) +## Initialise the client in haptik_api ``` - -Supplying application context: -```Python -app_context = {"userId": "test@email.com"} -client.is_enabled("User ID Toggle", app_context) +FeatureToggles.initialize( + url, + app_name, + environment, + cas_name, + redis_host, + redis_port, + redis_db + enable_feature_toggle_service) ``` -Supplying a fallback function: -```Python -def custom_fallback(feature_name: str, context: dict) -> bool: - return True - -client.is_enabled("My Toggle", fallback_function=custom_fallback) +## Usage in haptik Repositories ``` +# To check if feature is enabled for domain +FeatureToggles.is_enabled_for_domain(, ) -- Must accept the fature name and context as an argument. -- Client will evaluate the fallback function only if exception occurs when calling the `is_enabled()` method i.e. feature flag not found or other general exception. -- If both a `default_value` and `fallback_function` are supplied, client will define the default value by `OR`ing the default value and the output of the fallback function. - -### Getting a variant +# Check if certainfeature is enabled for partner +FeatureToggles.is_enabled_for_partner(, ) -Checking for a variant: -```python -context = {'userId': '2'} # Context must have userId, sessionId, or remoteAddr. If none are present, distribution will be random. +# Check if certain feature is enabled for business +FeatureToggles.is_enabled_for_business(, ) -variant = client.get_variant("MyvariantToggle", context) - -print(variant) -> { -> "name": "variant1", -> "payload": { -> "type": "string", -> "value": "val1" -> }, -> "enabled": True -> } +# Check if certain feature is enabled for an expert +FeatureToggles.is_enabled_for_expert(, ) ``` - -For more information about variants, see the [Beta feature documentation](https://unleash.github.io/docs/beta_features). From 80f4db220b61df8e5d47cd8bb3002836c18b5886 Mon Sep 17 00:00:00 2001 From: parvez alam Date: Mon, 17 May 2021 11:24:48 +0530 Subject: [PATCH 15/22] Rename director --- FeatureToggle/__init__.py | 353 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 353 insertions(+) create mode 100644 FeatureToggle/__init__.py diff --git a/FeatureToggle/__init__.py b/FeatureToggle/__init__.py new file mode 100644 index 0000000..99ee8d6 --- /dev/null +++ b/FeatureToggle/__init__.py @@ -0,0 +1,353 @@ +# Python Imports +import redis +import pickle +from typing import Dict, Any, Optional + +# Unleash Imports +from UnleashClient import constants as consts +from UnleashClient import UnleashClient +from UnleashClient.utils import LOGGER + + +class FeatureTogglesFromConst: + def __init__(self): + self.feature_toggles_dict = consts.FEATURE_TOGGLES_API_RESPONSE + + def is_enabled(self, feature_name, + app_context: Optional[Dict] = {}) -> bool: + """ + Check if certain feature is enabled in const + Args: + feature_name(str): Name of the feature + app_context(dict): App context to check when certain feature is enabled for given entity + eg: { + "partner_names": "" + } + Returns(bool): True if feature is enabled else False + """ + is_feature_enabled = feature_name in self.feature_toggles_dict + + # If Feature is not enabled then return is_feature_enabled Value + if not is_feature_enabled: + return is_feature_enabled + + if not app_context: # If there's not any app_context then return is_feature_enabled value + return is_feature_enabled + + app_context_parameter_key = list(app_context.keys())[0] + app_context_parameter_value = list(app_context.values())[0] + + feature_data = self.feature_toggles_dict[feature_name] + return app_context_parameter_value in feature_data.get(app_context_parameter_key, []) + + @staticmethod + def fetch_feature_toggles() -> Dict[str, Any]: + """ + Return Feature toggles from const + """ + return consts.FEATURE_TOGGLES_API_RESPONSE + + @staticmethod + def is_enabled_for_partner(feature_name: str, + partner_name: Optional[str] = ''): + context = {} + if partner_name: + context['partner_names'] = partner_name + + return FeatureTogglesFromConst().is_enabled(feature_name, context) + + @staticmethod + def is_enabled_for_expert(feature_name: str, + expert_email: Optional[str] = ''): + context = {} + if expert_email: + context['expert_emails'] = expert_email + + return FeatureTogglesFromConst().is_enabled(feature_name, context) + + @staticmethod + def is_enabled_for_business(feature_name: str, + business_via_name: Optional[str] = ''): + context = {} + if business_via_name: + context['business_via_names'] = business_via_name + + return FeatureTogglesFromConst().is_enabled(feature_name, context) + + @staticmethod + def is_enabled_for_domain(feature_name: str, + domain_name: Optional[str] = ''): + context = {} + if domain_name: + context['domain_names'] = domain_name + + return FeatureTogglesFromConst().is_enabled(feature_name, context) + + +class FeatureToggles: + __client = None + __url = None + __app_name = None + __instance_id = None + __redis_host = None + __redis_port = None + __redis_db = None + __cas_name = None + __environment = None + __cache = None + __enable_toggle_service = True + + @staticmethod + def initialize(url: str, + app_name: str, + instance_id: str, + cas_name: str, + environment: str, + redis_host: str, + redis_port: str, + redis_db: str, + enable_toggle_service: bool = True) -> None: + """ Static access method. """ + if FeatureToggles.__client is None: + FeatureToggles.__url = url + FeatureToggles.__app_name = app_name + FeatureToggles.__instance_id = instance_id + FeatureToggles.__cas_name = cas_name + FeatureToggles.__environment = environment + FeatureToggles.__redis_host = redis_host + FeatureToggles.__redis_port = redis_port + FeatureToggles.__redis_db = redis_db + FeatureToggles.__enable_toggle_service = enable_toggle_service + FeatureToggles.__cache = FeatureToggles.__get_cache() + else: + raise Exception("Client has been already initialized") + + @staticmethod + def __get_cache(): + """ + Create redis connection + """ + if FeatureToggles.__cache is None: + FeatureToggles.__cache = redis.Redis( + host=FeatureToggles.__redis_host, + port=FeatureToggles.__redis_port, + db=FeatureToggles.__redis_db + ) + + return FeatureToggles.__cache + + @staticmethod + def update_cache(data: Dict[str, Any]) -> None: + """ + Update cache data + Args: + data(dict): Feature toggles Data + Returns: + None + """ + if FeatureToggles.__cache is None: + raise Exception( + 'To update cache Feature Toggles class needs to be initialised' + ) + + LOGGER.info(f'Updating the cache data: {data}') + try: + FeatureToggles.__cache.set( + consts.FEATURES_URL, pickle.dumps(data) + ) + except Exception as err: + raise Exception( + f'Exception occured while updating the redis cache: {str(err)}' + ) + LOGGER.info(f'Cache Updatation is Done') + + @staticmethod + def __get_unleash_client(): + """ + Initialize the client if client is None Else Return the established client + """ + if FeatureToggles.__enable_toggle_service: + if FeatureToggles.__client is None: + FeatureToggles.__client = UnleashClient( + url=FeatureToggles.__url, + app_name=FeatureToggles.__app_name, + instance_id=FeatureToggles.__instance_id, + cas_name=FeatureToggles.__cas_name, + environment=FeatureToggles.__environment, + redis_host=FeatureToggles.__redis_host, + redis_port=FeatureToggles.__redis_port, + redis_db=FeatureToggles.__redis_db + ) + FeatureToggles.__client.initialize_client() + else: + FeatureToggles.__client = FeatureTogglesFromConst() + + return FeatureToggles.__client + + @staticmethod + def __get_full_feature_name(feature_name: str): + """ + construct full feature name + Args: + feature_name(str): Feature Name + eg: `enable_language_support` + Returns: + (str): fully constructed feature name including cas and env name + format => '{cas_name}.{environment}.{feature_name}' + eg => 'haptik.production.enable_language_support' + """ + try: + full_feature_name = ( + f'{FeatureToggles.__cas_name}.' + f'{FeatureToggles.__environment}.' + f'{feature_name}' + ) + return full_feature_name + except Exception as err: + raise Exception(f'Error while forming the feature name: {str(err)}') + + @staticmethod + def is_enabled_for_domain(feature_name: str, + domain_name: Optional[str] = ''): + """ + Util method to check whether given feature is enabled or not + Args: + feature_name(str): Name of the feature + domain_name(Optional[str]): Name of the domain + Returns: + (bool): True if Feature is enabled else False + """ + feature_name = FeatureToggles.__get_full_feature_name(feature_name) + + context = {} + if domain_name: + context['domain_names'] = domain_name + + return FeatureToggles.__get_unleash_client().is_enabled(feature_name, + context) + + @staticmethod + def is_enabled_for_partner(feature_name: str, + partner_name: Optional[str] = ''): + """ + Util method to check whether given feature is enabled or not + Args: + feature_name(str): Name of the feature + partner_name(Optional[str]): Name of the Partner + Returns: + (bool): True if Feature is enabled else False + """ + feature_name = FeatureToggles.__get_full_feature_name(feature_name) + + context = {} + if partner_name: + context['partner_names'] = partner_name + + return FeatureToggles.__get_unleash_client().is_enabled(feature_name, + context) + + @staticmethod + def is_enabled_for_business(feature_name: str, + business_via_name: Optional[str] = ''): + """ + Util method to check whether given feature is enabled or not + Args: + feature_name(str): Name of the feature + business_via_name(Optional[str]): Business Via Name + Returns: + (bool): True if Feature is enabled else False + """ + feature_name = FeatureToggles.__get_full_feature_name(feature_name) + + context = {} + if business_via_name: + context['business_via_names'] = business_via_name + + return FeatureToggles.__get_unleash_client().is_enabled(feature_name, + context) + + @staticmethod + def is_enabled_for_expert(feature_name: str, + expert_email: Optional[str] = ''): + """ + Util method to check whether given feature is enabled or not + Args: + feature_name(str): Name of the feature + expert_email(Optional[str]): Expert Emails + Returns: + (bool): True if Feature is enabled else False + """ + feature_name = FeatureToggles.__get_full_feature_name(feature_name) + + context = {} + if expert_email: + context['expert_emails'] = expert_email + + return FeatureToggles.__get_unleash_client().is_enabled(feature_name, + context) + + @staticmethod + def fetch_feature_toggles(): + """ + Returns(Dict): + Feature toggles data + Eg: { + "..": { + "domain_names": [], + "business_via_names": [], + "partner_names": [] + } + } + """ + # TODO: Remove the cas and environment name from the feature toggles while returning the response + feature_toggles = pickle.loads( + FeatureToggles.__cache.get(consts.FEATURES_URL) + ) + response = {} + try: + if feature_toggles: + for feature_toggle in feature_toggles: + full_feature_name = feature_toggle['name'] + # split the feature and get compare the cas and environment name + feature = full_feature_name.split('.') + cas_name = feature[0] + environment = feature[1] + + # Define empty list for empty values + domain_names = [] + partner_names = [] + business_via_names = [] + expert_emails = [] + + if cas_name == FeatureToggles.__cas_name and environment == FeatureToggles.__environment: + # Strip CAS and ENV name from feature name + active_cas_env_name = f'{cas_name}.{environment}.' + full_feature_name = full_feature_name.replace(active_cas_env_name, '') + if full_feature_name not in response: + response[full_feature_name] = {} + strategies = feature_toggle.get('strategies', []) + for strategy in strategies: + strategy_name = strategy.get('name', '') + parameters = strategy.get('parameters', {}) + if strategy_name == 'EnableForPartners': + partner_names = parameters.get('partner_names', '').replace(', ', ',').split(',') + + elif strategy_name == 'EnableForBusinesses': + business_via_names = parameters.get('business_via_names', '').replace(', ', ',').split(',') + elif strategy_name == 'EnableForDomains': + domain_names = parameters.get('domain_names', '').replace(', ', ',').split(',') + elif strategy_name == 'EnableForExperts': + expert_emails = parameters.get('expert_emails', '').replace(', ', ',').split(',') + + # Keep updating this list for new strategies which gets added + + # Assign the strategies data to feature name + response[full_feature_name]['partner_names'] = partner_names + response[full_feature_name]['business_via_names'] = business_via_names + response[full_feature_name]['domain_names'] = domain_names + response[full_feature_name]['expert_emails'] = expert_emails + except Exception as err: + # Handle this exception from where this util gets called + raise Exception(f'Error occured while parsing the response: {str(err)}') + + return response From 1f433c54b87b2a01699a643fc8fa4cd8f9d86293 Mon Sep 17 00:00:00 2001 From: parvez alam Date: Tue, 8 Jun 2021 02:51:21 +0530 Subject: [PATCH 16/22] Refactoring and Bug Fixes --- FeatureToggle/__init__.py | 111 ++------ UnleashClient/__init__.py | 249 ++---------------- UnleashClient/loader.py | 6 +- .../strategies/EnableForBusinessStrategy.py | 7 +- .../strategies/EnableForDomainStrategy.py | 10 +- .../strategies/EnableForExpertStrategy.py | 6 +- .../strategies/EnableForPartnerStrategy.py | 8 +- UnleashClient/strategies/__init__.py | 3 + 8 files changed, 61 insertions(+), 339 deletions(-) diff --git a/FeatureToggle/__init__.py b/FeatureToggle/__init__.py index 99ee8d6..197e0f4 100644 --- a/FeatureToggle/__init__.py +++ b/FeatureToggle/__init__.py @@ -9,81 +9,6 @@ from UnleashClient.utils import LOGGER -class FeatureTogglesFromConst: - def __init__(self): - self.feature_toggles_dict = consts.FEATURE_TOGGLES_API_RESPONSE - - def is_enabled(self, feature_name, - app_context: Optional[Dict] = {}) -> bool: - """ - Check if certain feature is enabled in const - Args: - feature_name(str): Name of the feature - app_context(dict): App context to check when certain feature is enabled for given entity - eg: { - "partner_names": "" - } - Returns(bool): True if feature is enabled else False - """ - is_feature_enabled = feature_name in self.feature_toggles_dict - - # If Feature is not enabled then return is_feature_enabled Value - if not is_feature_enabled: - return is_feature_enabled - - if not app_context: # If there's not any app_context then return is_feature_enabled value - return is_feature_enabled - - app_context_parameter_key = list(app_context.keys())[0] - app_context_parameter_value = list(app_context.values())[0] - - feature_data = self.feature_toggles_dict[feature_name] - return app_context_parameter_value in feature_data.get(app_context_parameter_key, []) - - @staticmethod - def fetch_feature_toggles() -> Dict[str, Any]: - """ - Return Feature toggles from const - """ - return consts.FEATURE_TOGGLES_API_RESPONSE - - @staticmethod - def is_enabled_for_partner(feature_name: str, - partner_name: Optional[str] = ''): - context = {} - if partner_name: - context['partner_names'] = partner_name - - return FeatureTogglesFromConst().is_enabled(feature_name, context) - - @staticmethod - def is_enabled_for_expert(feature_name: str, - expert_email: Optional[str] = ''): - context = {} - if expert_email: - context['expert_emails'] = expert_email - - return FeatureTogglesFromConst().is_enabled(feature_name, context) - - @staticmethod - def is_enabled_for_business(feature_name: str, - business_via_name: Optional[str] = ''): - context = {} - if business_via_name: - context['business_via_names'] = business_via_name - - return FeatureTogglesFromConst().is_enabled(feature_name, context) - - @staticmethod - def is_enabled_for_domain(feature_name: str, - domain_name: Optional[str] = ''): - context = {} - if domain_name: - context['domain_names'] = domain_name - - return FeatureTogglesFromConst().is_enabled(feature_name, context) - - class FeatureToggles: __client = None __url = None @@ -167,20 +92,17 @@ def __get_unleash_client(): Initialize the client if client is None Else Return the established client """ if FeatureToggles.__enable_toggle_service: - if FeatureToggles.__client is None: - FeatureToggles.__client = UnleashClient( - url=FeatureToggles.__url, - app_name=FeatureToggles.__app_name, - instance_id=FeatureToggles.__instance_id, - cas_name=FeatureToggles.__cas_name, - environment=FeatureToggles.__environment, - redis_host=FeatureToggles.__redis_host, - redis_port=FeatureToggles.__redis_port, - redis_db=FeatureToggles.__redis_db - ) - FeatureToggles.__client.initialize_client() - else: - FeatureToggles.__client = FeatureTogglesFromConst() + FeatureToggles.__client = UnleashClient( + url=FeatureToggles.__url, + app_name=FeatureToggles.__app_name, + instance_id=FeatureToggles.__instance_id, + cas_name=FeatureToggles.__cas_name, + environment=FeatureToggles.__environment, + redis_host=FeatureToggles.__redis_host, + redis_port=FeatureToggles.__redis_port, + redis_db=FeatureToggles.__redis_db + ) + FeatureToggles.__client.initialize_client() return FeatureToggles.__client @@ -300,6 +222,12 @@ def fetch_feature_toggles(): } """ # TODO: Remove the cas and environment name from the feature toggles while returning the response + + if FeatureToggles.__cache is None: + raise Exception( + 'To update cache Feature Toggles class needs to be initialised' + ) + feature_toggles = pickle.loads( FeatureToggles.__cache.get(consts.FEATURES_URL) ) @@ -321,7 +249,8 @@ def fetch_feature_toggles(): if cas_name == FeatureToggles.__cas_name and environment == FeatureToggles.__environment: # Strip CAS and ENV name from feature name - active_cas_env_name = f'{cas_name}.{environment}.' + active_cas_env_name = f'{FeatureToggles.__cas_name}.' + f'{FeatureToggles.__environment}.' full_feature_name = full_feature_name.replace(active_cas_env_name, '') if full_feature_name not in response: response[full_feature_name] = {} @@ -350,4 +279,4 @@ def fetch_feature_toggles(): # Handle this exception from where this util gets called raise Exception(f'Error occured while parsing the response: {str(err)}') - return response + return response \ No newline at end of file diff --git a/UnleashClient/__init__.py b/UnleashClient/__init__.py index 7f38b95..3e5f5a2 100644 --- a/UnleashClient/__init__.py +++ b/UnleashClient/__init__.py @@ -1,190 +1,30 @@ import redis -import pickle -from datetime import datetime, timezone -from typing import Dict, Callable, Any, Optional, List -import copy -from fcache.cache import FileCache -from apscheduler.job import Job -from apscheduler.schedulers.background import BackgroundScheduler -from apscheduler.triggers.interval import IntervalTrigger -from UnleashClient.api import register_client -from UnleashClient.periodic_tasks import fetch_and_load_features, aggregate_and_send_metrics -from UnleashClient.strategies import ApplicationHostname, Default, GradualRolloutRandom, \ - GradualRolloutSessionId, GradualRolloutUserId, UserWithId, RemoteAddress, FlexibleRollout, EnableForDomains -from UnleashClient import constants as consts -from .utils import LOGGER -from .deprecation_warnings import strategy_v2xx_deprecation_check, default_value_warning - - -class FeatureTogglesFromConst: - def __init__(self): - self.feature_toggles_dict = consts.FEATURE_TOGGLES_API_RESPONSE - - @classmethod - def is_enabled(self, feature_name, app_context: Optional[Dict] = {}) -> bool: - """ - Check if certain feature is enabled in const - - Args: - feature_name(str): Name of the feature - app_context(dict): App context to check when certain feature is enabled for given entity - eg: { - "partner_names": "" - } - - Returns(bool): True if feature is enabled else False - """ - is_feature_enabled = feature_name in self.feature_toggles_dict - - if not is_feature_enabled: # If Feature is not enabled then return is_feature_enabled Value - return is_feature_enabled - - if not app_context: # If there's not any app_context then return is_feature_enabled value - return is_feature_enabled - - app_context_parameter_key = list(app_context.keys())[0] - app_context_parameter_value = list(app_context.values())[0] - - feature_data = self.feature_toggles_dict[feature_name] - return app_context_parameter_value in feature_data.get(app_context_parameter_key, []) - - def fetch_feature_toggles(self) -> Dict[str, Any]: - """ - Return Feature toggles from const - """ - return self.feature_toggles_dict - - @staticmethod - def is_enabled_for_partner(feature_name: str, - partner_name: Optional[str] = ''): - context = {} - if partner_name: - context['partnerNames'] = partner_name - - return FeatureTogglesFromConst().is_enabled(feature_name, context) - - @staticmethod - def is_enabled_for_expert(feature_name: str, - expert_email: Optional[str] = ''): - context = {} - if expert_email: - context['expertEmails'] = expert_email - - return FeatureTogglesFromConst().is_enabled(feature_name, context) - - -class FeatureToggles: - __instance = None - __client = None - - __url = None - __app_name = None - __instance_id = None - __custom_strategies = {} - - __redis_host = None - __redis_port = None - __redis_db = None - - - def __init__(self): - """Initialize a class""" - if FeatureToggles.__instance is None: - print("FeatureFlag class not initialized!") - else: - return FeatureToggles.__instance - - - @classmethod - def __get_unleash_client(cls): - """ Static access method. """ - if FeatureToggles.__client is None: - FeatureToggles.__client = UnleashClient(FeatureToggles.__url, FeatureToggles.__app_name, - FeatureToggles.__instance_id, - FeatureToggles.__custom_strategies, - FeatureToggles.__redis_host, - FeatureToggles.__redis_port,FeatureToggles.__redis_db) - FeatureToggles.__client.initialize_client() - - return FeatureToggles.__client - - @classmethod - def initialize(cls, - url: str, - app_name: str, - isstance_id: str, - redis_host: str, - redis_port: str, - redis_db: str, - custom_strategies: Optional[Dict] = {},): - """ Static access method. """ - is_unleash_available = True if consts.FEATURE_TOGGLES_ENABLED else False - if is_unleash_available: - if FeatureToggles.__instance is None: - FeatureToggles.__instance = FeatureToggles() - - FeatureToggles.__url = url - FeatureToggles.__app_name = app_name - FeatureToggles.__redis_host = redis_host - FeatureToggles.__redis_port = redis_port - FeatureToggles.__redis_db = redis_db - FeatureToggles.__client = cls.__get_unleash_client() - else: - FeatureToggles.__client = FeatureTogglesFromConst() - @staticmethod - def is_enabled_for_domain(feature_name: str, - domain_name: Optional[str] = ''): - """ Static access method. """ - context = {} - if domain_name: - context['domainNames'] = domain_name - - return FeatureToggles.__client.is_enabled(feature_name, - context) - - @staticmethod - def is_enabled_for_business(feature_name: str, - business_via_name: Optional[str] = ''): - context = {} - if business_via_name: - context['businessViaNames'] = business_via_name - - return FeatureToggles.__client.is_enabled(feature_name, - context) +from typing import Dict, Callable - @staticmethod - def is_enabled_for_partner(feature_name: str, - partner_name: Optional[str]=''): - if partner_name: - context['partner_name'] = partner_name - - return FeatureToggles.__client.is_enabled(feature_name, - context) - - @staticmethod - def is_enabled_for_expert(feature_name: str, - expert_email: Optional[str] = ''): - context = {} - if expert_email: - context['expertEmails'] = expert_email +from UnleashClient.periodic_tasks import fetch_and_load_features +from UnleashClient.strategies import ApplicationHostname, Default, GradualRolloutRandom, \ + GradualRolloutSessionId, GradualRolloutUserId, UserWithId, RemoteAddress, FlexibleRollout, \ + EnableForDomains, EnableForBusinesses, EnableForPartners, EnableForExperts +from UnleashClient import constants as consts +from UnleashClient.utils import LOGGER +from UnleashClient.loader import load_features +from UnleashClient.deprecation_warnings import strategy_v2xx_deprecation_check, default_value_warning - return FeatureToggles.__client.is_enabled(feature_name, - context) # pylint: disable=dangerous-default-value class UnleashClient(): """ Client implementation. - """ def __init__(self, url: str, app_name: str, + environment: str, + cas_name: str, redis_host: str, redis_port: str, redis_db: str, - environment: str = "default", instance_id: str = "unleash-client-python", refresh_interval: int = 15, metrics_interval: int = 60, @@ -196,7 +36,6 @@ def __init__(self, cache_directory: str = None) -> None: """ A client for the Unleash feature toggle system. - :param url: URL of the unleash server, required. :param app_name: Name of the application using the unleash client, required. :param environment: Name of the environment using the unleash client, optinal & defaults to "default". @@ -212,7 +51,7 @@ def __init__(self, # Configuration self.unleash_url = url.rstrip('\\') self.unleash_app_name = app_name - self.unleash_environment = environment + self.unleash_environment = f'{cas_name}|{environment}' self.unleash_instance_id = instance_id self.unleash_refresh_interval = refresh_interval self.unleash_metrics_interval = metrics_interval @@ -226,19 +65,12 @@ def __init__(self, } # Class objects - self.cache = redis.Redis( + self.cache = redis.Redis( host=redis_host, port=redis_port, db=redis_db ) self.features = {} # type: Dict - self.scheduler = BackgroundScheduler() - self.fl_job = None # type: Job - self.metric_job = None # type: Job - self.cache.set( - consts.METRIC_LAST_SENT_TIME, - pickle.dumps(datetime.now(timezone.utc)) - ) # Mappings default_strategy_mapping = { @@ -250,7 +82,10 @@ def __init__(self, "remoteAddress": RemoteAddress, "userWithId": UserWithId, "flexibleRollout": FlexibleRollout, - "EnableForDomains": EnableForDomains + "EnableForDomains": EnableForDomains, + "EnableForExperts": EnableForExperts, + "EnableForPartners": EnableForPartners, + "EnableForBusinesses": EnableForBusinesses } if custom_strategies: @@ -264,12 +99,10 @@ def __init__(self, def initialize_client(self) -> None: """ Initializes client and starts communication with central unleash server(s). - This kicks off: * Client registration * Provisioning poll * Stats poll - :return: """ # Setup @@ -284,55 +117,18 @@ def initialize_client(self) -> None: "strategy_mapping": self.strategy_mapping } - metrics_args = { - "url": self.unleash_url, - "app_name": self.unleash_app_name, - "instance_id": self.unleash_instance_id, - "custom_headers": self.unleash_custom_headers, - "custom_options": self.unleash_custom_options, - "features": self.features, - "ondisk_cache": self.cache - } - - # Register app - if not self.unleash_disable_registration: - register_client( - self.unleash_url, self.unleash_app_name, self.unleash_instance_id, - self.unleash_metrics_interval, self.unleash_custom_headers, - self.unleash_custom_options, self.strategy_mapping - ) - - fetch_and_load_features(**fl_args) - - # Start periodic jobs - self.scheduler.start() - self.fl_job = self.scheduler.add_job( - fetch_and_load_features, - trigger=IntervalTrigger(seconds=int(self.unleash_refresh_interval)), - kwargs=fl_args - ) - - # if not self.unleash_disable_metrics: - # self.metric_job = self.scheduler.add_job( - # aggregate_and_send_metrics, - # trigger=IntervalTrigger(seconds=int#( self.unleash_metrics_interval)), - # kwargs=metrics_args - # ) + # Disabling the first API call + # fetch_and_load_features(**fl_args) + load_features(self.cache, self.features, self.strategy_mapping) self.is_initialized = True def destroy(self): """ Gracefully shuts down the Unleash client by stopping jobs, stopping the scheduler, and deleting the cache. - You shouldn't need this too much! - :return: """ - self.fl_job.remove() - if self.metric_job: - self.metric_job.remove() - self.scheduler.shutdown() self.cache.delete() @staticmethod @@ -352,10 +148,8 @@ def is_enabled(self, fallback_function: Callable = None) -> bool: """ Checks if a feature toggle is enabled. - Notes: * If client hasn't been initialized yet or an error occurs, flat will default to false. - :param feature_name: Name of the feature :param context: Dictionary with context (e.g. IPs, email) for feature toggle. :param default_value: Allows override of default value. (DEPRECIATED, used fallback_function instead!) @@ -379,17 +173,14 @@ def is_enabled(self, LOGGER.warning("Attempted to get feature_flag %s, but client wasn't initialized!", feature_name) return self._get_fallback_value(fallback_function, feature_name, context) - # pylint: disable=broad-except def get_variant(self, feature_name: str, context: dict = {}) -> dict: """ Checks if a feature toggle is enabled. If so, return variant. - Notes: * If client hasn't been initialized yet or an error occurs, flat will default to false. - :param feature_name: Name of the feature :param context: Dictionary with context (e.g. IPs, email) for feature toggle. :return: Dict with variant and feature flag status. diff --git a/UnleashClient/loader.py b/UnleashClient/loader.py index e8c1f72..f2ead8c 100644 --- a/UnleashClient/loader.py +++ b/UnleashClient/loader.py @@ -34,7 +34,7 @@ def _create_strategies(provisioning: dict, def _create_feature(provisioning: dict, - strategy_mapping: dict) -> Feature: + strategy_mapping: dict) -> Feature: if "strategies" in provisioning.keys(): parsed_strategies = _create_strategies(provisioning, strategy_mapping) else: @@ -69,10 +69,10 @@ def load_features(cache: redis.Redis, # Parse provisioning parsed_features = {} feature_names = [ - d["name"] for d in feature_provisioning["features"] + d["name"] for d in feature_provisioning ] - for provisioning in feature_provisioning["features"]: + for provisioning in feature_provisioning: parsed_features[provisioning["name"]] = provisioning # Delete old features/cache diff --git a/UnleashClient/strategies/EnableForBusinessStrategy.py b/UnleashClient/strategies/EnableForBusinessStrategy.py index 0e5da4e..12b12ed 100644 --- a/UnleashClient/strategies/EnableForBusinessStrategy.py +++ b/UnleashClient/strategies/EnableForBusinessStrategy.py @@ -3,7 +3,7 @@ class EnableForBusinesses(Strategy): def load_provisioning(self) -> list: return [ - x.strip() for x in self.parameters["businessViaNames"].split(',') + x.strip() for x in self.parameters["business_via_names"].split(',') ] def apply(self, context: dict = None) -> bool: @@ -15,8 +15,7 @@ def apply(self, context: dict = None) -> bool: """ default_value = False - if "businessViaNames" in context.keys(): - default_value = context["businessViaNames"] in self.parsed_provisioning + if "business_via_names" in context.keys(): + default_value = context["business_via_names"] in self.parsed_provisioning return default_value - diff --git a/UnleashClient/strategies/EnableForDomainStrategy.py b/UnleashClient/strategies/EnableForDomainStrategy.py index 1cb14c5..03a1547 100644 --- a/UnleashClient/strategies/EnableForDomainStrategy.py +++ b/UnleashClient/strategies/EnableForDomainStrategy.py @@ -2,18 +2,18 @@ class EnableForDomains(Strategy): def load_provisioning(self) -> list: - return [x.strip() for x in self.parameters["domainNames"].split(',')] + return [x.strip() for x in self.parameters["domain_names"].split(',')] def apply(self, context: dict = None) -> bool: """ - Check if feature is enabled for domain or not + Check if feature is enabled for given domain_name or not Args: - context(dict): domain name provided as context + context(dict): domain_name provided as context """ default_value = False - if "domainNames" in context.keys(): - default_value = context["domainNames"] in self.parsed_provisioning + if "domain_names" in context.keys(): + default_value = context["domain_names"] in self.parsed_provisioning return default_value diff --git a/UnleashClient/strategies/EnableForExpertStrategy.py b/UnleashClient/strategies/EnableForExpertStrategy.py index 43b52b4..7e56cbc 100644 --- a/UnleashClient/strategies/EnableForExpertStrategy.py +++ b/UnleashClient/strategies/EnableForExpertStrategy.py @@ -2,7 +2,7 @@ class EnableForExperts(Strategy): def load_provisioning(self) -> list: - return [x.strip() for x in self.parameters["expertEmails"].split(',')] + return [x.strip() for x in self.parameters["expert_emails"].split(',')] def apply(self, context: dict = None) -> bool: """ @@ -13,7 +13,7 @@ def apply(self, context: dict = None) -> bool: """ default_value = False - if "expertEmails" in context.keys(): - default_value = context["expertEmails"] in self.parsed_provisioning + if "expert_emails" in context.keys(): + default_value = context["expert_emails"] in self.parsed_provisioning return default_value diff --git a/UnleashClient/strategies/EnableForPartnerStrategy.py b/UnleashClient/strategies/EnableForPartnerStrategy.py index 4e03ec2..606c07e 100644 --- a/UnleashClient/strategies/EnableForPartnerStrategy.py +++ b/UnleashClient/strategies/EnableForPartnerStrategy.py @@ -1,9 +1,10 @@ + from UnleashClient.strategies import Strategy class EnableForPartners(Strategy): def load_provisioning(self) -> list: return [ - x.strip() for x in self.parameters["partnerNames"].split(',') + x.strip() for x in self.parameters["partner_names"].split(',') ] def apply(self, context: dict = None) -> bool: @@ -15,8 +16,7 @@ def apply(self, context: dict = None) -> bool: """ default_value = False - if "partnerNames" in context.keys(): - default_value = context["partnerNames"] in self.parsed_provisioning + if "partner_names" in context.keys(): + default_value = context["partner_names"] in self.parsed_provisioning return default_value - diff --git a/UnleashClient/strategies/__init__.py b/UnleashClient/strategies/__init__.py index 76d7bb4..1a62c70 100644 --- a/UnleashClient/strategies/__init__.py +++ b/UnleashClient/strategies/__init__.py @@ -8,3 +8,6 @@ from .ApplicationHostname import ApplicationHostname from .FlexibleRolloutStrategy import FlexibleRollout from .EnableForDomainStrategy import EnableForDomains +from .EnableForBusinessStrategy import EnableForBusinesses +from .EnableForExpertStrategy import EnableForExperts +from .EnableForPartnerStrategy import EnableForPartners From 002748816fdad54d999ea04fd8cff7681097b91a Mon Sep 17 00:00:00 2001 From: parvez alam Date: Tue, 8 Jun 2021 02:52:04 +0530 Subject: [PATCH 17/22] edit setup file --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index f208bbd..063d826 100644 --- a/setup.py +++ b/setup.py @@ -7,6 +7,7 @@ def readme(): with open('README.md') as file: return file.read() +# Forked by Parvez Alam setup( name='UnleashClient', From 22130562d09e286d8ebcd8de00dd7d7e72b97693 Mon Sep 17 00:00:00 2001 From: parvez alam Date: Tue, 8 Jun 2021 02:52:29 +0530 Subject: [PATCH 18/22] remove unused directory --- FeatureToggles/__init__.py | 353 ------------------------------------- 1 file changed, 353 deletions(-) delete mode 100644 FeatureToggles/__init__.py diff --git a/FeatureToggles/__init__.py b/FeatureToggles/__init__.py deleted file mode 100644 index 99ee8d6..0000000 --- a/FeatureToggles/__init__.py +++ /dev/null @@ -1,353 +0,0 @@ -# Python Imports -import redis -import pickle -from typing import Dict, Any, Optional - -# Unleash Imports -from UnleashClient import constants as consts -from UnleashClient import UnleashClient -from UnleashClient.utils import LOGGER - - -class FeatureTogglesFromConst: - def __init__(self): - self.feature_toggles_dict = consts.FEATURE_TOGGLES_API_RESPONSE - - def is_enabled(self, feature_name, - app_context: Optional[Dict] = {}) -> bool: - """ - Check if certain feature is enabled in const - Args: - feature_name(str): Name of the feature - app_context(dict): App context to check when certain feature is enabled for given entity - eg: { - "partner_names": "" - } - Returns(bool): True if feature is enabled else False - """ - is_feature_enabled = feature_name in self.feature_toggles_dict - - # If Feature is not enabled then return is_feature_enabled Value - if not is_feature_enabled: - return is_feature_enabled - - if not app_context: # If there's not any app_context then return is_feature_enabled value - return is_feature_enabled - - app_context_parameter_key = list(app_context.keys())[0] - app_context_parameter_value = list(app_context.values())[0] - - feature_data = self.feature_toggles_dict[feature_name] - return app_context_parameter_value in feature_data.get(app_context_parameter_key, []) - - @staticmethod - def fetch_feature_toggles() -> Dict[str, Any]: - """ - Return Feature toggles from const - """ - return consts.FEATURE_TOGGLES_API_RESPONSE - - @staticmethod - def is_enabled_for_partner(feature_name: str, - partner_name: Optional[str] = ''): - context = {} - if partner_name: - context['partner_names'] = partner_name - - return FeatureTogglesFromConst().is_enabled(feature_name, context) - - @staticmethod - def is_enabled_for_expert(feature_name: str, - expert_email: Optional[str] = ''): - context = {} - if expert_email: - context['expert_emails'] = expert_email - - return FeatureTogglesFromConst().is_enabled(feature_name, context) - - @staticmethod - def is_enabled_for_business(feature_name: str, - business_via_name: Optional[str] = ''): - context = {} - if business_via_name: - context['business_via_names'] = business_via_name - - return FeatureTogglesFromConst().is_enabled(feature_name, context) - - @staticmethod - def is_enabled_for_domain(feature_name: str, - domain_name: Optional[str] = ''): - context = {} - if domain_name: - context['domain_names'] = domain_name - - return FeatureTogglesFromConst().is_enabled(feature_name, context) - - -class FeatureToggles: - __client = None - __url = None - __app_name = None - __instance_id = None - __redis_host = None - __redis_port = None - __redis_db = None - __cas_name = None - __environment = None - __cache = None - __enable_toggle_service = True - - @staticmethod - def initialize(url: str, - app_name: str, - instance_id: str, - cas_name: str, - environment: str, - redis_host: str, - redis_port: str, - redis_db: str, - enable_toggle_service: bool = True) -> None: - """ Static access method. """ - if FeatureToggles.__client is None: - FeatureToggles.__url = url - FeatureToggles.__app_name = app_name - FeatureToggles.__instance_id = instance_id - FeatureToggles.__cas_name = cas_name - FeatureToggles.__environment = environment - FeatureToggles.__redis_host = redis_host - FeatureToggles.__redis_port = redis_port - FeatureToggles.__redis_db = redis_db - FeatureToggles.__enable_toggle_service = enable_toggle_service - FeatureToggles.__cache = FeatureToggles.__get_cache() - else: - raise Exception("Client has been already initialized") - - @staticmethod - def __get_cache(): - """ - Create redis connection - """ - if FeatureToggles.__cache is None: - FeatureToggles.__cache = redis.Redis( - host=FeatureToggles.__redis_host, - port=FeatureToggles.__redis_port, - db=FeatureToggles.__redis_db - ) - - return FeatureToggles.__cache - - @staticmethod - def update_cache(data: Dict[str, Any]) -> None: - """ - Update cache data - Args: - data(dict): Feature toggles Data - Returns: - None - """ - if FeatureToggles.__cache is None: - raise Exception( - 'To update cache Feature Toggles class needs to be initialised' - ) - - LOGGER.info(f'Updating the cache data: {data}') - try: - FeatureToggles.__cache.set( - consts.FEATURES_URL, pickle.dumps(data) - ) - except Exception as err: - raise Exception( - f'Exception occured while updating the redis cache: {str(err)}' - ) - LOGGER.info(f'Cache Updatation is Done') - - @staticmethod - def __get_unleash_client(): - """ - Initialize the client if client is None Else Return the established client - """ - if FeatureToggles.__enable_toggle_service: - if FeatureToggles.__client is None: - FeatureToggles.__client = UnleashClient( - url=FeatureToggles.__url, - app_name=FeatureToggles.__app_name, - instance_id=FeatureToggles.__instance_id, - cas_name=FeatureToggles.__cas_name, - environment=FeatureToggles.__environment, - redis_host=FeatureToggles.__redis_host, - redis_port=FeatureToggles.__redis_port, - redis_db=FeatureToggles.__redis_db - ) - FeatureToggles.__client.initialize_client() - else: - FeatureToggles.__client = FeatureTogglesFromConst() - - return FeatureToggles.__client - - @staticmethod - def __get_full_feature_name(feature_name: str): - """ - construct full feature name - Args: - feature_name(str): Feature Name - eg: `enable_language_support` - Returns: - (str): fully constructed feature name including cas and env name - format => '{cas_name}.{environment}.{feature_name}' - eg => 'haptik.production.enable_language_support' - """ - try: - full_feature_name = ( - f'{FeatureToggles.__cas_name}.' - f'{FeatureToggles.__environment}.' - f'{feature_name}' - ) - return full_feature_name - except Exception as err: - raise Exception(f'Error while forming the feature name: {str(err)}') - - @staticmethod - def is_enabled_for_domain(feature_name: str, - domain_name: Optional[str] = ''): - """ - Util method to check whether given feature is enabled or not - Args: - feature_name(str): Name of the feature - domain_name(Optional[str]): Name of the domain - Returns: - (bool): True if Feature is enabled else False - """ - feature_name = FeatureToggles.__get_full_feature_name(feature_name) - - context = {} - if domain_name: - context['domain_names'] = domain_name - - return FeatureToggles.__get_unleash_client().is_enabled(feature_name, - context) - - @staticmethod - def is_enabled_for_partner(feature_name: str, - partner_name: Optional[str] = ''): - """ - Util method to check whether given feature is enabled or not - Args: - feature_name(str): Name of the feature - partner_name(Optional[str]): Name of the Partner - Returns: - (bool): True if Feature is enabled else False - """ - feature_name = FeatureToggles.__get_full_feature_name(feature_name) - - context = {} - if partner_name: - context['partner_names'] = partner_name - - return FeatureToggles.__get_unleash_client().is_enabled(feature_name, - context) - - @staticmethod - def is_enabled_for_business(feature_name: str, - business_via_name: Optional[str] = ''): - """ - Util method to check whether given feature is enabled or not - Args: - feature_name(str): Name of the feature - business_via_name(Optional[str]): Business Via Name - Returns: - (bool): True if Feature is enabled else False - """ - feature_name = FeatureToggles.__get_full_feature_name(feature_name) - - context = {} - if business_via_name: - context['business_via_names'] = business_via_name - - return FeatureToggles.__get_unleash_client().is_enabled(feature_name, - context) - - @staticmethod - def is_enabled_for_expert(feature_name: str, - expert_email: Optional[str] = ''): - """ - Util method to check whether given feature is enabled or not - Args: - feature_name(str): Name of the feature - expert_email(Optional[str]): Expert Emails - Returns: - (bool): True if Feature is enabled else False - """ - feature_name = FeatureToggles.__get_full_feature_name(feature_name) - - context = {} - if expert_email: - context['expert_emails'] = expert_email - - return FeatureToggles.__get_unleash_client().is_enabled(feature_name, - context) - - @staticmethod - def fetch_feature_toggles(): - """ - Returns(Dict): - Feature toggles data - Eg: { - "..": { - "domain_names": [], - "business_via_names": [], - "partner_names": [] - } - } - """ - # TODO: Remove the cas and environment name from the feature toggles while returning the response - feature_toggles = pickle.loads( - FeatureToggles.__cache.get(consts.FEATURES_URL) - ) - response = {} - try: - if feature_toggles: - for feature_toggle in feature_toggles: - full_feature_name = feature_toggle['name'] - # split the feature and get compare the cas and environment name - feature = full_feature_name.split('.') - cas_name = feature[0] - environment = feature[1] - - # Define empty list for empty values - domain_names = [] - partner_names = [] - business_via_names = [] - expert_emails = [] - - if cas_name == FeatureToggles.__cas_name and environment == FeatureToggles.__environment: - # Strip CAS and ENV name from feature name - active_cas_env_name = f'{cas_name}.{environment}.' - full_feature_name = full_feature_name.replace(active_cas_env_name, '') - if full_feature_name not in response: - response[full_feature_name] = {} - strategies = feature_toggle.get('strategies', []) - for strategy in strategies: - strategy_name = strategy.get('name', '') - parameters = strategy.get('parameters', {}) - if strategy_name == 'EnableForPartners': - partner_names = parameters.get('partner_names', '').replace(', ', ',').split(',') - - elif strategy_name == 'EnableForBusinesses': - business_via_names = parameters.get('business_via_names', '').replace(', ', ',').split(',') - elif strategy_name == 'EnableForDomains': - domain_names = parameters.get('domain_names', '').replace(', ', ',').split(',') - elif strategy_name == 'EnableForExperts': - expert_emails = parameters.get('expert_emails', '').replace(', ', ',').split(',') - - # Keep updating this list for new strategies which gets added - - # Assign the strategies data to feature name - response[full_feature_name]['partner_names'] = partner_names - response[full_feature_name]['business_via_names'] = business_via_names - response[full_feature_name]['domain_names'] = domain_names - response[full_feature_name]['expert_emails'] = expert_emails - except Exception as err: - # Handle this exception from where this util gets called - raise Exception(f'Error occured while parsing the response: {str(err)}') - - return response From e4bb823680862e613faa9cdbd81a22dce720d318 Mon Sep 17 00:00:00 2001 From: parvez alam Date: Wed, 9 Jun 2021 14:55:53 +0530 Subject: [PATCH 19/22] Lock package versions --- requirements-dev.txt | 38 +++++++++++++++++++------------------- requirements-local.txt | 38 ++++++++++++++++++++------------------ requirements.txt | 35 ++++++++++++++++++----------------- 3 files changed, 57 insertions(+), 54 deletions(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 210041b..83621af 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,19 +1,19 @@ -coveralls -bumpversion -mimesis -mkdocs -mypy -pur -pylint -pytest -#pytest-benchmark -pytest-cov -pytest-flake8 -pytest-html -pytest-runner -pytest-rerunfailures -pytest-xdist -responses -tox -tox-conda -twine +# TODO: Remove up the package post removing the code which will not get used +bumpversion==0.6.0 +coveralls==3.0.1 +mimesis==2.1.0 +mkdocs==1.1.2 +mypy==0.812 +pur==5.3.0 +pylint==2.7.2 +pytest==6.2.2 +pytest-cov==2.11.1 +pytest-flake8==1.0.7 +pytest-html==1.22.0 +pytest-mock==3.5.1 +pytest-rerunfailures==9.1.1 +pytest-runner==5.3.0 +pytest-xdist==2.2.1 +responses==0.12.1 +tox==3.22.0 +twine==3.3.0 \ No newline at end of file diff --git a/requirements-local.txt b/requirements-local.txt index bb7e445..d005c69 100644 --- a/requirements-local.txt +++ b/requirements-local.txt @@ -3,23 +3,25 @@ requests==2.25.0 fcache==0.4.7 mmh3==2.5.1 APScheduler==3.6.3 +redis==2.10.6 +# TODO: Remove up the package post removing the code which will not get used # Development packages -bumpversion -coveralls -mimesis -mkdocs -mypy -pur -pylint -pytest -pytest-cov -pytest-flake8 -pytest-html -pytest-rerunfailures -pytest-runner -pytest-xdist -responses -tox -tox-conda -twine +bumpversion==0.6.0 +coveralls==3.0.1 +mimesis==2.1.0 +mkdocs==1.1.2 +mypy==0.812 +pur==5.3.0 +pylint==2.7.2 +pytest==6.2.2 +pytest-cov==2.11.1 +pytest-flake8==1.0.7 +pytest-html==1.22.0 +pytest-mock==3.5.1 +pytest-rerunfailures==9.1.1 +pytest-runner==5.3.0 +pytest-xdist==2.2.1 +responses==0.12.1 +tox==3.22.0 +twine==3.3.0 diff --git a/requirements.txt b/requirements.txt index 62af581..496ff0c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,24 +3,25 @@ requests==2.25.0 fcache==0.4.7 mmh3==2.5.1 APScheduler==3.6.3 +redis==2.10.6 +# TODO: Remove up the package post removing the code which will not get used # Development packages -bumpversion -coveralls +bumpversion==0.6.0 +coveralls==3.0.1 mimesis==2.1.0 -mkdocs -mypy -pur -pylint -pytest -pytest-cov -pytest-flake8 +mkdocs==1.1.2 +mypy==0.812 +pur==5.3.0 +pylint==2.7.2 +pytest==6.2.2 +pytest-cov==2.11.1 +pytest-flake8==1.0.7 pytest-html==1.22.0 -pytest-mock -pytest-rerunfailures -pytest-runner -pytest-xdist -responses -tox -twine -redis==2.10.6 +pytest-mock==3.5.1 +pytest-rerunfailures==9.1.1 +pytest-runner==5.3.0 +pytest-xdist==2.2.1 +responses==0.12.1 +tox==3.22.0 +twine==3.3.0 \ No newline at end of file From 89dc829b4992802019867397c18c08b170dc5d74 Mon Sep 17 00:00:00 2001 From: parvez alam Date: Wed, 9 Jun 2021 17:33:07 +0530 Subject: [PATCH 20/22] Remove cas and env name from feature name --- FeatureToggle/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/FeatureToggle/__init__.py b/FeatureToggle/__init__.py index 197e0f4..c8de5d0 100644 --- a/FeatureToggle/__init__.py +++ b/FeatureToggle/__init__.py @@ -232,6 +232,7 @@ def fetch_feature_toggles(): FeatureToggles.__cache.get(consts.FEATURES_URL) ) response = {} + try: if feature_toggles: for feature_toggle in feature_toggles: @@ -249,8 +250,8 @@ def fetch_feature_toggles(): if cas_name == FeatureToggles.__cas_name and environment == FeatureToggles.__environment: # Strip CAS and ENV name from feature name - active_cas_env_name = f'{FeatureToggles.__cas_name}.' - f'{FeatureToggles.__environment}.' + active_cas_env_name = f'{cas_name}.{environment}.' + full_feature_name = full_feature_name.replace(active_cas_env_name, '') full_feature_name = full_feature_name.replace(active_cas_env_name, '') if full_feature_name not in response: response[full_feature_name] = {} From 082e9b2019d2a7ad9bc92818e8050c4b97689664 Mon Sep 17 00:00:00 2001 From: 1a8e <5284980+1a8e@users.noreply.github.com> Date: Wed, 1 Dec 2021 15:14:09 +0530 Subject: [PATCH 21/22] feat(Strategy): Add Team ID based strategy Plus some cleanup. --- FeatureToggle/__init__.py | 46 ++++++++++++++++--- UnleashClient/__init__.py | 6 ++- .../strategies/EnableForBusinessStrategy.py | 1 + .../strategies/EnableForDomainStrategy.py | 1 + .../strategies/EnableForExpertStrategy.py | 1 + .../strategies/EnableForPartnerStrategy.py | 1 + .../strategies/EnableForTeamStrategy.py | 22 +++++++++ UnleashClient/utils.py | 1 + 8 files changed, 70 insertions(+), 9 deletions(-) create mode 100644 UnleashClient/strategies/EnableForTeamStrategy.py diff --git a/FeatureToggle/__init__.py b/FeatureToggle/__init__.py index c8de5d0..514c236 100644 --- a/FeatureToggle/__init__.py +++ b/FeatureToggle/__init__.py @@ -9,6 +9,12 @@ from UnleashClient.utils import LOGGER +def split_and_strip(parameters: str): + return [ + x.strip() for x in parameters.split(',') + ] + + class FeatureToggles: __client = None __url = None @@ -208,6 +214,29 @@ def is_enabled_for_expert(feature_name: str, return FeatureToggles.__get_unleash_client().is_enabled(feature_name, context) + @staticmethod + def is_enabled_for_team(feature_name: str, + team_id: Optional[int] = None): + """ + Util method to check whether given feature is enabled or not + Args: + feature_name(str): feature name + team_id(Optional[str]): list of team IDs + Returns: + (bool): True if feature is enabled else False + """ + feature_name = FeatureToggles.__get_full_feature_name(feature_name) + + context = {} + if team_id: + context['team_ids'] = team_id + + return ( + FeatureToggles + .__get_unleash_client() + .is_enabled(feature_name, context) + ) + @staticmethod def fetch_feature_toggles(): """ @@ -247,6 +276,7 @@ def fetch_feature_toggles(): partner_names = [] business_via_names = [] expert_emails = [] + team_ids = [] if cas_name == FeatureToggles.__cas_name and environment == FeatureToggles.__environment: # Strip CAS and ENV name from feature name @@ -260,14 +290,15 @@ def fetch_feature_toggles(): strategy_name = strategy.get('name', '') parameters = strategy.get('parameters', {}) if strategy_name == 'EnableForPartners': - partner_names = parameters.get('partner_names', '').replace(', ', ',').split(',') - + partner_names = split_and_strip(parameters.get('partner_names', '')) elif strategy_name == 'EnableForBusinesses': - business_via_names = parameters.get('business_via_names', '').replace(', ', ',').split(',') + business_via_names = split_and_strip(parameters.get('business_via_names', '')) elif strategy_name == 'EnableForDomains': - domain_names = parameters.get('domain_names', '').replace(', ', ',').split(',') + domain_names = split_and_strip(parameters.get('domain_names', '')) elif strategy_name == 'EnableForExperts': - expert_emails = parameters.get('expert_emails', '').replace(', ', ',').split(',') + expert_emails = split_and_strip(parameters.get('expert_emails', '')) + elif strategy_name == 'EnableForTeams': + team_ids = split_and_strip(parameters.get('team_ids', '')) # Keep updating this list for new strategies which gets added @@ -276,8 +307,9 @@ def fetch_feature_toggles(): response[full_feature_name]['business_via_names'] = business_via_names response[full_feature_name]['domain_names'] = domain_names response[full_feature_name]['expert_emails'] = expert_emails + response[full_feature_name]['team_ids'] = team_ids except Exception as err: # Handle this exception from where this util gets called - raise Exception(f'Error occured while parsing the response: {str(err)}') + raise Exception(f'An error occurred while parsing the response: {str(err)}') - return response \ No newline at end of file + return response diff --git a/UnleashClient/__init__.py b/UnleashClient/__init__.py index 3e5f5a2..1abcbb2 100644 --- a/UnleashClient/__init__.py +++ b/UnleashClient/__init__.py @@ -7,13 +7,14 @@ GradualRolloutSessionId, GradualRolloutUserId, UserWithId, RemoteAddress, FlexibleRollout, \ EnableForDomains, EnableForBusinesses, EnableForPartners, EnableForExperts from UnleashClient import constants as consts +from UnleashClient.strategies.EnableForTeamStrategy import EnableForTeams from UnleashClient.utils import LOGGER from UnleashClient.loader import load_features from UnleashClient.deprecation_warnings import strategy_v2xx_deprecation_check, default_value_warning # pylint: disable=dangerous-default-value -class UnleashClient(): +class UnleashClient: """ Client implementation. """ @@ -85,7 +86,8 @@ def __init__(self, "EnableForDomains": EnableForDomains, "EnableForExperts": EnableForExperts, "EnableForPartners": EnableForPartners, - "EnableForBusinesses": EnableForBusinesses + "EnableForBusinesses": EnableForBusinesses, + "EnableForTeams": EnableForTeams } if custom_strategies: diff --git a/UnleashClient/strategies/EnableForBusinessStrategy.py b/UnleashClient/strategies/EnableForBusinessStrategy.py index 12b12ed..8685520 100644 --- a/UnleashClient/strategies/EnableForBusinessStrategy.py +++ b/UnleashClient/strategies/EnableForBusinessStrategy.py @@ -1,5 +1,6 @@ from UnleashClient.strategies import Strategy + class EnableForBusinesses(Strategy): def load_provisioning(self) -> list: return [ diff --git a/UnleashClient/strategies/EnableForDomainStrategy.py b/UnleashClient/strategies/EnableForDomainStrategy.py index 03a1547..428e5f0 100644 --- a/UnleashClient/strategies/EnableForDomainStrategy.py +++ b/UnleashClient/strategies/EnableForDomainStrategy.py @@ -1,5 +1,6 @@ from UnleashClient.strategies import Strategy + class EnableForDomains(Strategy): def load_provisioning(self) -> list: return [x.strip() for x in self.parameters["domain_names"].split(',')] diff --git a/UnleashClient/strategies/EnableForExpertStrategy.py b/UnleashClient/strategies/EnableForExpertStrategy.py index 7e56cbc..35aa015 100644 --- a/UnleashClient/strategies/EnableForExpertStrategy.py +++ b/UnleashClient/strategies/EnableForExpertStrategy.py @@ -1,5 +1,6 @@ from UnleashClient.strategies import Strategy + class EnableForExperts(Strategy): def load_provisioning(self) -> list: return [x.strip() for x in self.parameters["expert_emails"].split(',')] diff --git a/UnleashClient/strategies/EnableForPartnerStrategy.py b/UnleashClient/strategies/EnableForPartnerStrategy.py index 606c07e..78e159e 100644 --- a/UnleashClient/strategies/EnableForPartnerStrategy.py +++ b/UnleashClient/strategies/EnableForPartnerStrategy.py @@ -1,6 +1,7 @@ from UnleashClient.strategies import Strategy + class EnableForPartners(Strategy): def load_provisioning(self) -> list: return [ diff --git a/UnleashClient/strategies/EnableForTeamStrategy.py b/UnleashClient/strategies/EnableForTeamStrategy.py new file mode 100644 index 0000000..be3ff55 --- /dev/null +++ b/UnleashClient/strategies/EnableForTeamStrategy.py @@ -0,0 +1,22 @@ +from UnleashClient.strategies import Strategy + + +class EnableForTeams(Strategy): + def load_provisioning(self) -> list: + return [ + x.strip() for x in self.parameters["team_ids"].split(',') + ] + + def apply(self, context: dict = None) -> bool: + """ + Check if feature is enabled for given team or not + + Args: + context(dict): team IDs provided as context + """ + default_value = False + + if "team_ids" in context.keys(): + default_value = context["team_ids"] in self.parsed_provisioning + + return default_value diff --git a/UnleashClient/utils.py b/UnleashClient/utils.py index e1da6b9..888db20 100644 --- a/UnleashClient/utils.py +++ b/UnleashClient/utils.py @@ -4,6 +4,7 @@ LOGGER = logging.getLogger(__name__) + def normalized_hash(identifier: str, activation_group: str, normalizer: int = 100) -> int: From 700722030e06f4df99d5e05b19629ca3e3a4a6f8 Mon Sep 17 00:00:00 2001 From: haptikrajasashtikar Date: Tue, 24 May 2022 17:02:33 +0530 Subject: [PATCH 22/22] remove unnecessary file changes --- unleash-client-python/UnleashClient/loader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/unleash-client-python/UnleashClient/loader.py b/unleash-client-python/UnleashClient/loader.py index f2ead8c..dabde22 100644 --- a/unleash-client-python/UnleashClient/loader.py +++ b/unleash-client-python/UnleashClient/loader.py @@ -34,7 +34,7 @@ def _create_strategies(provisioning: dict, def _create_feature(provisioning: dict, - strategy_mapping: dict) -> Feature: + strategy_mapping: dict) -> Feature: if "strategies" in provisioning.keys(): parsed_strategies = _create_strategies(provisioning, strategy_mapping) else: