Skip to content

Commit 1ca4fca

Browse files
feat: Implement get_all_feature_variables and get_feature_variable_json (#251)
* feat: Implement get_all_feature_variables and get_feature_variable_json
1 parent 167758d commit 1ca4fca

File tree

8 files changed

+593
-2
lines changed

8 files changed

+593
-2
lines changed

optimizely/entities.py

+1
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ class Type(object):
101101
BOOLEAN = 'boolean'
102102
DOUBLE = 'double'
103103
INTEGER = 'integer'
104+
JSON = 'json'
104105
STRING = 'string'
105106

106107
def __init__(self, id, key, type, defaultValue, **kwargs):

optimizely/helpers/enums.py

+1
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ class DecisionNotificationTypes(object):
7575
FEATURE = 'feature'
7676
FEATURE_TEST = 'feature-test'
7777
FEATURE_VARIABLE = 'feature-variable'
78+
ALL_FEATURE_VARIABLES = 'all-feature-variables'
7879

7980

8081
class DecisionSources(object):

optimizely/optimizely.py

+139
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,97 @@ def _get_feature_variable_for_type(
287287
)
288288
return actual_value
289289

290+
def _get_all_feature_variables_for_type(
291+
self, project_config, feature_key, user_id, attributes,
292+
):
293+
""" Helper method to determine value for all variables attached to a feature flag.
294+
295+
Args:
296+
project_config: Instance of ProjectConfig.
297+
feature_key: Key of the feature whose variable's value is being accessed.
298+
user_id: ID for user.
299+
attributes: Dict representing user attributes.
300+
301+
Returns:
302+
Dictionary of all variables. None if:
303+
- Feature key is invalid.
304+
"""
305+
if not validator.is_non_empty_string(feature_key):
306+
self.logger.error(enums.Errors.INVALID_INPUT.format('feature_key'))
307+
return None
308+
309+
if not isinstance(user_id, string_types):
310+
self.logger.error(enums.Errors.INVALID_INPUT.format('user_id'))
311+
return None
312+
313+
if not self._validate_user_inputs(attributes):
314+
return None
315+
316+
feature_flag = project_config.get_feature_from_key(feature_key)
317+
if not feature_flag:
318+
return None
319+
320+
feature_enabled = False
321+
source_info = {}
322+
323+
decision = self.decision_service.get_variation_for_feature(project_config, feature_flag, user_id, attributes)
324+
if decision.variation:
325+
326+
feature_enabled = decision.variation.featureEnabled
327+
if feature_enabled:
328+
self.logger.info(
329+
'Feature "%s" for variation "%s" is enabled.' % (feature_key, decision.variation.key)
330+
)
331+
else:
332+
self.logger.info(
333+
'Feature "%s" for variation "%s" is not enabled.' % (feature_key, decision.variation.key)
334+
)
335+
else:
336+
self.logger.info(
337+
'User "%s" is not in any variation or rollout rule. '
338+
'Returning default value for all variables of feature flag "%s".' % (user_id, feature_key)
339+
)
340+
341+
all_variables = {}
342+
for variable_key in feature_flag.variables:
343+
variable = project_config.get_variable_for_feature(feature_key, variable_key)
344+
variable_value = variable.defaultValue
345+
if feature_enabled:
346+
variable_value = project_config.get_variable_value_for_variation(variable, decision.variation)
347+
self.logger.debug(
348+
'Got variable value "%s" for variable "%s" of feature flag "%s".'
349+
% (variable_value, variable_key, feature_key)
350+
)
351+
352+
try:
353+
actual_value = project_config.get_typecast_value(variable_value, variable.type)
354+
except:
355+
self.logger.error('Unable to cast value. Returning None.')
356+
actual_value = None
357+
358+
all_variables[variable_key] = actual_value
359+
360+
if decision.source == enums.DecisionSources.FEATURE_TEST:
361+
source_info = {
362+
'experiment_key': decision.experiment.key,
363+
'variation_key': decision.variation.key,
364+
}
365+
366+
self.notification_center.send_notifications(
367+
enums.NotificationTypes.DECISION,
368+
enums.DecisionNotificationTypes.ALL_FEATURE_VARIABLES,
369+
user_id,
370+
attributes or {},
371+
{
372+
'feature_key': feature_key,
373+
'feature_enabled': feature_enabled,
374+
'variable_values': all_variables,
375+
'source': decision.source,
376+
'source_info': source_info,
377+
},
378+
)
379+
return all_variables
380+
290381
def activate(self, experiment_key, user_id, attributes=None):
291382
""" Buckets visitor and sends impression event to Optimizely.
292383
@@ -672,6 +763,54 @@ def get_feature_variable_string(self, feature_key, variable_key, user_id, attrib
672763
project_config, feature_key, variable_key, variable_type, user_id, attributes,
673764
)
674765

766+
def get_feature_variable_json(self, feature_key, variable_key, user_id, attributes=None):
767+
""" Returns value for a certain JSON variable attached to a feature.
768+
769+
Args:
770+
feature_key: Key of the feature whose variable's value is being accessed.
771+
variable_key: Key of the variable whose value is to be accessed.
772+
user_id: ID for user.
773+
attributes: Dict representing user attributes.
774+
775+
Returns:
776+
Dictionary object of the variable. None if:
777+
- Feature key is invalid.
778+
- Variable key is invalid.
779+
- Mismatch with type of variable.
780+
"""
781+
782+
variable_type = entities.Variable.Type.JSON
783+
project_config = self.config_manager.get_config()
784+
if not project_config:
785+
self.logger.error(enums.Errors.INVALID_PROJECT_CONFIG.format('get_feature_variable_json'))
786+
return None
787+
788+
return self._get_feature_variable_for_type(
789+
project_config, feature_key, variable_key, variable_type, user_id, attributes,
790+
)
791+
792+
def get_all_feature_variables(self, feature_key, user_id, attributes=None):
793+
""" Returns dictionary of all variables and their corresponding values in the context of a feature.
794+
795+
Args:
796+
feature_key: Key of the feature whose variable's value is being accessed.
797+
user_id: ID for user.
798+
attributes: Dict representing user attributes.
799+
800+
Returns:
801+
Dictionary mapping variable key to variable value. None if:
802+
- Feature key is invalid.
803+
"""
804+
805+
project_config = self.config_manager.get_config()
806+
if not project_config:
807+
self.logger.error(enums.Errors.INVALID_PROJECT_CONFIG.format('get_all_feature_variables'))
808+
return None
809+
810+
return self._get_all_feature_variables_for_type(
811+
project_config, feature_key, user_id, attributes,
812+
)
813+
675814
def set_forced_variation(self, experiment_key, user_id, variation_key):
676815
""" Force a user into a variation for a given experiment.
677816

optimizely/project_config.py

+11
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,15 @@ def __init__(self, datafile, logger, error_handler):
107107

108108
self.feature_key_map = self._generate_key_map(self.feature_flags, 'key', entities.FeatureFlag)
109109

110+
# As we cannot create json variables in datafile directly, here we convert
111+
# the variables of string type and json subType to json type
112+
# This is needed to fully support json variables
113+
for feature in self.feature_key_map:
114+
for variable in self.feature_key_map[feature].variables:
115+
sub_type = variable.get('subType', '')
116+
if variable['type'] == entities.Variable.Type.STRING and sub_type == entities.Variable.Type.JSON:
117+
variable['type'] = entities.Variable.Type.JSON
118+
110119
# Dict containing map of experiment ID to feature ID.
111120
# for checking that experiment is a feature experiment or not.
112121
self.experiment_feature_map = {}
@@ -177,6 +186,8 @@ def get_typecast_value(self, value, type):
177186
return int(value)
178187
elif type == entities.Variable.Type.DOUBLE:
179188
return float(value)
189+
elif type == entities.Variable.Type.JSON:
190+
return json.loads(value)
180191
else:
181192
return value
182193

tests/base.py

+11
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,8 @@ def setUp(self, config_dict='config_dict'):
152152
{'id': '128', 'value': 'prod'},
153153
{'id': '129', 'value': '10.01'},
154154
{'id': '130', 'value': '4242'},
155+
{'id': '132', 'value': '{"test": 122}'},
156+
{'id': '133', 'value': '{"true_test": 1.3}'},
155157
],
156158
},
157159
{
@@ -163,6 +165,8 @@ def setUp(self, config_dict='config_dict'):
163165
{'id': '128', 'value': 'staging'},
164166
{'id': '129', 'value': '10.02'},
165167
{'id': '130', 'value': '4243'},
168+
{'id': '132', 'value': '{"test": 123}'},
169+
{'id': '133', 'value': '{"true_test": 1.4}'},
166170
],
167171
},
168172
],
@@ -274,6 +278,7 @@ def setUp(self, config_dict='config_dict'):
274278
{'id': '133', 'value': 'Hello audience'},
275279
{'id': '134', 'value': '39.99'},
276280
{'id': '135', 'value': '399'},
281+
{'id': '136', 'value': '{"field": 12}'},
277282
],
278283
},
279284
{
@@ -285,6 +290,7 @@ def setUp(self, config_dict='config_dict'):
285290
{'id': '133', 'value': 'environment'},
286291
{'id': '134', 'value': '49.99'},
287292
{'id': '135', 'value': '499'},
293+
{'id': '136', 'value': '{"field": 123}'},
288294
],
289295
},
290296
],
@@ -324,6 +330,9 @@ def setUp(self, config_dict='config_dict'):
324330
{'id': '129', 'key': 'cost', 'defaultValue': '10.99', 'type': 'double'},
325331
{'id': '130', 'key': 'count', 'defaultValue': '999', 'type': 'integer'},
326332
{'id': '131', 'key': 'variable_without_usage', 'defaultValue': '45', 'type': 'integer'},
333+
{'id': '132', 'key': 'object', 'defaultValue': '{"test": 12}', 'type': 'string',
334+
'subType': 'json'},
335+
{'id': '133', 'key': 'true_object', 'defaultValue': '{"true_test": 23.54}', 'type': 'json'},
327336
],
328337
},
329338
{
@@ -336,6 +345,8 @@ def setUp(self, config_dict='config_dict'):
336345
{'id': '133', 'key': 'message', 'defaultValue': 'Hello', 'type': 'string'},
337346
{'id': '134', 'key': 'price', 'defaultValue': '99.99', 'type': 'double'},
338347
{'id': '135', 'key': 'count', 'defaultValue': '999', 'type': 'integer'},
348+
{'id': '136', 'key': 'object', 'defaultValue': '{"field": 1}', 'type': 'string',
349+
'subType': 'json'},
339350
],
340351
},
341352
{

tests/test_config.py

+7
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,8 @@ def test_init__with_v4_datafile(self):
284284
{'id': '128', 'key': 'environment', 'defaultValue': 'devel', 'type': 'string'},
285285
{'id': '129', 'key': 'number_of_days', 'defaultValue': '192', 'type': 'integer'},
286286
{'id': '130', 'key': 'significance_value', 'defaultValue': '0.00098', 'type': 'double'},
287+
{'id': '131', 'key': 'object', 'defaultValue': '{"field": 12.4}', 'type': 'string',
288+
'subType': 'json'},
287289
],
288290
},
289291
{
@@ -489,6 +491,7 @@ def test_init__with_v4_datafile(self):
489491
'environment': entities.Variable('128', 'environment', 'string', 'devel'),
490492
'number_of_days': entities.Variable('129', 'number_of_days', 'integer', '192'),
491493
'significance_value': entities.Variable('130', 'significance_value', 'double', '0.00098'),
494+
'object': entities.Variable('131', 'object', 'json', '{"field": 12.4}'),
492495
},
493496
),
494497
'test_feature_in_rollout': entities.FeatureFlag(
@@ -814,6 +817,7 @@ def test_get_feature_from_key__valid_feature_key(self):
814817
'message': entities.Variable('133', 'message', 'string', 'Hello'),
815818
'price': entities.Variable('134', 'price', 'double', '99.99'),
816819
'count': entities.Variable('135', 'count', 'integer', '999'),
820+
'object': entities.Variable('136', 'object', 'json', '{"field": 1}'),
817821
},
818822
)
819823

@@ -856,6 +860,7 @@ def test_get_rollout_from_id__valid_rollout_id(self):
856860
{'id': '133', 'value': 'Hello audience'},
857861
{'id': '134', 'value': '39.99'},
858862
{'id': '135', 'value': '399'},
863+
{'id': '136', 'value': '{"field": 12}'},
859864
],
860865
},
861866
{
@@ -867,6 +872,7 @@ def test_get_rollout_from_id__valid_rollout_id(self):
867872
{'id': '133', 'value': 'environment'},
868873
{'id': '134', 'value': '49.99'},
869874
{'id': '135', 'value': '499'},
875+
{'id': '136', 'value': '{"field": 123}'},
870876
],
871877
},
872878
],
@@ -893,6 +899,7 @@ def test_get_rollout_from_id__valid_rollout_id(self):
893899
},
894900
],
895901
)
902+
896903
self.assertEqual(expected_rollout, project_config.get_rollout_from_id('211111'))
897904

898905
def test_get_rollout_from_id__invalid_rollout_id(self):

0 commit comments

Comments
 (0)