diff --git a/optimizely/optimizely_config.py b/optimizely/optimizely_config.py index a5bb7566..4dc90bdc 100644 --- a/optimizely/optimizely_config.py +++ b/optimizely/optimizely_config.py @@ -25,8 +25,8 @@ def __init__(self, revision, experiments_map, features_map, datafile=None, self.experiments_map = experiments_map self.features_map = features_map self._datafile = datafile - self.sdk_key = sdk_key - self.environment_key = environment_key + self.sdk_key = sdk_key or '' + self.environment_key = environment_key or '' self.attributes = attributes or [] self.events = events or [] self.audiences = audiences or [] @@ -125,11 +125,9 @@ def __init__(self, project_config): Merging typed_audiences with audiences from project_config. The typed_audiences has higher precedence. ''' - - typed_audiences = project_config.typed_audiences[:] optly_typed_audiences = [] id_lookup_dict = {} - for typed_audience in typed_audiences: + for typed_audience in project_config.typed_audiences: optly_audience = OptimizelyAudience( typed_audience.get('id'), typed_audience.get('name'), @@ -269,6 +267,8 @@ def _create_lookup_maps(self): self.exp_id_to_feature_map = {} self.feature_key_variable_key_to_variable_map = {} self.feature_key_variable_id_to_variable_map = {} + self.feature_id_variable_id_to_feature_variables_map = {} + self.feature_id_variable_key_to_feature_variables_map = {} for feature in self.feature_flags: for experiment_id in feature['experimentIds']: @@ -283,10 +283,12 @@ def _create_lookup_maps(self): variables_key_map[variable['key']] = opt_variable variables_id_map[variable['id']] = opt_variable + self.feature_id_variable_id_to_feature_variables_map[feature['id']] = variables_id_map + self.feature_id_variable_key_to_feature_variables_map[feature['id']] = variables_key_map self.feature_key_variable_key_to_variable_map[feature['key']] = variables_key_map self.feature_key_variable_id_to_variable_map[feature['key']] = variables_id_map - def _get_variables_map(self, experiment, variation): + def _get_variables_map(self, experiment, variation, feature_id=None): """ Gets variables map for given experiment and variation. Args: @@ -296,23 +298,27 @@ def _get_variables_map(self, experiment, variation): Returns: dict - Map of variable key to OptimizelyVariable for the given variation. """ + variables_map = {} + feature_flag = self.exp_id_to_feature_map.get(experiment['id'], None) - if feature_flag is None: + if feature_flag is None and feature_id is None: return {} # set default variables for each variation - variables_map = {} - variables_map = copy.deepcopy(self.feature_key_variable_key_to_variable_map[feature_flag['key']]) + if feature_id: + variables_map = copy.deepcopy(self.feature_id_variable_key_to_feature_variables_map[feature_id]) + else: + variables_map = copy.deepcopy(self.feature_key_variable_key_to_variable_map[feature_flag['key']]) - # set variation specific variable value if any - if variation.get('featureEnabled'): - for variable in variation.get('variables', []): - feature_variable = self.feature_key_variable_id_to_variable_map[feature_flag['key']][variable['id']] - variables_map[feature_variable.key].value = variable['value'] + # set variation specific variable value if any + if variation.get('featureEnabled'): + for variable in variation.get('variables', []): + feature_variable = self.feature_key_variable_id_to_variable_map[feature_flag['key']][variable['id']] + variables_map[feature_variable.key].value = variable['value'] return variables_map - def _get_variations_map(self, experiment): + def _get_variations_map(self, experiment, feature_id=None): """ Gets variation map for the given experiment. Args: @@ -324,7 +330,7 @@ def _get_variations_map(self, experiment): variations_map = {} for variation in experiment.get('variations', []): - variables_map = self._get_variables_map(experiment, variation) + variables_map = self._get_variables_map(experiment, variation, feature_id) feature_enabled = variation.get('featureEnabled', None) optly_variation = OptimizelyVariation( @@ -394,7 +400,7 @@ def _get_features_map(self, experiments_id_map): for feature in self.feature_flags: - delivery_rules = self._get_delivery_rules(self.rollouts, feature.get('rolloutId')) + delivery_rules = self._get_delivery_rules(self.rollouts, feature.get('rolloutId'), feature['id']) experiment_rules = [] exp_map = {} @@ -415,7 +421,7 @@ def _get_features_map(self, experiments_id_map): return features_map - def _get_delivery_rules(self, rollouts, rollout_id): + def _get_delivery_rules(self, rollouts, rollout_id, feature_id): """ Gets an array of rollouts for the project config returns: @@ -435,12 +441,12 @@ def _get_delivery_rules(self, rollouts, rollout_id): for optly_audience in self.audiences: audiences_map[optly_audience.id] = optly_audience.name - # Get the experiments_map for that rollout - experiments = rollout.get('experiments_map') + # Get the experiments for that rollout + experiments = rollout.get('experiments') if experiments: for experiment in experiments: optly_exp = OptimizelyExperiment( - experiment['id'], experiment['key'], self._get_variations_map(experiment) + experiment['id'], experiment['key'], self._get_variations_map(experiment, feature_id) ) audiences = self.replace_ids_with_names(experiment.get('audienceConditions', []), audiences_map) optly_exp.audiences = audiences diff --git a/tests/test_optimizely_config.py b/tests/test_optimizely_config.py index b7cbbd7b..c37a8434 100644 --- a/tests/test_optimizely_config.py +++ b/tests/test_optimizely_config.py @@ -26,8 +26,8 @@ def setUp(self): self.opt_config_service = optimizely_config.OptimizelyConfigService(self.project_config) self.expected_config = { - 'sdk_key': None, - 'environment_key': None, + 'sdk_key': '', + 'environment_key': '', 'attributes': [{'key': 'test_attribute', 'id': '111094'}], 'events': [{'key': 'test_event', 'experiment_ids': ['111127'], 'id': '111095'}], 'audiences': [ @@ -616,7 +616,177 @@ def setUp(self): 'experiments_map': { }, - 'delivery_rules': [], + 'delivery_rules': [ + { + 'id': '211127', + 'key': '211127', + 'variations_map': { + '211129': { + 'id': '211129', + 'key': '211129', + 'feature_enabled': True, + 'variables_map': { + 'is_running': { + 'id': '132', + 'key': 'is_running', + 'type': 'boolean', + 'value': 'false' + }, + 'message': { + 'id': '133', + 'key': 'message', + 'type': 'string', + 'value': 'Hello' + }, + 'price': { + 'id': '134', + 'key': 'price', + 'type': 'double', + 'value': '99.99' + }, + 'count': { + 'id': '135', + 'key': 'count', + 'type': 'integer', + 'value': '999' + }, + 'object': { + 'id': '136', + 'key': 'object', + 'type': 'json', + 'value': '{"field": 1}' + } + } + }, + '211229': { + 'id': '211229', + 'key': '211229', + 'feature_enabled': False, + 'variables_map': { + 'is_running': { + 'id': '132', + 'key': 'is_running', + 'type': 'boolean', + 'value': 'false' + }, + 'message': { + 'id': '133', + 'key': 'message', + 'type': 'string', + 'value': 'Hello' + }, + 'price': { + 'id': '134', + 'key': 'price', + 'type': 'double', + 'value': '99.99' + }, + 'count': { + 'id': '135', + 'key': 'count', + 'type': 'integer', + 'value': '999' + }, + 'object': { + 'id': '136', + 'key': 'object', + 'type': 'json', + 'value': '{"field": 1}' + } + } + } + }, + 'audiences': '' + }, + { + 'id': '211137', + 'key': '211137', + 'variations_map': { + '211139': { + 'id': '211139', + 'key': '211139', + 'feature_enabled': True, + 'variables_map': { + 'is_running': { + 'id': '132', + 'key': 'is_running', + 'type': 'boolean', + 'value': 'false' + }, + 'message': { + 'id': '133', + 'key': 'message', + 'type': 'string', + 'value': 'Hello' + }, + 'price': { + 'id': '134', + 'key': 'price', + 'type': 'double', + 'value': '99.99' + }, + 'count': { + 'id': '135', + 'key': 'count', + 'type': 'integer', + 'value': '999' + }, + 'object': { + 'id': '136', + 'key': 'object', + 'type': 'json', + 'value': '{"field": 1}' + } + } + } + }, + 'audiences': '' + }, + { + 'id': '211147', + 'key': '211147', + 'variations_map': { + '211149': { + 'id': '211149', + 'key': '211149', + 'feature_enabled': True, + 'variables_map': { + 'is_running': { + 'id': '132', + 'key': 'is_running', + 'type': 'boolean', + 'value': 'false' + }, + 'message': { + 'id': '133', + 'key': 'message', + 'type': 'string', + 'value': 'Hello' + }, + 'price': { + 'id': '134', + 'key': 'price', + 'type': 'double', + 'value': '99.99' + }, + 'count': { + 'id': '135', + 'key': 'count', + 'type': 'integer', + 'value': '999' + }, + 'object': { + 'id': '136', + 'key': 'object', + 'type': 'json', + 'value': '{"field": 1}' + } + } + } + }, + 'audiences': '' + } + ], 'experiment_rules': [], 'id': '91112', 'key': 'test_feature_in_rollout' @@ -704,7 +874,53 @@ def setUp(self): 'audiences': '' } }, - 'delivery_rules': [], + 'delivery_rules': [ + { + 'id': '211127', + 'key': '211127', + 'variations_map': { + '211129': { + 'id': '211129', + 'key': '211129', + 'feature_enabled': True, + 'variables_map': {} + }, + '211229': { + 'id': '211229', + 'key': '211229', + 'feature_enabled': False, + 'variables_map': {} + } + }, + 'audiences': '' + }, + { + 'id': '211137', + 'key': '211137', + 'variations_map': { + '211139': { + 'id': '211139', + 'key': '211139', + 'feature_enabled': True, + 'variables_map': {} + } + }, + 'audiences': '' + }, + { + 'id': '211147', + 'key': '211147', + 'variations_map': { + '211149': { + 'id': '211149', + 'key': '211149', + 'feature_enabled': True, + 'variables_map': {} + } + }, + 'audiences': '' + } + ], 'experiment_rules': [ { 'id': '32223', @@ -780,7 +996,53 @@ def setUp(self): 'audiences': '"Test attribute users 3"' } }, - 'delivery_rules': [], + 'delivery_rules': [ + { + 'id': '211127', + 'key': '211127', + 'variations_map': { + '211129': { + 'id': '211129', + 'key': '211129', + 'feature_enabled': True, + 'variables_map': {} + }, + '211229': { + 'id': '211229', + 'key': '211229', + 'feature_enabled': False, + 'variables_map': {} + } + }, + 'audiences': '' + }, + { + 'id': '211137', + 'key': '211137', + 'variations_map': { + '211139': { + 'id': '211139', + 'key': '211139', + 'feature_enabled': True, + 'variables_map': {} + } + }, + 'audiences': '' + }, + { + 'id': '211147', + 'key': '211147', + 'variations_map': { + '211149': { + 'id': '211149', + 'key': '211149', + 'feature_enabled': True, + 'variables_map': {} + } + }, + 'audiences': '' + } + ], 'experiment_rules': [ { 'id': '42222', @@ -876,7 +1138,53 @@ def setUp(self): 'audiences': '"Test attribute users 3"' } }, - 'delivery_rules': [], + 'delivery_rules': [ + { + 'id': '211127', + 'key': '211127', + 'variations_map': { + '211129': { + 'id': '211129', + 'key': '211129', + 'feature_enabled': True, + 'variables_map': {} + }, + '211229': { + 'id': '211229', + 'key': '211229', + 'feature_enabled': False, + 'variables_map': {} + } + }, + 'audiences': '' + }, + { + 'id': '211137', + 'key': '211137', + 'variations_map': { + '211139': { + 'id': '211139', + 'key': '211139', + 'feature_enabled': True, + 'variables_map': {} + } + }, + 'audiences': '' + }, + { + 'id': '211147', + 'key': '211147', + 'variations_map': { + '211149': { + 'id': '211149', + 'key': '211149', + 'feature_enabled': True, + 'variables_map': {} + } + }, + 'audiences': '' + } + ], 'experiment_rules': [ { 'id': '111134', @@ -1400,7 +1708,8 @@ def test_stringify_audience_conditions_all_cases(self): ["not", ["and", "1", "2"]], ["or", "1", "100000"], ["and", "and"], - ["and"] + ["and"], + ["and", ["or", "1", ["and", "2", "3"]], ["and", "11", ["or", "12", "3"]]] ] audiences_output = [ @@ -1417,7 +1726,8 @@ def test_stringify_audience_conditions_all_cases(self): 'NOT ("us" AND "female")', '"us" OR "100000"', '', - '' + '', + '("us" OR ("female" AND "adult")) AND ("fr" AND ("male" OR "adult"))' ] config_service = optimizely_config.OptimizelyConfigService(config) @@ -1466,3 +1776,15 @@ def test_get_variations_from_experiments_map(self): self.assertEqual(variation.key, 'all_traffic_variation') else: self.assertEqual(variation.key, 'no_traffic_variation') + + def test_get_delivery_rules(self): + expected_features_map_dict = self.expected_config.get('features_map') + actual_features_map_dict = self.actual_config_dict.get('features_map') + actual_features_map = self.actual_config.features_map + + for optly_feature in actual_features_map.values(): + self.assertIsInstance(optly_feature, optimizely_config.OptimizelyFeature) + for delivery_rule in optly_feature.delivery_rules: + self.assertIsInstance(delivery_rule, optimizely_config.OptimizelyExperiment) + + self.assertEqual(expected_features_map_dict, actual_features_map_dict)