Skip to content

Commit

Permalink
Merge pull request #48 from optimizely/aliabbasrizvi/parse_ff
Browse files Browse the repository at this point in the history
Introducing variables parsing
  • Loading branch information
aliabbasrizvi committed Apr 24, 2017
2 parents 4db0eae + 6222c08 commit f08061f
Show file tree
Hide file tree
Showing 3 changed files with 292 additions and 7 deletions.
20 changes: 19 additions & 1 deletion optimizely/entities.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,22 @@ def __init__(self, id, key, status, audienceIds, variations, forcedVariations,
self.groupPolicy = groupPolicy


class FeatureFlag(BaseEntity):

class Type(object):
BOOLEAN = 'boolean'
DOUBLE = 'double'
INTEGER = 'integer'
STRING = 'string'


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


class Group(BaseEntity):

def __init__(self, id, policy, experiments, trafficAllocation, **kwargs):
Expand All @@ -69,6 +85,8 @@ def __init__(self, id, policy, experiments, trafficAllocation, **kwargs):

class Variation(BaseEntity):

def __init__(self, id, key, **kwargs):
def __init__(self, id, key, variables=None, featureFlagMap=None, **kwargs):
self.id = id
self.key = key
self.variables = variables or []
self.featureFlagMap = featureFlagMap or {}
44 changes: 44 additions & 0 deletions optimizely/project_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,15 @@ def __init__(self, datafile, logger, error_handler):
self.events = config.get('events', [])
self.attributes = config.get('attributes', [])
self.audiences = config.get('audiences', [])
self.feature_flags = config.get('variables', [])

# Utility maps for quick lookup
self.group_id_map = self._generate_key_map(self.groups, 'id', entities.Group)
self.experiment_key_map = self._generate_key_map(self.experiments, 'key', entities.Experiment)
self.event_key_map = self._generate_key_map(self.events, 'key', entities.Event)
self.attribute_key_map = self._generate_key_map(self.attributes, 'key', entities.Attribute)
self.audience_id_map = self._generate_key_map(self.audiences, 'id', entities.Audience)
self.feature_flag_id_map = self._generate_key_map(self.feature_flags, 'id', entities.FeatureFlag)
self.audience_id_map = self._deserialize_audience(self.audience_id_map)
for group in self.group_id_map.values():
experiments_in_group_key_map = self._generate_key_map(group.experiments, 'key', entities.Experiment)
Expand All @@ -80,6 +82,8 @@ def __init__(self, datafile, logger, error_handler):
)
self.variation_id_map[experiment.key] = {}
for variation in self.variation_key_map.get(experiment.key).values():
feature_flag_to_value_map = self._map_feature_flag_to_value(variation.variables, self.feature_flag_id_map)
variation.featureFlagMap = feature_flag_to_value_map
self.variation_id_map[experiment.key][variation.id] = variation

self.parsing_succeeded = True
Expand Down Expand Up @@ -123,6 +127,46 @@ def _deserialize_audience(audience_map):

return audience_map

def _get_typecast_value(self, value, type):
""" Helper method to determine actual value based on type of feature flag.
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 flag.
"""

if type == entities.FeatureFlag.Type.BOOLEAN:
return value == 'true'
elif type == entities.FeatureFlag.Type.INTEGER:
return int(value)
elif type == entities.FeatureFlag.Type.DOUBLE:
return float(value)
else:
return value

def _map_feature_flag_to_value(self, variables, feature_flag_id_map):
""" Helper method to create map of feature flag key to associated value for a given variation's feature flag set.
Args:
variables: List of dicts representing variables on an instance of Variation object.
feature_flag_id_map: Dict mapping feature flag key to feature flag object.
Returns:
Dict mapping values from feature flag key to value stored on the variation's variable.
"""

feature_flag_value_map = {}
for variable in variables:
feature_flag = feature_flag_id_map[variable.get('id')]
if not feature_flag:
continue
feature_flag_value_map[feature_flag.key] = self._get_typecast_value(variable.get('value'), feature_flag.type)

return feature_flag_value_map

