From 6a318391ff8cccbbeeaeff441ba1b1533c49f29a Mon Sep 17 00:00:00 2001 From: aliabbasrizvi Date: Fri, 14 Jun 2019 15:15:38 -0700 Subject: [PATCH 1/3] Adding notification for datafile update. Need to write long time pending tests for notification_center. --- optimizely/config_manager.py | 13 ++++++++++--- optimizely/helpers/enums.py | 3 +++ optimizely/optimizely.py | 7 +++++-- tests/test_config_manager.py | 27 ++++++++++++++++++++++----- tests/test_notification_center.py | 12 ++++++++++++ 5 files changed, 52 insertions(+), 10 deletions(-) create mode 100644 tests/test_notification_center.py diff --git a/optimizely/config_manager.py b/optimizely/config_manager.py index ff54cf06..7f658ba5 100644 --- a/optimizely/config_manager.py +++ b/optimizely/config_manager.py @@ -21,7 +21,8 @@ from . import exceptions as optimizely_exceptions from . import logger as optimizely_logger from . import project_config -from .error_handler import NoOpErrorHandler as noop_error_handler +from .error_handler import NoOpErrorHandler +from .notification_center import NotificationCenter from .helpers import enums from .helpers import validator @@ -41,7 +42,7 @@ def __init__(self, error_handler: Provides a handle_error method to handle exceptions. """ self.logger = logger or optimizely_logger.adapt_logger(logger or optimizely_logger.NoOpLogger()) - self.error_handler = error_handler or noop_error_handler + self.error_handler = error_handler or NoOpErrorHandler() @abc.abstractmethod def get_config(self): @@ -57,6 +58,7 @@ def __init__(self, datafile=None, logger=None, error_handler=None, + notification_center=None, skip_json_validation=False): """ Initialize config manager. Datafile has to be provided to use. @@ -64,11 +66,13 @@ def __init__(self, datafile: JSON string representing the Optimizely project. logger: Provides a logger instance. error_handler: Provides a handle_error method to handle exceptions. + notification_center: Notification center to generate config update notification. skip_json_validation: Optional boolean param which allows skipping JSON schema validation upon object invocation. By default JSON schema validation will be performed. """ super(StaticConfigManager, self).__init__(logger=logger, error_handler=error_handler) + self.notification_center = notification_center or NotificationCenter(self.logger) self._config = None self.validate_schema = not skip_json_validation self._set_config(datafile) @@ -108,8 +112,8 @@ def _set_config(self, datafile): if previous_revision == config.get_revision(): return - # TODO(ali): Add notification listener. self._config = config + self.notification_center.send_notifications(enums.NotificationTypes.OPTIMIZELY_CONFIG_UPDATE) self.logger.debug( 'Received new datafile and updated config. ' 'Old revision number: {}. New revision number: {}.'.format(previous_revision, config.get_revision()) @@ -135,6 +139,7 @@ def __init__(self, url_template=None, logger=None, error_handler=None, + notification_center=None, skip_json_validation=False): """ Initialize config manager. One of sdk_key or url has to be set to be able to use. @@ -148,6 +153,7 @@ def __init__(self, determines URL from where to fetch the datafile. logger: Provides a logger instance. error_handler: Provides a handle_error method to handle exceptions. + notification_center: Notification center to generate config update notification. skip_json_validation: Optional boolean param which allows skipping JSON schema validation upon object invocation. By default JSON schema validation will be performed. @@ -155,6 +161,7 @@ def __init__(self, """ super(PollingConfigManager, self).__init__(logger=logger, error_handler=error_handler, + notification_center=notification_center, skip_json_validation=skip_json_validation) self.datafile_url = self.get_datafile_url(sdk_key, url, url_template or enums.ConfigManager.DATAFILE_URL_TEMPLATE) diff --git a/optimizely/helpers/enums.py b/optimizely/helpers/enums.py index 64cd05cb..1e683fb3 100644 --- a/optimizely/helpers/enums.py +++ b/optimizely/helpers/enums.py @@ -119,9 +119,12 @@ class NotificationTypes(object): DECISION notification listener has the following parameters: DecisionNotificationTypes type, str user_id, dict attributes, dict decision_info + OPTIMIZELY_CONFIG_UPDATE notification listener has no associated parameters. + TRACK notification listener has the following parameters: str event_key, str user_id, dict attributes (can be None), event_tags (can be None), Event event """ ACTIVATE = 'ACTIVATE:experiment, user_id, attributes, variation, event' DECISION = 'DECISION:type, user_id, attributes, decision_info' + OPTIMIZELY_CONFIG_UPDATE = 'OPTIMIZELY_CONFIG_UPDATE' TRACK = 'TRACK:event_key, user_id, attributes, event_tags, event' diff --git a/optimizely/optimizely.py b/optimizely/optimizely.py index 925657d9..08b6be77 100644 --- a/optimizely/optimizely.py +++ b/optimizely/optimizely.py @@ -23,7 +23,7 @@ from .event_dispatcher import EventDispatcher as default_event_dispatcher from .helpers import enums from .helpers import validator -from .notification_center import NotificationCenter as notification_center +from .notification_center import NotificationCenter class Optimizely(object): @@ -70,22 +70,25 @@ def __init__(self, self.logger.exception(str(error)) return + self.notification_center = NotificationCenter(self.logger) + if not self.config_manager: if sdk_key: self.config_manager = PollingConfigManager(sdk_key=sdk_key, datafile=datafile, logger=self.logger, error_handler=self.error_handler, + notification_center=self.notification_center, skip_json_validation=skip_json_validation) else: self.config_manager = StaticConfigManager(datafile=datafile, logger=self.logger, error_handler=self.error_handler, + notification_center=self.notification_center, skip_json_validation=skip_json_validation) self.event_builder = event_builder.EventBuilder() self.decision_service = decision_service.DecisionService(self.logger, user_profile_service) - self.notification_center = notification_center(self.logger) def _validate_instantiation_options(self): """ Helper method to validate all instantiation parameters. diff --git a/tests/test_config_manager.py b/tests/test_config_manager.py index 13e5f170..a6fbbd84 100644 --- a/tests/test_config_manager.py +++ b/tests/test_config_manager.py @@ -28,34 +28,45 @@ def test_set_config__success(self): """ Test set_config when datafile is valid. """ test_datafile = json.dumps(self.config_dict_with_features) mock_logger = mock.Mock() + mock_notification_center = mock.Mock() project_config_manager = config_manager.StaticConfigManager(datafile=test_datafile, - logger=mock_logger) + logger=mock_logger, + notification_center=mock_notification_center) project_config_manager._set_config(test_datafile) mock_logger.debug.assert_called_with('Received new datafile and updated config. ' 'Old revision number: None. New revision number: 1.') + mock_notification_center.send_notifications.assert_called_once_with('OPTIMIZELY_CONFIG_UPDATE') def test_set_config__twice(self): """ Test calling set_config twice with same content to ensure config is not updated. """ test_datafile = json.dumps(self.config_dict_with_features) mock_logger = mock.Mock() + mock_notification_center = mock.Mock() project_config_manager = config_manager.StaticConfigManager(datafile=test_datafile, - logger=mock_logger) + logger=mock_logger, + notification_center=mock_notification_center) project_config_manager._set_config(test_datafile) mock_logger.debug.assert_called_with('Received new datafile and updated config. ' 'Old revision number: None. New revision number: 1.') self.assertEqual(1, mock_logger.debug.call_count) + mock_notification_center.send_notifications.assert_called_once_with('OPTIMIZELY_CONFIG_UPDATE') + + mock_logger.reset_mock() + mock_notification_center.reset_mock() # Call set config again and confirm that no new log message denoting config update is there project_config_manager._set_config(test_datafile) - self.assertEqual(1, mock_logger.debug.call_count) + self.assertEqual(0, mock_logger.debug.call_count) + self.assertEqual(0, mock_notification_center.call_count) def test_set_config__schema_validation(self): """ Test set_config calls or does not call schema validation based on skip_json_validation value. """ test_datafile = json.dumps(self.config_dict_with_features) mock_logger = mock.Mock() + mock_notification_center = mock.Mock() # Test that schema is validated. # Note: set_config is called in __init__ itself. @@ -78,9 +89,11 @@ def test_set_config__unsupported_datafile_version(self): test_datafile = json.dumps(self.config_dict_with_features) mock_logger = mock.Mock() + mock_notification_center = mock.Mock() project_config_manager = config_manager.StaticConfigManager(datafile=test_datafile, - logger=mock_logger) + logger=mock_logger, + notification_center=mock_notification_center) invalid_version_datafile = self.config_dict_with_features.copy() invalid_version_datafile['version'] = 'invalid_version' @@ -90,19 +103,23 @@ def test_set_config__unsupported_datafile_version(self): project_config_manager._set_config(test_datafile) mock_logger.error.assert_called_once_with('This version of the Python SDK does not support ' 'the given datafile version: "invalid_version".') + self.assertEqual(0, mock_notification_center.call_count) def test_set_config__invalid_datafile(self): """ Test set_config when datafile is invalid. """ test_datafile = json.dumps(self.config_dict_with_features) mock_logger = mock.Mock() + mock_notification_center = mock.Mock() project_config_manager = config_manager.StaticConfigManager(datafile=test_datafile, - logger=mock_logger) + logger=mock_logger, + notification_center=mock_notification_center) # Call set_config with invalid content project_config_manager._set_config('invalid_datafile') mock_logger.error.assert_called_once_with('Provided "datafile" is in an invalid format.') + self.assertEqual(0, mock_notification_center.call_count) def test_get_config(self): """ Test get_config. """ diff --git a/tests/test_notification_center.py b/tests/test_notification_center.py new file mode 100644 index 00000000..44eb24c7 --- /dev/null +++ b/tests/test_notification_center.py @@ -0,0 +1,12 @@ +# Copyright 2019, Optimizely +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. From 650a28e76630ea1ca311587c9f8f051ff5ddb0d1 Mon Sep 17 00:00:00 2001 From: aliabbasrizvi Date: Fri, 14 Jun 2019 16:13:54 -0700 Subject: [PATCH 2/3] Undoing adding notification_center tests. --- tests/test_notification_center.py | 12 ------------ 1 file changed, 12 deletions(-) delete mode 100644 tests/test_notification_center.py diff --git a/tests/test_notification_center.py b/tests/test_notification_center.py deleted file mode 100644 index 44eb24c7..00000000 --- a/tests/test_notification_center.py +++ /dev/null @@ -1,12 +0,0 @@ -# Copyright 2019, Optimizely -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. From a3d856bee139a75667a459ecc1c904d795c55f72 Mon Sep 17 00:00:00 2001 From: aliabbasrizvi Date: Mon, 17 Jun 2019 10:15:57 -0700 Subject: [PATCH 3/3] Lint fix --- tests/test_config_manager.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_config_manager.py b/tests/test_config_manager.py index a6fbbd84..5aafa361 100644 --- a/tests/test_config_manager.py +++ b/tests/test_config_manager.py @@ -66,7 +66,6 @@ def test_set_config__schema_validation(self): test_datafile = json.dumps(self.config_dict_with_features) mock_logger = mock.Mock() - mock_notification_center = mock.Mock() # Test that schema is validated. # Note: set_config is called in __init__ itself.