Skip to content

Commit f08061f

Browse files
Merge pull request #48 from optimizely/aliabbasrizvi/parse_ff
Introducing variables parsing
2 parents 4db0eae + 6222c08 commit f08061f

File tree

3 files changed

+292
-7
lines changed

3 files changed

+292
-7
lines changed

optimizely/entities.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,22 @@ def __init__(self, id, key, status, audienceIds, variations, forcedVariations,
5858
self.groupPolicy = groupPolicy
5959

6060

61+
class FeatureFlag(BaseEntity):
62+
63+
class Type(object):
64+
BOOLEAN = 'boolean'
65+
DOUBLE = 'double'
66+
INTEGER = 'integer'
67+
STRING = 'string'
68+
69+
70+
def __init__(self, id, key, type, defaultValue, **kwargs):
71+
self.id = id
72+
self.key = key
73+
self.type = type
74+
self.defaultValue = defaultValue
75+
76+
6177
class Group(BaseEntity):
6278

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

7086
class Variation(BaseEntity):
7187

72-
def __init__(self, id, key, **kwargs):
88+
def __init__(self, id, key, variables=None, featureFlagMap=None, **kwargs):
7389
self.id = id
7490
self.key = key
91+
self.variables = variables or []
92+
self.featureFlagMap = featureFlagMap or {}

optimizely/project_config.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,13 +53,15 @@ def __init__(self, datafile, logger, error_handler):
5353
self.events = config.get('events', [])
5454
self.attributes = config.get('attributes', [])
5555
self.audiences = config.get('audiences', [])
56+
self.feature_flags = config.get('variables', [])
5657

5758
# Utility maps for quick lookup
5859
self.group_id_map = self._generate_key_map(self.groups, 'id', entities.Group)
5960
self.experiment_key_map = self._generate_key_map(self.experiments, 'key', entities.Experiment)
6061
self.event_key_map = self._generate_key_map(self.events, 'key', entities.Event)
6162
self.attribute_key_map = self._generate_key_map(self.attributes, 'key', entities.Attribute)
6263
self.audience_id_map = self._generate_key_map(self.audiences, 'id', entities.Audience)
64+
self.feature_flag_id_map = self._generate_key_map(self.feature_flags, 'id', entities.FeatureFlag)
6365
self.audience_id_map = self._deserialize_audience(self.audience_id_map)
6466
for group in self.group_id_map.values():
6567
experiments_in_group_key_map = self._generate_key_map(group.experiments, 'key', entities.Experiment)
@@ -80,6 +82,8 @@ def __init__(self, datafile, logger, error_handler):
8082
)
8183
self.variation_id_map[experiment.key] = {}
8284
for variation in self.variation_key_map.get(experiment.key).values():
85+
feature_flag_to_value_map = self._map_feature_flag_to_value(variation.variables, self.feature_flag_id_map)
86+
variation.featureFlagMap = feature_flag_to_value_map
8387
self.variation_id_map[experiment.key][variation.id] = variation
8488

8589
self.parsing_succeeded = True
@@ -123,6 +127,46 @@ def _deserialize_audience(audience_map):
123127

124128
return audience_map
125129