def was_parsing_successful(self):
""" Helper method to determine if parsing the datafile was successful.
Expand Down
235 changes: 229 additions & 6 deletions tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,8 +159,8 @@ def test_init(self):
self.assertEqual(expected_variation_key_map, self.project_config.variation_key_map)
self.assertEqual(expected_variation_id_map, self.project_config.variation_id_map)

def test_init__with_more_fields(self):
""" Test that no issues occur on creating object with datafile consisting of more fields. """
def test_init__with_v3_datafile(self):
""" Test that on creating object, properties are initiated correctly for version 3 datafile. """

# Adding some additional fields like live variables and IP anonymization
config_dict = {
Expand All @@ -172,6 +172,21 @@ def test_init__with_more_fields(self):
'key': 'is_working',
'defaultValue': 'true',
'type': 'boolean',
}, {
'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',
}],
'events': [{
'key': 'test_event',
Expand Down Expand Up @@ -230,11 +245,29 @@ def test_init__with_more_fields(self):
'variations': [{
'key': 'group_exp_1_control',
'id': '28901',
'variables': []
'variables': [{
'id': '128',
'value': 'prod'
}, {
'id': '129',
'value': '1772'
}, {
'id': '130',
'value': '1.22992'
}]
}, {
'key': 'group_exp_1_variation',
'id': '28902',
'variables': []
'variables': [{
'id': '128',
'value': 'stage'
}, {
'id': '129',
'value': '112'
}, {
'id': '130',
'value': '1.211'
}]
}],
'forcedVariations': {
'user_1': 'group_exp_1_control',
Expand Down Expand Up @@ -276,7 +309,7 @@ def test_init__with_more_fields(self):
}],
'trafficAllocation': [{
'entityId': '32222',
"endOfRange": 3000
'endOfRange': 3000
}, {
'entityId': '32223',
'endOfRange': 7500
Expand All @@ -297,7 +330,197 @@ def test_init__with_more_fields(self):
}

test_obj = optimizely.Optimizely(json.dumps(config_dict))
self.assertTrue(test_obj.is_valid)
project_config = test_obj.config
self.assertEqual(config_dict['accountId'], project_config.account_id)
self.assertEqual(config_dict['projectId'], project_config.project_id)
self.assertEqual(config_dict['revision'], project_config.revision)
self.assertEqual(config_dict['experiments'], project_config.experiments)
self.assertEqual(config_dict['events'], project_config.events)
self.assertEqual(config_dict['variables'], project_config.feature_flags)

expected_group_id_map = {
'19228': entities.Group(
config_dict['groups'][0]['id'],
config_dict['groups'][0]['policy'],
config_dict['groups'][0]['experiments'],
config_dict['groups'][0]['trafficAllocation']
)
}
expected_experiment_key_map = {
'test_experiment': entities.Experiment(
'111127', 'test_experiment', 'Running', ['11154'], [{
'key': 'control',
'id': '111128',
'variables': [{
'id': '127',
'value': 'false'
}]
}, {
'key': 'variation',
'id': '111129',
'variables': [{
'id': '127',
'value': 'true'
}]
}], {
'user_1': 'control',
'user_2': 'control'
}, [{
'entityId': '111128',
'endOfRange': 4000
}, {
'entityId': '',
'endOfRange': 5000
}, {
'entityId': '111129',
'endOfRange': 9000
}],
'111182'),
'group_exp_1': entities.Experiment(
'32222', 'group_exp_1', 'Running', [], [{
'key': 'group_exp_1_control',
'id': '28901',
'variables': [{
'id': '128',
'value': 'prod'
}, {
'id': '129',
'value': '1772'
}, {
'id': '130',
'value': '1.22992'
}]
}, {
'key': 'group_exp_1_variation',
'id': '28902',
'variables': [{
'id': '128',
'value': 'stage'
}, {
'id': '129',
'value': '112'
}, {
'id': '130',
'value': '1.211'
}]
}], {
'user_1': 'group_exp_1_control',
'user_2': 'group_exp_1_control'
}, [{
'entityId': '28901',
'endOfRange': 3000
}, {
'entityId': '28902',
'endOfRange': 9000
}], '111183', groupId='19228', groupPolicy='random'
),
'group_exp_2': entities.Experiment(
'32223', 'group_exp_2', 'Running', [], [{
'key': 'group_exp_2_control',
'id': '28905',
'variables': []
}, {
'key': 'group_exp_2_variation',
'id': '28906',
'variables': []
}], {
'user_1': 'group_exp_2_control',
'user_2': 'group_exp_2_control'
}, [{
'entityId': '28905',
'endOfRange': 8000
}, {
'entityId': '28906',
'endOfRange': 10000
}], '111184', groupId='19228', groupPolicy='random'
),
}
expected_experiment_id_map = {
'111127': expected_experiment_key_map.get('test_experiment'),
'32222': expected_experiment_key_map.get('group_exp_1'),
'32223': expected_experiment_key_map.get('group_exp_2')
}
expected_event_key_map = {
'test_event': entities.Event('111095', 'test_event', ['111127']),
'Total Revenue': entities.Event('111096', 'Total Revenue', ['111127'])
}
expected_attribute_key_map = {
'test_attribute': entities.Attribute('111094', 'test_attribute', segmentId='11133')
}
expected_audience_id_map = {
'11154': entities.Audience(
'11154', 'Test attribute users',
'["and", ["or", ["or", {"name": "test_attribute", "type": "custom_attribute", "value": "test_value"}]]]',
conditionStructure=['and', ['or', ['or', 0]]],
conditionList=[['test_attribute', 'test_value']]
)
}
expected_variation_key_map = {
'test_experiment': {
'control': entities.Variation('111128', 'control', [{'id': '127', 'value': 'false'}], {'is_working': False}),
'variation': entities.Variation('111129', 'variation', [{'id': '127', 'value': 'true'}], {'is_working': True})
},
'group_exp_1': {
'group_exp_1_control': entities.Variation(
'28901', 'group_exp_1_control', [
{'id': '128', 'value': 'prod'}, {'id': '129', 'value': '1772'}, {'id': '130', 'value': '1.22992'}], {
'environment': 'prod',
'number_of_days': 1772,
'significance_value': 1.22992
}),
'group_exp_1_variation': entities.Variation(
'28902', 'group_exp_1_variation', [
{'id': '128', 'value': 'stage'}, {'id': '129', 'value': '112'}, {'id': '130', 'value': '1.211'}], {
'environment': 'stage',
'number_of_days': 112,
'significance_value': 1.211
})
},
'group_exp_2': {
'group_exp_2_control': entities.Variation('28905', 'group_exp_2_control'),
'group_exp_2_variation': entities.Variation('28906', 'group_exp_2_variation')
}
}
expected_variation_id_map = {
'test_experiment': {
'111128': entities.Variation('111128', 'control', [{'id': '127', 'value': 'false'}], {'is_working': False}),
'111129': entities.Variation('111129', 'variation', [{'id': '127', 'value': 'true'}], {'is_working': True})
},
'group_exp_1': {
'28901': entities.Variation('28901', 'group_exp_1_control', [
{'id': '128', 'value': 'prod'}, {'id': '129', 'value': '1772'}, {'id': '130', 'value': '1.22992'}], {
'environment': 'prod',
'number_of_days': 1772,
'significance_value': 1.22992
}),
'28902': entities.Variation('28902', 'group_exp_1_variation', [
{'id': '128', 'value': 'stage'}, {'id': '129', 'value': '112'}, {'id': '130', 'value': '1.211'}], {
'environment': 'stage',
'number_of_days': 112,
'significance_value': 1.211
})
},
'group_exp_2': {
'28905': entities.Variation('28905', 'group_exp_2_control'),
'28906': entities.Variation('28906', 'group_exp_2_variation')
}
}
expected_feature_flag_id_map = {
'127': entities.FeatureFlag('127', 'is_working', 'boolean', 'true'),
'128': entities.FeatureFlag('128', 'environment', 'string', 'devel'),
'129': entities.FeatureFlag('129', 'number_of_days', 'integer', '192'),
'130': entities.FeatureFlag('130', 'significance_value', 'double', '0.00098')
}

self.assertEqual(expected_group_id_map, project_config.group_id_map)
self.assertEqual(expected_experiment_key_map, project_config.experiment_key_map)
self.assertEqual(expected_experiment_id_map, project_config.experiment_id_map)
self.assertEqual(expected_event_key_map, project_config.event_key_map)
self.assertEqual(expected_attribute_key_map, project_config.attribute_key_map)
self.assertEqual(expected_audience_id_map, project_config.audience_id_map)
self.assertEqual(expected_variation_key_map, project_config.variation_key_map)
self.assertEqual(expected_variation_id_map, project_config.variation_id_map)
self.assertEqual(expected_feature_flag_id_map, project_config.feature_flag_id_map)

def test_get_version(self):
""" Test that JSON version is retrieved correctly when using get_version. """
Expand Down

0 comments on commit f08061f

Please sign in to comment.