Skip to content

Commit db4a9a6

Browse files
Introducing feature variable accessors (#86)
1 parent 9f80f46 commit db4a9a6

File tree

5 files changed

+360
-12
lines changed

5 files changed

+360
-12
lines changed

optimizely/optimizely.py

Lines changed: 135 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,13 @@
1515
import sys
1616

1717
from . import decision_service
18+
from . import entities
1819
from . import event_builder
1920
from . import exceptions
2021
from . import project_config
2122
from .error_handler import NoOpErrorHandler as noop_error_handler
2223
from .event_dispatcher import EventDispatcher as default_event_dispatcher
2324
from .helpers import enums
24-
from .helpers import event_tag_utils
2525
from .helpers import validator
2626
from .logger import NoOpLogger as noop_logger
2727
from .logger import SimpleLogger
@@ -178,6 +178,64 @@ def _send_impression_event(self, experiment, variation, user_id, attributes):
178178
error = sys.exc_info()[1]
179179
self.logger.log(enums.LogLevels.ERROR, 'Unable to dispatch impression event. Error: %s' % str(error))
180180

181+
def _get_feature_variable_for_type(self, feature_key, variable_key, variable_type, user_id, attributes):
182+
""" Helper method to determine value for a certain variable attached to a feature flag based on type of variable.
183+
184+
Args:
185+
feature_key: Key of the feature whose variable's value is being accessed.
186+
variable_key: Key of the variable whose value is to be accessed.
187+
variable_type: Type of variable which could be one of boolean/double/integer/string.
188+
user_id: ID for user.
189+
attributes: Dict representing user attributes.
190+
191+
Returns:
192+
Value of the variable. None if:
193+
- Feature key is invalid.
194+
- Variable key is invalid.
195+
- Mismatch with type of variable.
196+
"""
197+
198+
feature_flag = self.config.get_feature_from_key(feature_key)
199+
if not feature_flag:
200+
return None
201+
202+
variable = self.config.get_variable_for_feature(feature_key, variable_key)
203+
if not variable:
204+
return None
205+
206+
# Return None if type differs
207+
if variable.type != variable_type:
208+
self.logger.log(
209+
enums.LogLevels.WARNING,
210+
'Requested variable type "%s", but variable is of type "%s". '
211+
'Use correct API to retrieve value. Returning None.' % (variable_type, variable.type)
212+
)
213+
return None
214+
215+
decision = self.decision_service.get_variation_for_feature(feature_flag, user_id, attributes)
216+
if decision.variation:
217+
variable_value = self.config.get_variable_value_for_variation(variable, decision.variation)
218+
self.logger.log(
219+
enums.LogLevels.INFO,
220+
'Value for variable "%s" of feature flag "%s" is %s for user "%s".' % (
221+
variable_key, feature_key, variable_value, user_id
222+
))
223+
else:
224+
variable_value = variable.defaultValue
225+
self.logger.log(
226+
enums.LogLevels.INFO,
227+
'User "%s" is not in any variation or rollout rule. '
228+
'Returning default value for variable "%s" of feature flag "%s".' % (user_id, variable_key, feature_key)
229+
)
230+
231+
try:
232+
actual_value = self.config.get_typecast_value(variable_value, variable_type)
233+
except:
234+
self.logger.log(enums.LogLevels.ERROR, 'Unable to cast value. Returning None.')
235+
actual_value = None
236+
237+
return actual_value
238+
181239
def activate(self, experiment_key, user_id, attributes=None):
182240
""" Buckets visitor and sends impression event to Optimizely.
183241
@@ -350,6 +408,82 @@ def get_enabled_features(self, user_id, attributes=None):
350408

351409
return enabled_features
352410

411+
def get_feature_variable_boolean(self, feature_key, variable_key, user_id, attributes=None):
412+
""" Returns value for a certain boolean variable attached to a feature flag.
413+
414+
Args:
415+
feature_key: Key of the feature whose variable's value is being accessed.
416+
variable_key: Key of the variable whose value is to be accessed.
417+
user_id: ID for user.
418+
attributes: Dict representing user attributes.
419+
420+
Returns:
421+
Boolean value of the variable. None if:
422+
- Feature key is invalid.
423+
- Variable key is invalid.
424+
- Mismatch with type of variable.
425+
"""
426+
427+
variable_type = entities.Variable.Type.BOOLEAN
428+
return self._get_feature_variable_for_type(feature_key, variable_key, variable_type, user_id, attributes)
429+
430+
def get_feature_variable_double(self, feature_key, variable_key, user_id, attributes=None):
431+
""" Returns value for a certain double variable attached to a feature flag.
432+
433+
Args:
434+
feature_key: Key of the feature whose variable's value is being accessed.
435+
variable_key: Key of the variable whose value is to be accessed.
436+
user_id: ID for user.
437+
attributes: Dict representing user attributes.
438+
439+
Returns:
440+
Double value of the variable. None if:
441+
- Feature key is invalid.
442+
- Variable key is invalid.
443+
- Mismatch with type of variable.
444+
"""
445+
446+
variable_type = entities.Variable.Type.DOUBLE
447+
return self._get_feature_variable_for_type(feature_key, variable_key, variable_type, user_id, attributes)
448+
449+
def get_feature_variable_integer(self, feature_key, variable_key, user_id, attributes=None):
450+
""" Returns value for a certain integer variable attached to a feature flag.
451+
452+
Args:
453+
feature_key: Key of the feature whose variable's value is being accessed.
454+
variable_key: Key of the variable whose value is to be accessed.
455+
user_id: ID for user.
456+
attributes: Dict representing user attributes.
457+
458+
Returns:
459+
Integer value of the variable. None if:
460+
- Feature key is invalid.
461+
- Variable key is invalid.
462+
- Mismatch with type of variable.
463+
"""
464+
465+
variable_type = entities.Variable.Type.INTEGER
466+
return self._get_feature_variable_for_type(feature_key, variable_key, variable_type, user_id, attributes)
467+
468+
def get_feature_variable_string(self, feature_key, variable_key, user_id, attributes=None):
469+
""" Returns value for a certain string variable attached to a feature.
470+
471+
Args:
472+
feature_key: Key of the feature whose variable's value is being accessed.
473+
variable_key: Key of the variable whose value is to be accessed.
474+
user_id: ID for user.
475+
attributes: Dict representing user attributes.
476+
477+
Returns:
478+
String value of the variable. None if:
479+
- Feature key is invalid.
480+
- Variable key is invalid.
481+
- Mismatch with type of variable.
482+
"""
483+
484+
variable_type = entities.Variable.Type.STRING
485+
return self._get_feature_variable_for_type(feature_key, variable_key, variable_type, user_id, attributes)
486+
353487
def set_forced_variation(self, experiment_key, user_id, variation_key):
354488
""" Force a user into a variation for a given experiment.
355489

optimizely/project_config.py

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ def _deserialize_audience(audience_map):
153153

154154
return audience_map
155155

156-
def _get_typecast_value(self, value, type):
156+
def get_typecast_value(self, value, type):
157157
""" Helper method to determine actual value based on type of feature variable.
158158
159159
Args:
@@ -420,12 +420,13 @@ def get_variable_value_for_variation(self, variable, variation):
420420
""" Get the variable value for the given variation.
421421
422422
Args:
423-
Variable: The Variable for which we are getting the value.
424-
Variation: The Variation for which we are getting the variable value.
423+
variable: The Variable for which we are getting the value.
424+
variation: The Variation for which we are getting the variable value.
425425
426426
Returns:
427-
The type-casted variable value or None if any of the inputs are invalid.
427+
The variable value or None if any of the inputs are invalid.
428428
"""
429+
429430
if not variable or not variation:
430431
return None
431432

@@ -437,13 +438,13 @@ def get_variable_value_for_variation(self, variable, variation):
437438
variable_usages = self.variation_variable_usage_map[variation.id]
438439

439440
# Find usage in given variation
440-
variable_usage = variable_usages[variable.id]
441+
variable_usage = variable_usages.get(variable.id)
441442

442-
value = self._get_typecast_value(variable_usage.value, variable.type)
443-
return value
443+
# Return default value in case there is no variable usage for the variable.
444+
return variable_usage.value if variable_usage else variable.defaultValue
444445

445446
def get_variable_for_feature(self, feature_key, variable_key):
446-
""" Get the variable with the given variable key for the given feature
447+
""" Get the variable with the given variable key for the given feature.
447448
448449
Args:
449450
feature_key: The key of the feature for which we are getting the variable.

tests/base.py

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,10 +176,23 @@ def setUp(self):
176176
'id': '127', 'value': 'false'
177177
}, {
178178
'id': '128', 'value': 'prod'
179+
}, {
180+
'id': '129', 'value': '10.01'
181+
}, {
182+
'id': '130', 'value': '4242'
179183
}]
180184
}, {
181185
'key': 'variation',
182-
'id': '111129'
186+
'id': '111129',
187+
'variables': [{
188+
'id': '127', 'value': 'true'
189+
}, {
190+
'id': '128', 'value': 'staging'
191+
}, {
192+
'id': '129', 'value': '10.02'
193+
}, {
194+
'id': '130', 'value': '4243'
195+
}]
183196
}]
184197
}],
185198
'groups': [{
@@ -324,6 +337,21 @@ def setUp(self):
324337
'key': 'environment',
325338
'defaultValue': 'devel',
326339
'type': 'string',
340+
}, {
341+
'id': '129',
342+
'key': 'cost',
343+
'defaultValue': '10.99',
344+
'type': 'double',
345+
}, {
346+
'id': '130',
347+
'key': 'count',
348+
'defaultValue': '999',
349+
'type': 'integer',
350+
}, {
351+
'id': '131',
352+
'key': 'variable_without_usage',
353+
'defaultValue': '45',
354+
'type': 'integer',
327355
}]
328356
}, {
329357
'id': '91112',

tests/test_config.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1197,14 +1197,14 @@ def test_get_rollout_from_id__valid_rollout_id(self):
11971197
self.assertEqual(expected_rollout, project_config.get_rollout_from_id('211111'))
11981198

11991199
def test_get_variable_value_for_variation__returns_valid_value(self):
1200-
""" Test that the right value and type are returned. """
1200+
""" Test that the right value is returned. """
12011201
opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features))
12021202
project_config = opt_obj.config
12031203

12041204
variation = project_config.get_variation_from_id('test_experiment', '111128')
12051205
is_working_variable = project_config.get_variable_for_feature('test_feature_in_experiment', 'is_working')
12061206
environment_variable = project_config.get_variable_for_feature('test_feature_in_experiment', 'environment')
1207-
self.assertEqual(False, project_config.get_variable_value_for_variation(is_working_variable, variation))
1207+
self.assertEqual('false', project_config.get_variable_value_for_variation(is_working_variable, variation))
12081208
self.assertEqual('prod', project_config.get_variable_value_for_variation(environment_variable, variation))
12091209

12101210
def test_get_variable_value_for_variation__invalid_variable(self):
@@ -1226,6 +1226,17 @@ def test_get_variable_value_for_variation__no_variables_for_variation(self):
12261226
is_working_variable = project_config.get_variable_for_feature('test_feature_in_experiment', 'is_working')
12271227
self.assertIsNone(project_config.get_variable_value_for_variation(is_working_variable, variation))
12281228

1229+
def test_get_variable_value_for_variation__no_usage_of_variable(self):
1230+
""" Test that a variable with no usage will return default value for variable. """
1231+
1232+
opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features))
1233+
project_config = opt_obj.config
1234+
1235+
variation = project_config.get_variation_from_id('test_experiment', '111128')
1236+
variable_without_usage_variable = project_config.get_variable_for_feature('test_feature_in_experiment',
1237+
'variable_without_usage')
1238+
self.assertEqual('45', project_config.get_variable_value_for_variation(variable_without_usage_variable, variation))
1239+
12291240
def test_get_variable_for_feature__returns_valid_variable(self):
12301241
""" Test that the feature variable is returned. """
12311242

0 commit comments

Comments
 (0)