Skip to content

Commit afde471

Browse files
Moving variations to use objects (#24)
1 parent a91117b commit afde471

File tree

8 files changed

+192
-168
lines changed

8 files changed

+192
-168
lines changed

optimizely/bucketer.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -77,14 +77,14 @@ def _find_bucket(self, user_id, parent_id, traffic_allocations):
7777
return None
7878

7979
def bucket(self, experiment, user_id):
80-
""" For a given experiment and bucketing ID determines ID of variation to be shown to user.
80+
""" For a given experiment and bucketing ID determines variation to be shown to user.
8181
8282
Args:
8383
experiment: Object representing the experiment for which user is to be bucketed.
8484
user_id: ID for user.
8585
8686
Returns:
87-
Variation ID for variation in which the visitor with ID user_id will be put in. None if no variation.
87+
Variation in which user with ID user_id will be put in. None if no variation.
8888
"""
8989

9090
if not experiment:
@@ -94,11 +94,11 @@ def bucket(self, experiment, user_id):
9494
forced_variations = experiment.forcedVariations
9595
if forced_variations and user_id in forced_variations:
9696
variation_key = forced_variations.get(user_id)
97-
variation_id = self.config.get_variation_id(experiment.key, variation_key)
98-
if variation_id:
97+
variation = self.config.get_variation_from_key(experiment.key, variation_key)
98+
if variation:
9999
self.config.logger.log(enums.LogLevels.INFO,
100100
'User "%s" is forced in variation "%s".' % (user_id, variation_key))
101-
return variation_id
101+
return variation
102102

103103
# Determine if experiment is in a mutually exclusive group
104104
if experiment.groupPolicy in GROUP_POLICIES:
@@ -123,10 +123,10 @@ def bucket(self, experiment, user_id):
123123
# Bucket user if not in white-list and in group (if any)
124124
variation_id = self._find_bucket(user_id, experiment.id, experiment.trafficAllocation)
125125
if variation_id:
126-
variation_key = self.config.get_variation_key_from_id(experiment.key, variation_id)
126+
variation = self.config.get_variation_from_id(experiment.key, variation_id)
127127
self.config.logger.log(enums.LogLevels.INFO, 'User "%s" is in variation "%s" of experiment %s.' %
128-
(user_id, variation_key, experiment.key))
129-
return variation_id
128+
(user_id, variation.key, experiment.key))
129+
return variation
130130

131131
self.config.logger.log(enums.LogLevels.INFO, 'User "%s" is in no variation.' % user_id)
132132
return None

optimizely/entities.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,3 +53,10 @@ def __init__(self, id, policy, experiments, trafficAllocation):
5353
self.policy = policy
5454
self.experiments = experiments
5555
self.trafficAllocation = trafficAllocation
56+
57+
58+
class Variation(BaseEntity):
59+
60+
def __init__(self, id, key):
61+
self.id = id
62+
self.key = key

optimizely/event_builder.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -163,10 +163,10 @@ def _add_experiment_variation_params(self, user_id, valid_experiments):
163163
"""
164164

165165
for experiment in valid_experiments:
166-
variation_id = self.bucketer.bucket(experiment, user_id)
167-
if variation_id:
166+
variation = self.bucketer.bucket(experiment, user_id)
167+
if variation:
168168
self.params[self.EXPERIMENT_PARAM_FORMAT.format(experiment_prefix=self.EventParams.EXPERIMENT_PREFIX,
169-
experiment_id=experiment.id)] = variation_id
169+
experiment_id=experiment.id)] = variation.id
170170