130+
def _get_typecast_value(self, value, type):
131+
""" Helper method to determine actual value based on type of feature flag.
132+
133+
Args:
134+
value: Value in string form as it was parsed from datafile.
135+
type: Type denoting the feature flag type.
136+
137+
Return:
138+
Value type-casted based on type of feature flag.
139+
"""
140+
141+
if type == entities.FeatureFlag.Type.BOOLEAN:
142+
return value == 'true'
143+
elif type == entities.FeatureFlag.Type.INTEGER:
144+
return int(value)
145+
elif type == entities.FeatureFlag.Type.DOUBLE:
146+
return float(value)
147+
else:
148+
return value
149+
150+
def _map_feature_flag_to_value(self, variables, feature_flag_id_map):
151+
""" Helper method to create map of feature flag key to associated value for a given variation's feature flag set.
152+
153+
Args:
154+
variables: List of dicts representing variables on an instance of Variation object.
155+
feature_flag_id_map: Dict mapping feature flag key to feature flag object.
156+
157+
Returns:
158+
Dict mapping values from feature flag key to value stored on the variation's variable.
159+
"""
160+
161+
feature_flag_value_map = {}
162+
for variable in variables:
163+
feature_flag = feature_flag_id_map[variable.get('id')]
164+
if not feature_flag:
165+
continue
166+
feature_flag_value_map[feature_flag.key] = self._get_typecast_value(variable.get('value'), feature_flag.type)
167+
168+
return feature_flag_value_map
169+
126170
def was_parsing_successful(self):
127171
""" Helper method to determine if parsing the datafile was successful.
128172

tests/test_config.py

Lines changed: 229 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -159,8 +159,8 @@ def test_init(self):
159159
self.assertEqual(expected_variation_key_map, self.project_config.variation_key_map)
160160
self.assertEqual(expected_variation_id_map, self.project_config.variation_id_map)
161161

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

165165
# Adding some additional fields like live variables and IP anonymization
166166
config_dict = {
@@ -172,6 +172,21 @@ def test_init__with_more_fields(self):
172172
'key': 'is_working',
173173
'defaultValue': 'true',
174174
'type': 'boolean',
175+
}, {
176+
'id': '128',
177+
'key': 'environment',
178+
'defaultValue': 'devel',
179+
'type': 'string',
180+
}, {
181+
'id': '129',
182+
'key': 'number_of_days',
183+
'defaultValue': '192',
184+
'type': 'integer',
185+
}, {
186+
'id': '130',
187+
'key': 'significance_value',
188+
'defaultValue': '0.00098',
189+
'type': 'double',
175190
}],
176191
'events': [{
177192
'key': 'test_event',
@@ -230,11 +245,29 @@ def test_init__with_more_fields(self):
230245
'variations': [{
231246
'key': 'group_exp_1_control',
232247
'id': '28901',
233-
'variables': []
248+
'variables': [{
249+
'id': '128',
250+
'value': 'prod'
251+
}, {
252+
'id': '129',
253+
'value': '1772'
254+
}, {
255+
'id': '130',
256+
'value': '1.22992'
257+
}]
234258
}, {
235259
'key': 'group_exp_1_variation',
236260
'id': '28902',
237-
'variables': []
261+
'variables': [{
262+
'id': '128',
263+
'value': 'stage'
264+
}, {
265+
'id': '129',
266+
'value': '112'
267+
}, {
268+
'id': '130',
269+
'value': '1.211'
270+
}]
238271
}],
239272
'forcedVariations': {
240273
'user_1': 'group_exp_1_control',
@@ -276,7 +309,7 @@ def test_init__with_more_fields(self):
276309
}],
277310
'trafficAllocation': [{
278311
'entityId': '32222',
279-
"endOfRange": 3000
312+
'endOfRange': 3000
280313
}, {
281314
'entityId': '32223',
282315
'endOfRange': 7500
@@ -297,7 +330,197 @@ def test_init__with_more_fields(self):
297330
}
298331

299332
test_obj = optimizely.Optimizely(json.dumps(config_dict))
300-
self.assertTrue(test_obj.is_valid)
333+
project_config = test_obj.config
334+
self.assertEqual(config_dict['accountId'], project_config.account_id)
335+
self.assertEqual(config_dict['projectId'], project_config.project_id)
336+
self.assertEqual(config_dict['revision'], project_config.revision)
337+
self.assertEqual(config_dict['experiments'], project_config.experiments)
338+
self.assertEqual(config_dict['events'], project_config.events)
339+
self.assertEqual(config_dict['variables'], project_config.feature_flags)
340+
341+
expected_group_id_map = {
342+
'19228': entities.Group(
343+
config_dict['groups'][0]['id'],
344+
config_dict['groups'][0]['policy'],
345+
config_dict['groups'][0]['experiments'],
346+
config_dict['groups'][0]['trafficAllocation']
347+
)
348+
}
349+
expected_experiment_key_map = {
350+
'test_experiment': entities.Experiment(
351+
'111127', 'test_experiment', 'Running', ['11154'], [{
352+
'key': 'control',
353+
'id': '111128',
354+
'variables': [{
355+
'id': '127',
356+
'value': 'false'
357+
}]
358+
}, {
359+
'key': 'variation',
360+
'id': '111129',
361+
'variables': [{
362+
'id': '127',
363+
'value': 'true'
364+
}]
365+
}], {
366+
'user_1': 'control',
367+
'user_2': 'control'
368+
}, [{
369+
'entityId': '111128',
370+
'endOfRange': 4000
371+
}, {
372+
'entityId': '',
373+
'endOfRange': 5000
374+
}, {
375+
'entityId': '111129',
376+
'endOfRange': 9000
377+
}],
378+
'111182'),
379+
'group_exp_1': entities.Experiment(
380+
'32222', 'group_exp_1', 'Running', [], [{
381+
'key': 'group_exp_1_control',
382+
'id': '28901',
383+
'variables': [{
384+
'id': '128',
385+
'value': 'prod'
386+
}, {
387+
'id': '129',
388+
'value': '1772'
389+
}, {
390+
'id': '130',
391+
'value': '1.22992'
392+
}]
393+
}, {
394+
'key': 'group_exp_1_variation',
395+
'id': '28902',
396+
'variables': [{
397+
'id': '128',
398+
'value': 'stage'
399+
}, {
400+
'id': '129',
401+
'value': '112'
402+
}, {
403+
'id': '130',
404+
'value': '1.211'
405+
}]
406+
}], {
407+
'user_1': 'group_exp_1_control',
408+
'user_2': 'group_exp_1_control'
409+
}, [{
410+
'entityId': '28901',
411+
'endOfRange': 3000
412+
}, {
413+
'entityId': '28902',
414+
'endOfRange': 9000
415+
}], '111183', groupId='19228', groupPolicy='random'
416+
),
417+
'group_exp_2': entities.Experiment(
418+
'32223', 'group_exp_2', 'Running', [], [{
419+
'key': 'group_exp_2_control',
420+
'id': '28905',
421+
'variables': []
422+
}, {
423+
'key': 'group_exp_2_variation',
424+
'id': '28906',
425+
'variables': []
426+
}], {
427+
'user_1': 'group_exp_2_control',
428+
'user_2': 'group_exp_2_control'
429+
}, [{
430+
'entityId': '28905',
431+
'endOfRange': 8000
432+
}, {
433+
'entityId': '28906',
434+
'endOfRange': 10000
435+
}], '111184', groupId='19228', groupPolicy='random'
436+
),
437+
}
438+
expected_experiment_id_map = {
439+
'111127': expected_experiment_key_map.get('test_experiment'),
440+
'32222': expected_experiment_key_map.get('group_exp_1'),
441+
'32223': expected_experiment_key_map.get('group_exp_2')
442+
}
443+
expected_event_key_map = {
444+
'test_event': entities.Event('111095', 'test_event', ['111127']),
445+
'Total Revenue': entities.Event('111096', 'Total Revenue', ['111127'])
446+
}
447+
expected_attribute_key_map = {
448+
'test_attribute': entities.Attribute('111094', 'test_attribute', segmentId='11133')
449+
}
450+
expected_audience_id_map = {
451+
'11154': entities.Audience(
452+
'11154', 'Test attribute users',
453+
'["and", ["or", ["or", {"name": "test_attribute", "type": "custom_attribute", "value": "test_value"}]]]',
454+
conditionStructure=['and', ['or', ['or', 0]]],
455+
conditionList=[['test_attribute', 'test_value']]
456+
)
457+
}
458+
expected_variation_key_map = {
459+
'test_experiment': {
460+
'control': entities.Variation('111128', 'control', [{'id': '127', 'value': 'false'}], {'is_working': False}),
461+
'variation': entities.Variation('111129', 'variation', [{'id': '127', 'value': 'true'}], {'is_working': True})
462+
},
463+
'group_exp_1': {
464+
'group_exp_1_control': entities.Variation(
465+
'28901', 'group_exp_1_control', [
466+
{'id': '128', 'value': 'prod'}, {'id': '129', 'value': '1772'}, {'id': '130', 'value': '1.22992'}], {
467+
'environment': 'prod',
468+
'number_of_days': 1772,
469+
'significance_value': 1.22992
470+
}),
471+
'group_exp_1_variation': entities.Variation(
472+
'28902', 'group_exp_1_variation', [
473+
{'id': '128', 'value': 'stage'}, {'id': '129', 'value': '112'}, {'id': '130', 'value': '1.211'}], {
474+
'environment': 'stage',
475+
'number_of_days': 112,
476+
'significance_value': 1.211
477+
})
478+
},
479+
'group_exp_2': {
480+
'group_exp_2_control': entities.Variation('28905', 'group_exp_2_control'),
481+
'group_exp_2_variation': entities.Variation('28906', 'group_exp_2_variation')
482+
}
483+
}
484+
expected_variation_id_map = {
485+
'test_experiment': {
486+
'111128': entities.Variation('111128', 'control', [{'id': '127', 'value': 'false'}], {'is_working': False}),
487+
'111129': entities.Variation('111129', 'variation', [{'id': '127', 'value': 'true'}], {'is_working': True})
488+
},
489+
'group_exp_1': {
490+
'28901': entities.Variation('28901', 'group_exp_1_control', [
491+
{'id': '128', 'value': 'prod'}, {'id': '129', 'value': '1772'}, {'id': '130', 'value': '1.22992'}], {
492+
'environment': 'prod',
493+
'number_of_days': 1772,
494+
'significance_value': 1.22992
495+
}),
496+
'28902': entities.Variation('28902', 'group_exp_1_variation', [
497+
{'id': '128', 'value': 'stage'}, {'id': '129', 'value': '112'}, {'id': '130', 'value': '1.211'}], {
498+
'environment': 'stage',
499+
'number_of_days': 112,
500+
'significance_value': 1.211
501+
})
502+
},
503+
'group_exp_2': {
504+
'28905': entities.Variation('28905', 'group_exp_2_control'),
505+
'28906': entities.Variation('28906', 'group_exp_2_variation')
506+
}
507+
}
508+
expected_feature_flag_id_map = {
509+
'127': entities.FeatureFlag('127', 'is_working', 'boolean', 'true'),
510+
'128': entities.FeatureFlag('128', 'environment', 'string', 'devel'),
511+
'129': entities.FeatureFlag('129', 'number_of_days', 'integer', '192'),
512+
'130': entities.FeatureFlag('130', 'significance_value', 'double', '0.00098')
513+
}
514+
515+
self.assertEqual(expected_group_id_map, project_config.group_id_map)
516+
self.assertEqual(expected_experiment_key_map, project_config.experiment_key_map)
517+
self.assertEqual(expected_experiment_id_map, project_config.experiment_id_map)
518+
self.assertEqual(expected_event_key_map, project_config.event_key_map)
519+
self.assertEqual(expected_attribute_key_map, project_config.attribute_key_map)
520+
self.assertEqual(expected_audience_id_map, project_config.audience_id_map)
521+
self.assertEqual(expected_variation_key_map, project_config.variation_key_map)
522+
self.assertEqual(expected_variation_id_map, project_config.variation_id_map)
523+
self.assertEqual(expected_feature_flag_id_map, project_config.feature_flag_id_map)
301524

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

0 commit comments

Comments
 (0)