Skip to content

Commit

Permalink
feat: Implement get_all_feature_variables and get_feature_variable_json
Browse files Browse the repository at this point in the history
  • Loading branch information
pawels-optimizely committed Apr 21, 2020
1 parent 167758d commit 6bbc7c4
Show file tree
Hide file tree
Showing 7 changed files with 544 additions and 2 deletions.
1 change: 1 addition & 0 deletions optimizely/entities.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ class Type(object):
BOOLEAN = 'boolean'
DOUBLE = 'double'
INTEGER = 'integer'
JSON = 'json'
STRING = 'string'

def __init__(self, id, key, type, defaultValue, **kwargs):
Expand Down
138 changes: 138 additions & 0 deletions optimizely/optimizely.py
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,96 @@ def _get_feature_variable_for_type(
)
return actual_value

def _get_all_feature_variables_for_type(
self, project_config, feature_key, user_id, attributes,
):
""" Helper method to determine value for all variables attached to a feature flag.
Args:
project_config: Instance of ProjectConfig.
feature_key: Key of the feature whose variable's value is being accessed.
user_id: ID for user.
attributes: Dict representing user attributes.
Returns:
Dictionary of all variables. None if:
- Feature key is invalid.
"""
if not validator.is_non_empty_string(feature_key):
self.logger.error(enums.Errors.INVALID_INPUT.format('feature_key'))
return None

if not isinstance(user_id, string_types):
self.logger.error(enums.Errors.INVALID_INPUT.format('user_id'))
return None

if not self._validate_user_inputs(attributes):
return None

feature_flag = project_config.get_feature_from_key(feature_key)
if not feature_flag:
return None

feature_enabled = False
source_info = {}

decision = self.decision_service.get_variation_for_feature(project_config, feature_flag, user_id, attributes)
if decision.variation:

feature_enabled = decision.variation.featureEnabled
if feature_enabled:
self.logger.info(
'Feature "%s" for variation "%s" is enabled.' % (feature_key, decision.variation.key)
)
else:
self.logger.info(
'Feature "%s" for variation "%s" is not enabled.' % (feature_key, decision.variation.key)
)
else:
self.logger.info(
'User "%s" is not in any variation or rollout rule. '
'Returning default value for all variables of feature flag "%s".' % (user_id, feature_key)
)

all_variables = {}
for variable_key in feature_flag.variables:
variable = project_config.get_variable_for_feature(feature_key, variable_key)
variable_value = variable.defaultValue
if feature_enabled:
variable_value = project_config.get_variable_value_for_variation(variable, decision.variation)
self.logger.info(
'Got variable value "%s" for variable "%s" of feature flag "%s".'
% (variable_value, variable_key, feature_key)
)

try:
actual_value = project_config.get_typecast_value(variable_value, variable.type)
except:
self.logger.error('Unable to cast value. Returning None.')
actual_value = None

all_variables[variable_key] = actual_value

if decision.source == enums.DecisionSources.FEATURE_TEST:
source_info = {
'experiment_key': decision.experiment.key,
'variation_key': decision.variation.key,
}

self.notification_center.send_notifications(
enums.NotificationTypes.DECISION,
enums.DecisionNotificationTypes.FEATURE,
user_id,
attributes or {},
{
'feature_key': feature_key,
'feature_enabled': feature_enabled,
'source': decision.source,
'source_info': source_info,
},
)
return all_variables

def activate(self, experiment_key, user_id, attributes=None):
""" Buckets visitor and sends impression event to Optimizely.
Expand Down Expand Up @@ -672,6 +762,54 @@ def get_feature_variable_string(self, feature_key, variable_key, user_id, attrib
project_config, feature_key, variable_key, variable_type, user_id, attributes,
)

def get_feature_variable_json(self, feature_key, variable_key, user_id, attributes=None):
""" Returns value for a certain json variable attached to a feature.
Args:
feature_key: Key of the feature whose variable's value is being accessed.
variable_key: Key of the variable whose value is to be accessed.
user_id: ID for user.
attributes: Dict representing user attributes.
Returns:
Dictionary object of the variable. None if:
- Feature key is invalid.
- Variable key is invalid.
- Mismatch with type of variable.
"""

variable_type = entities.Variable.Type.JSON
project_config = self.config_manager.get_config()
if not project_config:
self.logger.error(enums.Errors.INVALID_PROJECT_CONFIG.format('get_feature_variable_json'))
return None

return self._get_feature_variable_for_type(
project_config, feature_key, variable_key, variable_type, user_id, attributes,
)

def get_all_feature_variables(self, feature_key, user_id, attributes=None):
""" Returns dictionary of all variables attached to a feature.
Args:
feature_key: Key of the feature whose variable's value is being accessed.
user_id: ID for user.
attributes: Dict representing user attributes.
Returns:
Dictionary object of the variable. None if:
- Feature key is invalid.
"""

project_config = self.config_manager.get_config()
if not project_config:
self.logger.error(enums.Errors.INVALID_PROJECT_CONFIG.format('get_all_feature_variables'))
return None

return self._get_all_feature_variables_for_type(
project_config, feature_key, user_id, attributes,
)

def set_forced_variation(self, experiment_key, user_id, variation_key):
""" Force a user into a variation for a given experiment.
Expand Down
7 changes: 7 additions & 0 deletions optimizely/project_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,11 @@ def __init__(self, datafile, logger, error_handler):
)