171171
def _add_conversion_goal(self, event_key, event_value):
172172
""" Add conversion goal information to the event.
@@ -337,14 +337,14 @@ def _add_required_params_for_conversion(self, event_key, user_id, event_value, v
337337

338338
self.params[self.EventParams.LAYER_STATES] = []
339339
for experiment in valid_experiments:
340-
variation_id = self.bucketer.bucket(experiment, user_id)
341-
if variation_id:
340+
variation = self.bucketer.bucket(experiment, user_id)
341+
if variation:
342342
self.params[self.EventParams.LAYER_STATES].append({
343343
self.EventParams.LAYER_ID: experiment.layerId,
344344
self.EventParams.ACTION_TRIGGERED: True,
345345
self.EventParams.DECISION: {
346346
self.EventParams.EXPERIMENT_ID: experiment.id,
347-
self.EventParams.VARIATION_ID: variation_id,
347+
self.EventParams.VARIATION_ID: variation.id,
348348
self.EventParams.IS_LAYER_HOLDBACK: False
349349
}
350350
})

optimizely/optimizely.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -111,21 +111,21 @@ def activate(self, experiment_key, user_id, attributes=None):
111111
self.logger.log(enums.LogLevels.INFO, 'Not activating user "%s".' % user_id)
112112
return None
113113

114-
variation_id = self.bucketer.bucket(experiment, user_id)
114+
variation = self.bucketer.bucket(experiment, user_id)
115115

116-
if not variation_id:
116+
if not variation:
117117
self.logger.log(enums.LogLevels.INFO, 'Not activating user "%s".' % user_id)
118118
return None
119119

120120
# Create and dispatch impression event
121-
impression_event = self.event_builder.create_impression_event(experiment, variation_id, user_id, attributes)
121+
impression_event = self.event_builder.create_impression_event(experiment, variation.id, user_id, attributes)
122122
self.logger.log(enums.LogLevels.INFO, 'Activating user "%s" in experiment "%s".' % (user_id, experiment.key))
123123
self.logger.log(enums.LogLevels.DEBUG,
124124
'Dispatching impression event to URL %s with params %s.' % (impression_event.url,
125125
impression_event.params))
126126
self.event_dispatcher.dispatch_event(impression_event)
127127

128-
return self.config.get_variation_key_from_id(experiment.key, variation_id)
128+
return variation.key
129129

130130
def track(self, event_key, user_id, attributes=None, event_value=None):
131131
""" Send conversion event to Optimizely.
@@ -185,5 +185,10 @@ def get_variation(self, experiment_key, user_id, attributes=None):
185185

186186
if not self._validate_preconditions(experiment, user_id, attributes):
187187
return None
188-
variation_id = self.bucketer.bucket(experiment, user_id)
189-
return self.config.get_variation_key_from_id(experiment_key, variation_id)
188+
variation = self.bucketer.bucket(experiment, user_id)
189+
190+
if variation:
191+
return variation.key
192+
193+
return None
194+

optimizely/project_config.py

