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..5aafa361 100644 --- a/tests/test_config_manager.py +++ b/tests/test_config_manager.py @@ -28,28 +28,38 @@ 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. """ @@ -78,9 +88,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 +102,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. """