self.feature_key_map = self._generate_key_map(self.feature_flags, 'key', entities.FeatureFlag)
for feature in self.feature_key_map:
for variable in self.feature_key_map[feature].variables:
sub_type = variable.get('subType', '')
if variable['type'] == entities.Variable.Type.STRING and sub_type == entities.Variable.Type.JSON:
variable['type'] = entities.Variable.Type.JSON

# Dict containing map of experiment ID to feature ID.
# for checking that experiment is a feature experiment or not.
Expand Down Expand Up @@ -177,6 +182,8 @@ def get_typecast_value(self, value, type):
return int(value)
elif type == entities.Variable.Type.DOUBLE:
return float(value)
elif type == entities.Variable.Type.JSON:
return json.loads(value)
else:
return value

Expand Down
6 changes: 6 additions & 0 deletions tests/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ def setUp(self, config_dict='config_dict'):
{'id': '128', 'value': 'prod'},
{'id': '129', 'value': '10.01'},
{'id': '130', 'value': '4242'},
{'id': '132', 'value': '{"test": 122}'},
],
},
{
Expand All @@ -163,6 +164,7 @@ def setUp(self, config_dict='config_dict'):
{'id': '128', 'value': 'staging'},
{'id': '129', 'value': '10.02'},
{'id': '130', 'value': '4243'},
{'id': '132', 'value': '{"test": 123}'},
],
},
],
Expand Down Expand Up @@ -274,6 +276,7 @@ def setUp(self, config_dict='config_dict'):
{'id': '133', 'value': 'Hello audience'},
{'id': '134', 'value': '39.99'},
{'id': '135', 'value': '399'},
{'id': '136', 'value': '{"field": 12}'},
],
},
{
Expand All @@ -285,6 +288,7 @@ def setUp(self, config_dict='config_dict'):
{'id': '133', 'value': 'environment'},
{'id': '134', 'value': '49.99'},
{'id': '135', 'value': '499'},
{'id': '136', 'value': '{"field": 123}'},
],
},
],
Expand Down Expand Up @@ -324,6 +328,7 @@ def setUp(self, config_dict='config_dict'):
{'id': '129', 'key': 'cost', 'defaultValue': '10.99', 'type': 'double'},
{'id': '130', 'key': 'count', 'defaultValue': '999', 'type': 'integer'},
{'id': '131', 'key': 'variable_without_usage', 'defaultValue': '45', 'type': 'integer'},
{'id': '132', 'key': 'object', 'defaultValue': '{"test": 12}', 'type': 'string', 'subType': 'json'},
],
},
{
Expand All @@ -336,6 +341,7 @@ def setUp(self, config_dict='config_dict'):
{'id': '133', 'key': 'message', 'defaultValue': 'Hello', 'type': 'string'},
{'id': '134', 'key': 'price', 'defaultValue': '99.99', 'type': 'double'},
{'id': '135', 'key': 'count', 'defaultValue': '999', 'type': 'integer'},
{'id': '136', 'key': 'object', 'defaultValue': '{"field": 1}', 'type': 'string', 'subType': 'json'},
],
},
{
Expand Down
7 changes: 7 additions & 0 deletions tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,7 @@ def test_init__with_v4_datafile(self):
{'id': '128', 'key': 'environment', 'defaultValue': 'devel', 'type': 'string'},
{'id': '129', 'key': 'number_of_days', 'defaultValue': '192', 'type': 'integer'},
{'id': '130', 'key': 'significance_value', 'defaultValue': '0.00098', 'type': 'double'},
{'id': '131', 'key': 'object', 'defaultValue': '{"field": 12.4}', 'type': 'string', 'subType': 'json'},
],
},
{
Expand Down Expand Up @@ -489,6 +490,7 @@ def test_init__with_v4_datafile(self):
'environment': entities.Variable('128', 'environment', 'string', 'devel'),
'number_of_days': entities.Variable('129', 'number_of_days', 'integer', '192'),
'significance_value': entities.Variable('130', 'significance_value', 'double', '0.00098'),
'object': entities.Variable('131', 'object', 'json', '{"field": 12.4}'),
},
),
'test_feature_in_rollout': entities.FeatureFlag(
Expand Down Expand Up @@ -814,6 +816,7 @@ def test_get_feature_from_key__valid_feature_key(self):
'message': entities.Variable('133', 'message', 'string', 'Hello'),
'price': entities.Variable('134', 'price', 'double', '99.99'),
'count': entities.Variable('135', 'count', 'integer', '999'),
'object': entities.Variable('136', 'object', 'json', '{"field": 1}'),
},
)

Expand Down Expand Up @@ -856,6 +859,7 @@ def test_get_rollout_from_id__valid_rollout_id(self):
{'id': '133', 'value': 'Hello audience'},
{'id': '134', 'value': '39.99'},
{'id': '135', 'value': '399'},
{'id': '136', 'value': '{"field": 12}'},
],
},
{
Expand All @@ -867,6 +871,7 @@ def test_get_rollout_from_id__valid_rollout_id(self):
{'id': '133', 'value': 'environment'},
{'id': '134', 'value': '49.99'},
{'id': '135', 'value': '499'},
{'id': '136', 'value': '{"field": 123}'},
],
},
],
Expand All @@ -893,6 +898,8 @@ def test_get_rollout_from_id__valid_rollout_id(self):
},
],
)

print(json.loads(json.dumps(expected_rollout, default=lambda o: o.__dict__)))
self.assertEqual(expected_rollout, project_config.get_rollout_from_id('211111'))

def test_get_rollout_from_id__invalid_rollout_id(self):
Expand Down

0 comments on commit 6bbc7c4

Please sign in to comment.