diff --git a/.devcontainer/post_install.sh b/.devcontainer/post_install.sh index 2dafbc69..f62680cf 100755 --- a/.devcontainer/post_install.sh +++ b/.devcontainer/post_install.sh @@ -2,11 +2,7 @@ set -ex # Install package -sudo python -m venv .venv -source .venv/bin/activate -pip install -U -r requirements.txt -python setup.py install -./get-spec.sh +sudo su - vscode bash -c "cd /workspaces/unleash-client-python; python -m venv .venv; source .venv/bin/activate; pip install -U -r requirements.txt; python setup.py install; ./get-spec.sh;" # Install pre-config # pip install pre-commit diff --git a/UnleashClient/__init__.py b/UnleashClient/__init__.py index 065f2df5..9cba8fb9 100644 --- a/UnleashClient/__init__.py +++ b/UnleashClient/__init__.py @@ -47,7 +47,7 @@ class UnleashClient: :param scheduler: Custom APScheduler object. Use this if you want to customize jobstore or executors. When unset, UnleashClient will create it's own scheduler. :param scheduler_executor: Name of APSCheduler executor to use if using a custom scheduler. :param multiple_instance_mode: Determines how multiple instances being instantiated is handled by the SDK, when set to InstanceAllowType.BLOCK, the client constructor will fail when more than one instance is detected, when set to InstanceAllowType.WARN, multiple instances will be allowed but log a warning, when set to InstanceAllowType.SILENTLY_ALLOW, no warning or failure will be raised when instantiating multiple instances of the client. Defaults to InstanceAllowType.WARN - :param event_callback: Function to call if impression events are enabled. + :param event_callback: Function to call if impression events are enabled. WARNING: Depending on your event library, this may have performance implications! """ def __init__(self, url: str, @@ -302,16 +302,20 @@ def is_enabled(self, feature = self.features[feature_name] feature_check = feature.is_enabled(context) - if self.unleash_event_callback and feature.impression_data: - event = UnleashEvent( - event_type=UnleashEventType.FEATURE_FLAG, - event_id=uuid.uuid1(), - context=context, - enabled=feature_check, - feature_name=feature_name - ) - - self.unleash_event_callback(event) + try: + if self.unleash_event_callback and feature.impression_data: + event = UnleashEvent( + event_type=UnleashEventType.FEATURE_FLAG, + event_id=uuid.uuid4(), + context=context, + enabled=feature_check, + feature_name=feature_name + ) + + self.unleash_event_callback(event) + except Exception as excep: + LOGGER.log(self.unleash_verbose_log_level, "Error in event callback: %s", excep) + return feature_check return feature_check except Exception as excep: @@ -347,16 +351,20 @@ def get_variant(self, variant_check = feature.get_variant(context) if self.unleash_event_callback and feature.impression_data: - event = UnleashEvent( - event_type=UnleashEventType.VARIANT, - event_id=uuid.uuid1(), - context=context, - enabled=variant_check['enabled'], - feature_name=feature_name, - variant=variant_check['enabled'] - ) - - self.unleash_event_callback(event) + try: + event = UnleashEvent( + event_type=UnleashEventType.VARIANT, + event_id=uuid.uuid4(), + context=context, + enabled=variant_check['enabled'], + feature_name=feature_name, + variant=variant_check['name'] + ) + + self.unleash_event_callback(event) + except Exception as excep: + LOGGER.log(self.unleash_verbose_log_level, "Error in event callback: %s", excep) + return variant_check return variant_check except Exception as excep: diff --git a/UnleashClient/api/features.py b/UnleashClient/api/features.py index a7aff813..cdb46701 100644 --- a/UnleashClient/api/features.py +++ b/UnleashClient/api/features.py @@ -61,7 +61,7 @@ def get_feature_toggles(url: str, if resp.status_code not in [200, 304]: log_resp_info(resp) LOGGER.warning("Unleash Client feature fetch failed due to unexpected HTTP status code.") - raise Exception("Unleash Client feature fetch failed!") # pylint: disable=broad-exception-raised + raise Exception("Unleash Client feature fetch failed!") # pylint: disable=broad-exception-raised etag = '' if 'etag' in resp.headers.keys(): diff --git a/requirements.txt b/requirements.txt index ae465a13..ca23ae1a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -31,3 +31,4 @@ m2r2 tox types-requests twine +blinker diff --git a/tests/unit_tests/test_client.py b/tests/unit_tests/test_client.py index 8714f711..062e8b5a 100644 --- a/tests/unit_tests/test_client.py +++ b/tests/unit_tests/test_client.py @@ -7,6 +7,8 @@ import responses from apscheduler.schedulers.background import BackgroundScheduler from apscheduler.executors.pool import ThreadPoolExecutor +from blinker import signal + from UnleashClient import UnleashClient, INSTANCES from UnleashClient.strategies import Strategy from UnleashClient.utils import InstanceAllowType @@ -17,6 +19,7 @@ from tests.utilities.mocks.mock_all_features import MOCK_ALL_FEATURES from UnleashClient.constants import REGISTER_URL, FEATURES_URL, METRICS_URL from UnleashClient.cache import FileCache +from UnleashClient.events import UnleashEvent, UnleashEventType class EnvironmentStrategy(Strategy): @@ -574,3 +577,48 @@ def test_multiple_instances_are_unique_on_api_key(caplog): UnleashClient(URL, "some-probably-unique-app-name", custom_headers={"Authorization": "penguins"}) UnleashClient(URL, "some-probably-unique-app-name", custom_headers={"Authorization": "hamsters"}) assert not all(["Multiple instances has been disabled" in r.msg for r in caplog.records]) + +@responses.activate +def test_signals_feature_flag(cache): + # Set up API + responses.add(responses.POST, URL + REGISTER_URL, json={}, status=202) + responses.add(responses.GET, URL + FEATURES_URL, json=MOCK_FEATURE_RESPONSE, status=200) + responses.add(responses.POST, URL + METRICS_URL, json={}, status=202) + + # Set up signals + send_data = signal('send-data') + + @send_data.connect + def receive_data(sender, **kw): + print("Caught signal from %r, data %r" % (sender, kw)) + + if kw['data'].event_type == UnleashEventType.FEATURE_FLAG: + assert kw['data'].feature_name == 'testFlag' + assert kw['data'].enabled == True + elif kw['data'].event_type == UnleashEventType.VARIANT: + assert kw['data'].feature_name == 'testVariations' + assert kw['data'].enabled == True + assert kw['data'].variant == 'VarA' + + raise Exception("Random!") + + def example_callback(event: UnleashEvent): + send_data.send('anonymous', data=event) + + # Set up Unleash + unleash_client = UnleashClient( + URL, + APP_NAME, + refresh_interval=REFRESH_INTERVAL, + metrics_interval=METRICS_INTERVAL, + cache=cache, + event_callback=example_callback + ) + + # Create Unleash client and check initial load + unleash_client.initialize_client() + time.sleep(1) + + assert unleash_client.is_enabled("testFlag") + variant = unleash_client.get_variant("testVariations", context={'userId': '2'}) + assert variant['name'] == 'VarA' diff --git a/tests/utilities/mocks/mock_features.py b/tests/utilities/mocks/mock_features.py index e84c7af2..89452947 100644 --- a/tests/utilities/mocks/mock_features.py +++ b/tests/utilities/mocks/mock_features.py @@ -12,7 +12,7 @@ } ], "createdAt": "2018-10-04T01:27:28.477Z", - "impressionData": False + "impressionData": True }, { "name": "testFlag2", @@ -113,7 +113,7 @@ } ], "createdAt": "2019-10-25T13:22:02.035Z", - "impressionData": False + "impressionData": True } ] }