From ddf06ed4db78491e4877b18ac4c7dff561ddc76c Mon Sep 17 00:00:00 2001 From: Peter Thompson Date: Wed, 1 Jul 2020 10:48:30 -0700 Subject: [PATCH] feat: add datafile accessor (#275) * feat: add datafile accessor * refactor: fetch datafile from project_config in OptimizelyConfigService constructor * test: fix existing test to accommodate new config format due to datafile addition * test: replace deprecated assert functions to eliminate warnings * test: add tests for datafile accessor methods * style: fix indent and spacing of test comments * style: reorder datafile accessor method to more appropriate location * fix: revert back to Regexp b/c python2.7 and pypy were failing * docs: modify comment and incorporate changes * docs: update method comment --- optimizely/optimizely_config.py | 14 +- optimizely/project_config.py | 233 +++++++++++++++++--------------- tests/test_config.py | 12 ++ tests/test_optimizely_config.py | 11 +- 4 files changed, 157 insertions(+), 113 deletions(-) diff --git a/optimizely/optimizely_config.py b/optimizely/optimizely_config.py index 9fcc0948..e429c3c4 100644 --- a/optimizely/optimizely_config.py +++ b/optimizely/optimizely_config.py @@ -17,10 +17,19 @@ class OptimizelyConfig(object): - def __init__(self, revision, experiments_map, features_map): + def __init__(self, revision, experiments_map, features_map, datafile=None): self.revision = revision self.experiments_map = experiments_map self.features_map = features_map + self.datafile = datafile + + def get_datafile(self): + """ Get the datafile associated with OptimizelyConfig. + + Returns: + A JSON string representation of the environment's datafile. + """ + return self.datafile class OptimizelyExperiment(object): @@ -68,6 +77,7 @@ def __init__(self, project_config): self.is_valid = False return + self._datafile = project_config.to_datafile() self.experiments = project_config.experiments self.feature_flags = project_config.feature_flags self.groups = project_config.groups @@ -88,7 +98,7 @@ def get_config(self): experiments_key_map, experiments_id_map = self._get_experiments_maps() features_map = self._get_features_map(experiments_id_map) - return OptimizelyConfig(self.revision, experiments_key_map, features_map) + return OptimizelyConfig(self.revision, experiments_key_map, features_map, self._datafile) def _create_lookup_maps(self): """ Creates lookup maps to avoid redundant iteration of config objects. """ diff --git a/optimizely/project_config.py b/optimizely/project_config.py index 7265dc81..69cdb827 100644 --- a/optimizely/project_config.py +++ b/optimizely/project_config.py @@ -33,13 +33,14 @@ class ProjectConfig(object): def __init__(self, datafile, logger, error_handler): """ ProjectConfig init method to load and set project config data. - Args: - datafile: JSON string representing the project. - logger: Provides a logger instance. - error_handler: Provides a handle_error method to handle exceptions. - """ + Args: + datafile: JSON string representing the project. + logger: Provides a logger instance. + error_handler: Provides a handle_error method to handle exceptions. + """ config = json.loads(datafile) + self._datafile = datafile self.logger = logger self.error_handler = error_handler self.version = config.get('version') @@ -137,14 +138,14 @@ def __init__(self, datafile, logger, error_handler): def _generate_key_map(entity_list, key, entity_class): """ Helper method to generate map from key to entity object for given list of dicts. - Args: - entity_list: List consisting of dict. - key: Key in each dict which will be key in the map. - entity_class: Class representing the entity. + Args: + entity_list: List consisting of dict. + key: Key in each dict which will be key in the map. + entity_class: Class representing the entity. - Returns: - Map mapping key to entity object. - """ + Returns: + Map mapping key to entity object. + """ key_map = {} for obj in entity_list: @@ -156,12 +157,12 @@ def _generate_key_map(entity_list, key, entity_class): def _deserialize_audience(audience_map): """ Helper method to de-serialize and populate audience map with the condition list and structure. - Args: - audience_map: Dict mapping audience ID to audience object. + Args: + audience_map: Dict mapping audience ID to audience object. - Returns: - Dict additionally consisting of condition list and structure on every audience object. - """ + Returns: + Dict additionally consisting of condition list and structure on every audience object. + """ for audience in audience_map.values(): condition_structure, condition_list = condition_helper.loads(audience.conditions) @@ -172,13 +173,13 @@ def _deserialize_audience(audience_map): def get_typecast_value(self, value, type): """ Helper method to determine actual value based on type of feature variable. - Args: - value: Value in string form as it was parsed from datafile. - type: Type denoting the feature flag type. + Args: + value: Value in string form as it was parsed from datafile. + type: Type denoting the feature flag type. - Return: - Value type-casted based on type of feature variable. - """ + Returns: + Value type-casted based on type of feature variable. + """ if type == entities.Variable.Type.BOOLEAN: return value == 'true' @@ -191,51 +192,60 @@ def get_typecast_value(self, value, type): else: return value + def to_datafile(self): + """ Get the datafile corresponding to ProjectConfig. + + Returns: + A JSON string representation of the project datafile. + """ + + return self._datafile + def get_version(self): """ Get version of the datafile. - Returns: - Version of the datafile. - """ + Returns: + Version of the datafile. + """ return self.version def get_revision(self): """ Get revision of the datafile. - Returns: - Revision of the datafile. - """ + Returns: + Revision of the datafile. + """ return self.revision def get_account_id(self): """ Get account ID from the config. - Returns: - Account ID information from the config. - """ + Returns: + Account ID information from the config. + """ return self.account_id def get_project_id(self): """ Get project ID from the config. - Returns: - Project ID information from the config. - """ + Returns: + Project ID information from the config. + """ return self.project_id def get_experiment_from_key(self, experiment_key): """ Get experiment for the provided experiment key. - Args: - experiment_key: Experiment key for which experiment is to be determined. + Args: + experiment_key: Experiment key for which experiment is to be determined. - Returns: - Experiment corresponding to the provided experiment key. - """ + Returns: + Experiment corresponding to the provided experiment key. + """ experiment = self.experiment_key_map.get(experiment_key) @@ -249,12 +259,12 @@ def get_experiment_from_key(self, experiment_key): def get_experiment_from_id(self, experiment_id): """ Get experiment for the provided experiment ID. - Args: - experiment_id: Experiment ID for which experiment is to be determined. + Args: + experiment_id: Experiment ID for which experiment is to be determined. - Returns: - Experiment corresponding to the provided experiment ID. - """ + Returns: + Experiment corresponding to the provided experiment ID. + """ experiment = self.experiment_id_map.get(experiment_id) @@ -268,12 +278,12 @@ def get_experiment_from_id(self, experiment_id): def get_group(self, group_id): """ Get group for the provided group ID. - Args: - group_id: Group ID for which group is to be determined. + Args: + group_id: Group ID for which group is to be determined. - Returns: - Group corresponding to the provided group ID. - """ + Returns: + Group corresponding to the provided group ID. + """ group = self.group_id_map.get(group_id) @@ -287,12 +297,12 @@ def get_group(self, group_id): def get_audience(self, audience_id): """ Get audience object for the provided audience ID. - Args: - audience_id: ID of the audience. + Args: + audience_id: ID of the audience. - Returns: - Dict representing the audience. - """ + Returns: + Dict representing the audience. + """ audience = self.audience_id_map.get(audience_id) if audience: @@ -304,13 +314,13 @@ def get_audience(self, audience_id): def get_variation_from_key(self, experiment_key, variation_key): """ Get variation given experiment and variation key. - Args: - experiment: Key representing parent experiment of variation. - variation_key: Key representing the variation. + Args: + experiment: Key representing parent experiment of variation. + variation_key: Key representing the variation. - Returns - Object representing the variation. - """ + Returns + Object representing the variation. + """ variation_map = self.variation_key_map.get(experiment_key) @@ -330,13 +340,13 @@ def get_variation_from_key(self, experiment_key, variation_key): def get_variation_from_id(self, experiment_key, variation_id): """ Get variation given experiment and variation ID. - Args: - experiment: Key representing parent experiment of variation. - variation_id: ID representing the variation. + Args: + experiment: Key representing parent experiment of variation. + variation_id: ID representing the variation. - Returns - Object representing the variation. - """ + Returns + Object representing the variation. + """ variation_map = self.variation_id_map.get(experiment_key) @@ -356,12 +366,12 @@ def get_variation_from_id(self, experiment_key, variation_id): def get_event(self, event_key): """ Get event for the provided event key. - Args: - event_key: Event key for which event is to be determined. + Args: + event_key: Event key for which event is to be determined. - Returns: - Event corresponding to the provided event key. - """ + Returns: + Event corresponding to the provided event key. + """ event = self.event_key_map.get(event_key) @@ -375,12 +385,12 @@ def get_event(self, event_key): def get_attribute_id(self, attribute_key): """ Get attribute ID for the provided attribute key. - Args: - attribute_key: Attribute key for which attribute is to be fetched. + Args: + attribute_key: Attribute key for which attribute is to be fetched. - Returns: - Attribute ID corresponding to the provided attribute key. - """ + Returns: + Attribute ID corresponding to the provided attribute key. + """ attribute = self.attribute_key_map.get(attribute_key) has_reserved_prefix = attribute_key.startswith(RESERVED_ATTRIBUTE_PREFIX) @@ -406,12 +416,13 @@ def get_attribute_id(self, attribute_key): def get_feature_from_key(self, feature_key): """ Get feature for the provided feature key. - Args: - feature_key: Feature key for which feature is to be fetched. + Args: + feature_key: Feature key for which feature is to be fetched. + + Returns: + Feature corresponding to the provided feature key. + """ - Returns: - Feature corresponding to the provided feature key. - """ feature = self.feature_key_map.get(feature_key) if feature: @@ -423,12 +434,13 @@ def get_feature_from_key(self, feature_key): def get_rollout_from_id(self, rollout_id): """ Get rollout for the provided ID. - Args: - rollout_id: ID of the rollout to be fetched. + Args: + rollout_id: ID of the rollout to be fetched. + + Returns: + Rollout corresponding to the provided ID. + """ - Returns: - Rollout corresponding to the provided ID. - """ layer = self.rollout_id_map.get(rollout_id) if layer: @@ -440,13 +452,13 @@ def get_rollout_from_id(self, rollout_id): def get_variable_value_for_variation(self, variable, variation): """ Get the variable value for the given variation. - Args: - variable: The Variable for which we are getting the value. - variation: The Variation for which we are getting the variable value. + Args: + variable: The Variable for which we are getting the value. + variation: The Variation for which we are getting the variable value. - Returns: - The variable value or None if any of the inputs are invalid. - """ + Returns: + The variable value or None if any of the inputs are invalid. + """ if not variable or not variation: return None @@ -481,13 +493,14 @@ def get_variable_value_for_variation(self, variable, variation): def get_variable_for_feature(self, feature_key, variable_key): """ Get the variable with the given variable key for the given feature. - Args: - feature_key: The key of the feature for which we are getting the variable. - variable_key: The key of the variable we are getting. + Args: + feature_key: The key of the feature for which we are getting the variable. + variable_key: The key of the variable we are getting. + + Returns: + Variable with the given key in the given variation. + """ - Returns: - Variable with the given key in the given variation. - """ feature = self.feature_key_map.get(feature_key) if not feature: self.logger.error('Feature with key "%s" not found in the datafile.' % feature_key) @@ -502,29 +515,29 @@ def get_variable_for_feature(self, feature_key, variable_key): def get_anonymize_ip_value(self): """ Gets the anonymize IP value. - Returns: - A boolean value that indicates if the IP should be anonymized. - """ + Returns: + A boolean value that indicates if the IP should be anonymized. + """ return self.anonymize_ip def get_bot_filtering_value(self): """ Gets the bot filtering value. - Returns: - A boolean value that indicates if bot filtering should be enabled. - """ + Returns: + A boolean value that indicates if bot filtering should be enabled. + """ return self.bot_filtering def is_feature_experiment(self, experiment_id): """ Determines if given experiment is a feature test. - Args: - experiment_id: Experiment ID for which feature test is to be determined. + Args: + experiment_id: Experiment ID for which feature test is to be determined. - Returns: - A boolean value that indicates if given experiment is a feature test. - """ + Returns: + A boolean value that indicates if given experiment is a feature test. + """ return experiment_id in self.experiment_feature_map diff --git a/tests/test_config.py b/tests/test_config.py index 13cf1105..6ef70133 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -985,6 +985,18 @@ def test_get_variable_for_feature__invalid_variable_key(self): self.assertIsNone(project_config.get_variable_for_feature('test_feature_in_experiment', 'invalid_variable_key')) + def test_to_datafile(self): + """ Test that to_datafile returns the expected datafile. """ + + expected_datafile = json.dumps(self.config_dict_with_features) + + opt_obj = optimizely.Optimizely(expected_datafile) + project_config = opt_obj.config_manager.get_config() + + actual_datafile = project_config.to_datafile() + + self.assertEqual(expected_datafile, actual_datafile) + class ConfigLoggingTest(base.BaseTest): def setUp(self): diff --git a/tests/test_optimizely_config.py b/tests/test_optimizely_config.py index 098b6a29..0ccbeb0d 100644 --- a/tests/test_optimizely_config.py +++ b/tests/test_optimizely_config.py @@ -455,7 +455,8 @@ def setUp(self): 'key': 'test_feature_in_experiment_and_rollout' } }, - 'revision': '1' + 'revision': '1', + 'datafile': json.dumps(self.config_dict_with_features) } self.actual_config = self.opt_config_service.get_config() @@ -537,3 +538,11 @@ def test__get_variables_map(self): self.assertIsInstance(variable, optimizely_config.OptimizelyVariable) self.assertEqual(expected_variables_map, self.to_dict(actual_variables_map)) + + def test__get_datafile(self): + """ Test that get_datafile returns the expected datafile. """ + + expected_datafile = json.dumps(self.config_dict_with_features) + actual_datafile = self.actual_config.get_datafile() + + self.assertEqual(expected_datafile, actual_datafile)