Lines changed: 27 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -55,15 +55,14 @@ def __init__(self, datafile, logger, error_handler):
5555
self.experiment_id_map = {}
5656
self.variation_key_map = {}
5757
self.variation_id_map = {}
58-
for experiment_key in self.experiment_key_map.keys():
59-
experiment = self.experiment_key_map.get(experiment_key)
58+
for experiment in self.experiment_key_map.values():
6059
self.experiment_id_map[experiment.id] = experiment
61-
self.variation_key_map[experiment_key] = self._generate_key_map(
62-
self.experiment_key_map.get(experiment_key).variations, 'key'
63-
)
64-
self.variation_id_map[experiment_key] = self._generate_key_map(
65-
self.experiment_key_map.get(experiment_key).variations, 'id'
60+
self.variation_key_map[experiment.key] = self._generate_key_map_entity(
61+
experiment.variations, 'key', entities.Variation
6662
)
63+
self.variation_id_map[experiment.key] = {}
64+
for variation in self.variation_key_map.get(experiment.key).values():
65+
self.variation_id_map[experiment.key][variation.id] = variation
6766

6867
@staticmethod
6968
def _generate_key_map(list, key):
@@ -224,51 +223,51 @@ def get_audience(self, audience_id):
224223
self.logger.log(enums.LogLevels.ERROR, 'Audience ID "%s" is not in datafile.' % audience_id)
225224
self.error_handler.handle_error(exceptions.InvalidAudienceException((enums.Errors.INVALID_AUDIENCE_ERROR)))
226225

227-
def get_variation_key_from_id(self, experiment_key, variation_id):
228-
""" Get variation key given experiment key and variation ID.
226+
def get_variation_from_key(self, experiment_key, variation_key):
227+
""" Get variation given experiment and variation key.
229228
230229
Args:
231-
experiment_key: Key representing parent experiment of variation.
232-
variation_id: ID of the variation.
230+
experiment: Key representing parent experiment of variation.
231+
variation_key: Key representing the variation.
233232
234233
Returns
235-
Variation key.
234+
Object representing the variation.
236235
"""
237236

238-
variation_map = self.variation_id_map.get(experiment_key)
237+
variation_map = self.variation_key_map.get(experiment_key)
239238

240239
if variation_map:
241-
variation_obj = variation_map.get(variation_id)
242-
if variation_obj:
243-
return variation_obj['key']
240+
variation = variation_map.get(variation_key)
241+
if variation:
242+
return variation
244243
else:
245-
self.logger.log(enums.LogLevels.ERROR, 'Variation ID "%s" is not in datafile.' % variation_id)
244+
self.logger.log(enums.LogLevels.ERROR, 'Variation key "%s" is not in datafile.' % variation_key)
246245
self.error_handler.handle_error(exceptions.InvalidVariationException(enums.Errors.INVALID_VARIATION_ERROR))
247246
return None
248247

249248
self.logger.log(enums.LogLevels.ERROR, 'Experiment key "%s" is not in datafile.' % experiment_key)
250249
self.error_handler.handle_error(exceptions.InvalidExperimentException(enums.Errors.INVALID_EXPERIMENT_KEY_ERROR))
251250
return None
252251

253-
def get_variation_id(self, experiment_key, variation_key):
254-
""" Get variation ID given the experiment and variation key.
252+
def get_variation_from_id(self, experiment_key, variation_id):
253+
""" Get variation given experiment and variation ID.
255254
256255
Args:
257-
experiment_key: Parent experiment for the variation.
258-
variation_key: Variation for which the ID is to be determined.
256+
experiment: Key representing parent experiment of variation.
257+
variation_id: ID representing the variation.
259258
260-
Returns:
261-
Variation ID corresponding to the variation.
259+
Returns
260+
Object representing the variation.
262261
"""
263262

264-
variation_map = self.variation_key_map.get(experiment_key)
263+
variation_map = self.variation_id_map.get(experiment_key)
265264

266265
if variation_map:
267-
variation_obj = variation_map.get(variation_key)
268-
if variation_obj:
269-
return variation_obj['id']
266+
variation = variation_map.get(variation_id)
267+
if variation:
268+
return variation
270269
else:
271-
self.logger.log(enums.LogLevels.ERROR, 'Variation key "%s" is not in datafile.' % variation_key)
270+
self.logger.log(enums.LogLevels.ERROR, 'Variation ID "%s" is not in datafile.' % variation_id)
272271
self.error_handler.handle_error(exceptions.InvalidVariationException(enums.Errors.INVALID_VARIATION_ERROR))
273272
return None
274273

tests/test_bucketing.py

Lines changed: 27 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import random
55

66
from optimizely import bucketer
7+
from optimizely import entities
78
from optimizely import logger
89
from optimizely import optimizely
910
from optimizely.helpers import enums
@@ -24,15 +25,18 @@ def test_bucket(self):
2425
# Variation 1
2526
with mock.patch('optimizely.bucketer.Bucketer._generate_bucket_value',
2627
return_value=42) as mock_generate_bucket_value:
27-
self.assertEqual('111128', self.bucketer.bucket(self.project_config.get_experiment_from_key('test_experiment'),
28-
'test_user'))
28+
self.assertEqual(entities.Variation('111128', 'control'),
29+
self.bucketer.bucket(
30+
self.project_config.get_experiment_from_key('test_experiment'), 'test_user'
31+
))
2932
mock_generate_bucket_value.assert_called_once_with('test_user111127')
3033

3134
# Variation 2
3235
with mock.patch('optimizely.bucketer.Bucketer._generate_bucket_value',
3336
return_value=4242) as mock_generate_bucket_value:
34-
self.assertEqual('111129', self.bucketer.bucket(self.project_config.get_experiment_from_key('test_experiment'),
35-
'test_user'))
37+
self.assertEqual(entities.Variation('111129', 'variation'),
38+
self.bucketer.bucket(self.project_config.get_experiment_from_key('test_experiment'),
39+
'test_user'))
3640
mock_generate_bucket_value.assert_called_once_with('test_user111127')
3741

3842
# No matching variation
@@ -52,8 +56,8 @@ def test_bucket__user_in_forced_variation(self):
5256
""" Test that bucket returns variation ID for variation user is forced in. """
5357

5458
with mock.patch('optimizely.bucketer.Bucketer._generate_bucket_value') as mock_generate_bucket_value:
55-
self.assertEqual('111128', self.bucketer.bucket(self.project_config.get_experiment_from_key('test_experiment'),
56-
'user_1'))
59+
self.assertEqual(entities.Variation('111128', 'control'),
60+
self.bucketer.bucket(self.project_config.get_experiment_from_key('test_experiment'), 'user_1'))
5761

5862
# Confirm that bucket value generation did not happen
5963
self.assertEqual(0, mock_generate_bucket_value.call_count)
@@ -62,7 +66,7 @@ def test_bucket__user_in_forced_variation__invalid_variation_id(self):
6266
""" Test that bucket returns None when variation user is forced in is invalid. """
6367

6468
with mock.patch('optimizely.bucketer.Bucketer._generate_bucket_value') as mock_generate_bucket_value, \
65-
mock.patch('optimizely.project_config.ProjectConfig.get_variation_id',
69+
mock.patch('optimizely.project_config.ProjectConfig.get_variation_from_key',
6670
return_value=None) as mock_get_variation_id:
6771
self.assertIsNone(self.bucketer.bucket(self.project_config.get_experiment_from_key('test_experiment'),
6872
'user_1'))
@@ -77,8 +81,8 @@ def test_bucket__experiment_in_group(self):
7781
# In group, matching experiment and variation
7882
with mock.patch('optimizely.bucketer.Bucketer._generate_bucket_value',
7983
side_effect=[42, 4242]) as mock_generate_bucket_value:
80-
self.assertEqual('28902', self.bucketer.bucket(self.project_config.get_experiment_from_key('group_exp_1'),
81-
'test_user'))
84+
self.assertEqual(entities.Variation('28902', 'group_exp_1_variation'),
85+
self.bucketer.bucket(self.project_config.get_experiment_from_key('group_exp_1'), 'test_user'))
8286

8387
self.assertEqual([mock.call('test_user19228'), mock.call('test_user32222')],
8488
mock_generate_bucket_value.call_args_list)
@@ -110,8 +114,8 @@ def test_bucket__experiment_in_group__user_in_forced_variation(self):
110114
""" Test that bucket returns variation ID for variation user is forced in. """
111115

112116
with mock.patch('optimizely.bucketer.Bucketer._generate_bucket_value') as mock_generate_bucket_value:
113-
self.assertEqual('28905', self.bucketer.bucket(self.project_config.get_experiment_from_key('group_exp_2'),
114-
'user_1'))
117+
self.assertEqual(entities.Variation('28905', 'group_exp_2_control'),
118+
self.bucketer.bucket(self.project_config.get_experiment_from_key('group_exp_2'), 'user_1'))
115119

116120
# Confirm that bucket value generation did not happen
117121
self.assertEqual(0, mock_generate_bucket_value.call_count)
@@ -151,8 +155,9 @@ def test_bucket(self):
151155
# Variation 1
152156
with mock.patch('optimizely.bucketer.Bucketer._generate_bucket_value', return_value=42),\
153157
mock.patch('optimizely.logger.SimpleLogger.log') as mock_logging:
154-
self.assertEqual('111128', self.bucketer.bucket(self.project_config.get_experiment_from_key('test_experiment'),
155-
'test_user'))
158+
self.assertEqual(entities.Variation('111128', 'control'),
159+
self.bucketer.bucket(self.project_config.get_experiment_from_key('test_experiment'),
160+
'test_user'))
156161

157162
self.assertEqual(2, mock_logging.call_count)
158163
self.assertEqual(mock.call(enums.LogLevels.DEBUG, 'Assigned bucket 42 to user "test_user".'),
@@ -165,8 +170,9 @@ def test_bucket(self):
165170
# Variation 2
166171
with mock.patch('optimizely.bucketer.Bucketer._generate_bucket_value', return_value=4242),\
167172
mock.patch('optimizely.logger.SimpleLogger.log') as mock_logging:
168-
self.assertEqual('111129', self.bucketer.bucket(self.project_config.get_experiment_from_key('test_experiment'),
169-
'test_user'))
173+
self.assertEqual(entities.Variation('111129', 'variation'),
174+
self.bucketer.bucket(self.project_config.get_experiment_from_key('test_experiment'),
175+
'test_user'))
170176
self.assertEqual(2, mock_logging.call_count)
171177
self.assertEqual(mock.call(enums.LogLevels.DEBUG, 'Assigned bucket 4242 to user "test_user".'),
172178
mock_logging.call_args_list[0])
@@ -191,8 +197,8 @@ def test_bucket__user_in_forced_variation(self):
191197

192198
with mock.patch('optimizely.bucketer.Bucketer._generate_bucket_value'),\
193199
mock.patch('optimizely.logger.SimpleLogger.log') as mock_logging:
194-
self.assertEqual('111128', self.bucketer.bucket(self.project_config.get_experiment_from_key('test_experiment'),
195-
'user_1'))
200+
self.assertEqual(entities.Variation('111128', 'control'),
201+
self.bucketer.bucket(self.project_config.get_experiment_from_key('test_experiment'), 'user_1'))
196202

197203
mock_logging.assert_called_with(enums.LogLevels.INFO, 'User "user_1" is forced in variation "control".')
198204

@@ -203,8 +209,8 @@ def test_bucket__experiment_in_group(self):
203209
with mock.patch('optimizely.bucketer.Bucketer._generate_bucket_value',
204210
side_effect=[42, 4242]),\
205211
mock.patch('optimizely.logger.SimpleLogger.log') as mock_logging:
206-
self.assertEqual('28902', self.bucketer.bucket(self.project_config.get_experiment_from_key('group_exp_1'),
207-
'test_user'))
212+
self.assertEqual(entities.Variation('28902', 'group_exp_1_variation'),
213+
self.bucketer.bucket(self.project_config.get_experiment_from_key('group_exp_1'), 'test_user'))
208214
self.assertEqual(4, mock_logging.call_count)
209215
self.assertEqual(mock.call(enums.LogLevels.DEBUG, 'Assigned bucket 42 to user "test_user".'),
210216
mock_logging.call_args_list[0])
@@ -277,8 +283,8 @@ def test_bucket__experiment_in_group__user_in_forced_variation(self):
277283

278284
with mock.patch('optimizely.bucketer.Bucketer._generate_bucket_value'),\
279285
mock.patch('optimizely.logger.SimpleLogger.log') as mock_logging:
280-
self.assertEqual('28905', self.bucketer.bucket(self.project_config.get_experiment_from_key('group_exp_2'),
281-
'user_1'))
286+
self.assertEqual(entities.Variation('28905', 'group_exp_2_control'),
287+
self.bucketer.bucket(self.project_config.get_experiment_from_key('group_exp_2'), 'user_1'))
282288

283289
# Confirm that bucket value generation did not happen
284290
mock_logging.assert_called_with(enums.LogLevels.INFO, 'User "user_1" is forced in variation "group_exp_2_control".')

0 commit comments

Comments
 (0)