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 diff --git a/FeatureToggle/__init__.py b/FeatureToggle/__init__.py new file mode 100644 index 0000000..514c236 --- /dev/null +++ b/FeatureToggle/__init__.py @@ -0,0 +1,315 @@ +# 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 + + +def split_and_strip(parameters: str): + return [ + x.strip() for x in parameters.split(',') + ] + + +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: + 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 + + @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 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(): + """ + 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 + + 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) + ) + 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 = [] + team_ids = [] + + 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, '') + 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 = split_and_strip(parameters.get('partner_names', '')) + elif strategy_name == 'EnableForBusinesses': + business_via_names = split_and_strip(parameters.get('business_via_names', '')) + elif strategy_name == 'EnableForDomains': + domain_names = split_and_strip(parameters.get('domain_names', '')) + elif strategy_name == 'EnableForExperts': + 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 + + # 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 + response[full_feature_name]['team_ids'] = team_ids + except Exception as err: + # Handle this exception from where this util gets called + raise Exception(f'An error occurred while parsing the response: {str(err)}') + + return response diff --git a/UnleashClient/constants.py b/UnleashClient/constants.py new file mode 100644 index 0000000..50d3a7d --- /dev/null +++ b/UnleashClient/constants.py @@ -0,0 +1,47 @@ +# Library +SDK_NAME = "unleash-client-python" +SDK_VERSION = "3.5.0" +REQUEST_TIMEOUT = 30 +METRIC_LAST_SENT_TIME = "mlst" + +# =Unleash= +APPLICATION_HEADERS = {"Content-Type": "application/json"} +DISABLED_VARIATION = { + 'name': 'disabled', + 'enabled': False +} + +# Paths +REGISTER_URL = "/client/register" +FEATURES_URL = "/client/features" +METRICS_URL = "/client/metrics" + + +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"] + } +} + diff --git a/UnleashClient/strategies/EnableForDomainStrategy.py b/UnleashClient/strategies/EnableForDomainStrategy.py new file mode 100644 index 0000000..428e5f0 --- /dev/null +++ b/UnleashClient/strategies/EnableForDomainStrategy.py @@ -0,0 +1,20 @@ +from UnleashClient.strategies import Strategy + + +class EnableForDomains(Strategy): + def load_provisioning(self) -> list: + return [x.strip() for x in self.parameters["domain_names"].split(',')] + + def apply(self, context: dict = None) -> bool: + """ + Check if feature is enabled for given domain_name or not + + Args: + context(dict): domain_name provided as context + """ + default_value = False + + if "domain_names" in context.keys(): + default_value = context["domain_names"] in self.parsed_provisioning + + return default_value diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..83621af --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,19 @@ +# 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 new file mode 100644 index 0000000..d005c69 --- /dev/null +++ b/requirements-local.txt @@ -0,0 +1,27 @@ +# App packages +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==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 new file mode 100644 index 0000000..496ff0c --- /dev/null +++ b/requirements.txt @@ -0,0 +1,27 @@ +# App packages +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==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/setup.py b/setup.py index 2a87067..063d826 100644 --- a/setup.py +++ b/setup.py @@ -1,20 +1,38 @@ -import setuptools +"""Setup file for UnleashClient""" +from setuptools import setup, find_packages -with open("README.md", "r") as fh: - long_description = fh.read() -setuptools.setup( - name="unleash-client", - version="3.6.0", - author="pvz301", - author_email="parvez.alam@haptik.co", - description="Client for unleash server", - long_description=long_description, - url="https://github.com/hellohaptik/unleash-python-client", - packages=setuptools.find_packages(), +def readme(): + """Include README.rst content in PyPi build information""" + with open('README.md') as file: + return file.read() + +# Forked by Parvez Alam + +setup( + name='UnleashClient', + version='3.5.0', + author='Ivan Lee', + author_email='ivanklee86@gmail.com', + description='Python client for the Unleash feature toggle system!', + long_description=readme(), + long_description_content_type="text/markdown", + url='https://github.com/Unleash/unleash-client-python', + packages=find_packages(), + install_requires=["requests==2.25.0", + "fcache==0.4.7", + "mmh3==2.5.1", + "apscheduler==3.6.3"], + tests_require=['pytest', "mimesis", "responses", 'pytest-mock'], + zip_safe=False, + include_package_data=True, classifiers=[ - "Programming Language :: Python :: 3", + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", - "Operating System :: OS Independent", - ], + "Programming Language :: Python :: 3.5", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8" + ] ) diff --git a/unleash-client-python/UnleashClient/__init__.py b/unleash-client-python/UnleashClient/__init__.py index 331dbd1..1abcbb2 100644 --- a/unleash-client-python/UnleashClient/__init__.py +++ b/unleash-client-python/UnleashClient/__init__.py @@ -7,15 +7,16 @@ 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. - """ def __init__(self, url: str, @@ -36,7 +37,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". @@ -86,7 +86,8 @@ def __init__(self, "EnableForDomains": EnableForDomains, "EnableForExperts": EnableForExperts, "EnableForPartners": EnableForPartners, - "EnableForBusinesses": EnableForBusinesses + "EnableForBusinesses": EnableForBusinesses, + "EnableForTeams": EnableForTeams } if custom_strategies: @@ -100,12 +101,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 @@ -122,15 +121,14 @@ def initialize_client(self) -> None: # 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.cache.delete() @@ -152,10 +150,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!) @@ -185,10 +181,8 @@ def get_variant(self, 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/unleash-client-python/UnleashClient/strategies/EnableForBusinessStrategy.py b/unleash-client-python/UnleashClient/strategies/EnableForBusinessStrategy.py index 12b12ed..8685520 100644 --- a/unleash-client-python/UnleashClient/strategies/EnableForBusinessStrategy.py +++ b/unleash-client-python/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/unleash-client-python/UnleashClient/strategies/EnableForExpertStrategy.py b/unleash-client-python/UnleashClient/strategies/EnableForExpertStrategy.py index 7e56cbc..35aa015 100644 --- a/unleash-client-python/UnleashClient/strategies/EnableForExpertStrategy.py +++ b/unleash-client-python/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/unleash-client-python/UnleashClient/strategies/EnableForPartnerStrategy.py b/unleash-client-python/UnleashClient/strategies/EnableForPartnerStrategy.py index c4279ea..78e159e 100644 --- a/unleash-client-python/UnleashClient/strategies/EnableForPartnerStrategy.py +++ b/unleash-client-python/UnleashClient/strategies/EnableForPartnerStrategy.py @@ -1,5 +1,7 @@ + from UnleashClient.strategies import Strategy + class EnableForPartners(Strategy): def load_provisioning(self) -> list: return [ diff --git a/unleash-client-python/UnleashClient/strategies/EnableForTeamStrategy.py b/unleash-client-python/UnleashClient/strategies/EnableForTeamStrategy.py new file mode 100644 index 0000000..be3ff55 --- /dev/null +++ b/unleash-client-python/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/unleash-client-python/UnleashClient/utils.py b/unleash-client-python/UnleashClient/utils.py index e1da6b9..888db20 100644 --- a/unleash-client-python/UnleashClient/utils.py +++ b/unleash-client-python/UnleashClient/utils.py @@ -4,6 +4,7 @@ LOGGER = logging.getLogger(__name__) + def normalized_hash(identifier: str, activation_group: str, normalizer: int = 100) -> int: