Skip to content

Commit 51fef17

Browse files
Refactoring and passing ProjectConfig from Optimizely itself. (#176)
1 parent 5d3cee2 commit 51fef17

File tree

9 files changed

+343
-329
lines changed

9 files changed

+343
-329
lines changed

optimizely/bucketer.py

Lines changed: 17 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright 2016-2017, Optimizely
1+
# Copyright 2016-2017, 2019 Optimizely
22
# Licensed under the Apache License, Version 2.0 (the "License");
33
# you may not use this file except in compliance with the License.
44
# You may obtain a copy of the License at
@@ -29,15 +29,10 @@
2929
class Bucketer(object):
3030
""" Optimizely bucketing algorithm that evenly distributes visitors. """
3131

32-
def __init__(self, project_config):
33-
""" Bucketer init method to set bucketing seed and project config data.
34-
35-
Args:
36-
project_config: Project config data to be used in making bucketing decisions.
37-
"""
32+
def __init__(self):
33+
""" Bucketer init method to set bucketing seed and logger instance. """
3834

3935
self.bucket_seed = HASH_SEED
40-
self.config = project_config
4136

4237
def _generate_unsigned_hash_code_32_bit(self, bucketing_id):
4338
""" Helper method to retrieve hash code.
@@ -65,10 +60,11 @@ def _generate_bucket_value(self, bucketing_id):
6560
ratio = float(self._generate_unsigned_hash_code_32_bit(bucketing_id)) / MAX_HASH_VALUE
6661
return math.floor(ratio * MAX_TRAFFIC_VALUE)
6762

68-
def find_bucket(self, bucketing_id, parent_id, traffic_allocations):
63+
def find_bucket(self, project_config, bucketing_id, parent_id, traffic_allocations):
6964
""" Determine entity based on bucket value and traffic allocations.
7065
7166
Args:
67+
project_config: Instance of ProjectConfig.
7268
bucketing_id: ID to be used for bucketing the user.
7369
parent_id: ID representing group or experiment.
7470
traffic_allocations: Traffic allocations representing traffic allotted to experiments or variations.
@@ -79,7 +75,7 @@ def find_bucket(self, bucketing_id, parent_id, traffic_allocations):
7975

8076
bucketing_key = BUCKETING_ID_TEMPLATE.format(bucketing_id=bucketing_id, parent_id=parent_id)
8177
bucketing_number = self._generate_bucket_value(bucketing_key)
82-
self.config.logger.debug('Assigned bucket %s to user with bucketing ID "%s".' % (
78+
project_config.logger.debug('Assigned bucket %s to user with bucketing ID "%s".' % (
8379
bucketing_number,
8480
bucketing_id
8581
))
@@ -91,10 +87,11 @@ def find_bucket(self, bucketing_id, parent_id, traffic_allocations):
9187

9288
return None
9389

94-
def bucket(self, experiment, user_id, bucketing_id):
90+
def bucket(self, project_config, experiment, user_id, bucketing_id):
9591
""" For a given experiment and bucketing ID determines variation to be shown to user.
9692
9793
Args:
94+
project_config: Instance of ProjectConfig.
9895
experiment: Object representing the experiment for which user is to be bucketed.
9996
user_id: ID for user.
10097
bucketing_id: ID to be used for bucketing the user.
@@ -108,40 +105,40 @@ def bucket(self, experiment, user_id, bucketing_id):
108105

109106
# Determine if experiment is in a mutually exclusive group
110107
if experiment.groupPolicy in GROUP_POLICIES:
111-
group = self.config.get_group(experiment.groupId)
108+
group = project_config.get_group(experiment.groupId)
112109

113110
if not group:
114111
return None
115112

116-
user_experiment_id = self.find_bucket(bucketing_id, experiment.groupId, group.trafficAllocation)
113+
user_experiment_id = self.find_bucket(project_config, bucketing_id, experiment.groupId, group.trafficAllocation)
117114
if not user_experiment_id:
118-
self.config.logger.info('User "%s" is in no experiment.' % user_id)
115+
project_config.logger.info('User "%s" is in no experiment.' % user_id)
119116
return None
120117

121118
if user_experiment_id != experiment.id:
122-
self.config.logger.info('User "%s" is not in experiment "%s" of group %s.' % (
119+
project_config.logger.info('User "%s" is not in experiment "%s" of group %s.' % (
123120
user_id,
124121
experiment.key,
125122
experiment.groupId
126123
))
127124
return None
128125

129-
self.config.logger.info('User "%s" is in experiment %s of group %s.' % (
126+
project_config.logger.info('User "%s" is in experiment %s of group %s.' % (
130127
user_id,
131128
experiment.key,
132129
experiment.groupId
133130
))
134131

135132
# Bucket user if not in white-list and in group (if any)
136-
variation_id = self.find_bucket(bucketing_id, experiment.id, experiment.trafficAllocation)
133+
variation_id = self.find_bucket(project_config, bucketing_id, experiment.id, experiment.trafficAllocation)
137134
if variation_id:
138-
variation = self.config.get_variation_from_id(experiment.key, variation_id)
139-
self.config.logger.info('User "%s" is in variation "%s" of experiment %s.' % (
135+
variation = project_config.get_variation_from_id(experiment.key, variation_id)
136+
project_config.logger.info('User "%s" is in variation "%s" of experiment %s.' % (
140137
user_id,
141138
variation.key,
142139
experiment.key
143140
))
144141
return variation
145142

146-
self.config.logger.info('User "%s" is in no variation.' % user_id)
143+
project_config.logger.info('User "%s" is in no variation.' % user_id)
147144
return None

optimizely/decision_service.py

Lines changed: 41 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,10 @@
2727
class DecisionService(object):
2828
""" Class encapsulating all decision related capabilities. """
2929

30-
def __init__(self, config, user_profile_service):
31-
self.bucketer = bucketer.Bucketer(config)
30+
def __init__(self, logger, user_profile_service):
31+
self.bucketer = bucketer.Bucketer()
32+
self.logger = logger
3233
self.user_profile_service = user_profile_service
33-
self.config = config
34-
self.logger = config.logger
3534

3635
def _get_bucketing_id(self, user_id, attributes):
3736
""" Helper method to determine bucketing ID for the user.
@@ -55,10 +54,11 @@ def _get_bucketing_id(self, user_id, attributes):
5554

5655
return user_id
5756

58-
def get_forced_variation(self, experiment, user_id):
57+
def get_forced_variation(self, project_config, experiment, user_id):
5958
""" Determine if a user is forced into a variation for the given experiment and return that variation.
6059
6160
Args:
61+
project_config: Instance of ProjectConfig.
6262
experiment: Object representing the experiment for which user is to be bucketed.
6363
user_id: ID for the user.
6464
@@ -69,17 +69,18 @@ def get_forced_variation(self, experiment, user_id):
6969
forced_variations = experiment.forcedVariations
7070
if forced_variations and user_id in forced_variations:
7171
variation_key = forced_variations.get(user_id)
72-
variation = self.config.get_variation_from_key(experiment.key, variation_key)
72+
variation = project_config.get_variation_from_key(experiment.key, variation_key)
7373
if variation:
7474
self.logger.info('User "%s" is forced in variation "%s".' % (user_id, variation_key))
7575
return variation
7676

7777
return None
7878

79-
def get_stored_variation(self, experiment, user_profile):
79+
def get_stored_variation(self, project_config, experiment, user_profile):
8080
""" Determine if the user has a stored variation available for the given experiment and return that.
8181
8282
Args:
83+
project_config: Instance of ProjectConfig.
8384
experiment: Object representing the experiment for which user is to be bucketed.
8485
user_profile: UserProfile object representing the user's profile.
8586
@@ -91,7 +92,7 @@ def get_stored_variation(self, experiment, user_profile):
9192
variation_id = user_profile.get_variation_for_experiment(experiment.id)
9293

9394
if variation_id:
94-
variation = self.config.get_variation_from_id(experiment.key, variation_id)
95+
variation = project_config.get_variation_from_id(experiment.key, variation_id)
9596
if variation:
9697
self.logger.info('Found a stored decision. User "%s" is in variation "%s" of experiment "%s".' % (
9798
user_id,
@@ -102,7 +103,7 @@ def get_stored_variation(self, experiment, user_profile):
102103

103104
return None
104105

105-
def get_variation(self, experiment, user_id, attributes, ignore_user_profile=False):
106+
def get_variation(self, project_config, experiment, user_id, attributes, ignore_user_profile=False):
106107
""" Top-level function to help determine variation user should be put in.
107108
108109
First, check if experiment is running.
@@ -112,6 +113,7 @@ def get_variation(self, experiment, user_id, attributes, ignore_user_profile=Fal
112113
Fifth, bucket the user and return the variation.
113114
114115
Args:
116+
project_config: Instance of ProjectConfig.
115117
experiment: Experiment for which user variation needs to be determined.
116118
user_id: ID for user.
117119
attributes: Dict representing user attributes.
@@ -127,12 +129,12 @@ def get_variation(self, experiment, user_id, attributes, ignore_user_profile=Fal
127129
return None
128130

129131
# Check if the user is forced into a variation
130-
variation = self.config.get_forced_variation(experiment.key, user_id)
132+
variation = project_config.get_forced_variation(experiment.key, user_id)
131133
if variation:
132134
return variation
133135

134136
# Check to see if user is white-listed for a certain variation
135-
variation = self.get_forced_variation(experiment, user_id)
137+
variation = self.get_forced_variation(project_config, experiment, user_id)
136138
if variation:
137139
return variation
138140

@@ -147,14 +149,14 @@ def get_variation(self, experiment, user_id, attributes, ignore_user_profile=Fal
147149

148150
if validator.is_user_profile_valid(retrieved_profile):
149151
user_profile = UserProfile(**retrieved_profile)
150-
variation = self.get_stored_variation(experiment, user_profile)
152+
variation = self.get_stored_variation(project_config, experiment, user_profile)
151153
if variation:
152154
return variation
153155
else:
154156
self.logger.warning('User profile has invalid format.')
155157

156158
# Bucket user and store the new decision
157-
if not audience_helper.is_user_in_experiment(self.config, experiment, attributes, self.logger):
159+
if not audience_helper.is_user_in_experiment(project_config, experiment, attributes, self.logger):
158160
self.logger.info('User "%s" does not meet conditions to be in experiment "%s".' % (
159161
user_id,
160162
experiment.key
@@ -163,7 +165,7 @@ def get_variation(self, experiment, user_id, attributes, ignore_user_profile=Fal
163165

164166
# Determine bucketing ID to be used
165167
bucketing_id = self._get_bucketing_id(user_id, attributes)
166-
variation = self.bucketer.bucket(experiment, user_id, bucketing_id)
168+
variation = self.bucketer.bucket(project_config, experiment, user_id, bucketing_id)
167169

168170
if variation:
169171
# Store this new decision and return the variation for the user
@@ -177,11 +179,12 @@ def get_variation(self, experiment, user_id, attributes, ignore_user_profile=Fal
177179

178180
return None
179181

180-
def get_variation_for_rollout(self, rollout, user_id, attributes=None):
182+
def get_variation_for_rollout(self, project_config, rollout, user_id, attributes=None):
181183
""" Determine which experiment/variation the user is in for a given rollout.
182184
Returns the variation of the first experiment the user qualifies for.
183185
184186
Args:
187+
project_config: Instance of ProjectConfig.
185188
rollout: Rollout for which we are getting the variation.
186189
user_id: ID for user.
187190
attributes: Dict representing user attributes.
@@ -193,10 +196,10 @@ def get_variation_for_rollout(self, rollout, user_id, attributes=None):
193196
# Go through each experiment in order and try to get the variation for the user
194197
if rollout and len(rollout.experiments) > 0:
195198
for idx in range(len(rollout.experiments) - 1):
196-
experiment = self.config.get_experiment_from_key(rollout.experiments[idx].get('key'))
199+
experiment = project_config.get_experiment_from_key(rollout.experiments[idx].get('key'))
197200

198201
# Check if user meets audience conditions for targeting rule
199-
if not audience_helper.is_user_in_experiment(self.config, experiment, attributes, self.logger):
202+
if not audience_helper.is_user_in_experiment(project_config, experiment, attributes, self.logger):
200203
self.logger.debug('User "%s" does not meet conditions for targeting rule %s.' % (
201204
user_id,
202205
idx + 1
@@ -206,7 +209,7 @@ def get_variation_for_rollout(self, rollout, user_id, attributes=None):
206209
self.logger.debug('User "%s" meets conditions for targeting rule %s.' % (user_id, idx + 1))
207210
# Determine bucketing ID to be used
208211
bucketing_id = self._get_bucketing_id(user_id, attributes)
209-
variation = self.bucketer.bucket(experiment, user_id, bucketing_id)
212+
variation = self.bucketer.bucket(project_config, experiment, user_id, bucketing_id)
210213
if variation:
211214
self.logger.debug('User "%s" is in variation %s of experiment %s.' % (
212215
user_id,
@@ -221,34 +224,36 @@ def get_variation_for_rollout(self, rollout, user_id, attributes=None):
221224
break
222225

223226
# Evaluate last rule i.e. "Everyone Else" rule
224-
everyone_else_experiment = self.config.get_experiment_from_key(rollout.experiments[-1].get('key'))
225-
if audience_helper.is_user_in_experiment(self.config,
226-
self.config.get_experiment_from_key(rollout.experiments[-1].get('key')),
227-
attributes,
228-
self.logger):
227+
everyone_else_experiment = project_config.get_experiment_from_key(rollout.experiments[-1].get('key'))
228+
if audience_helper.is_user_in_experiment(
229+
project_config,
230+
project_config.get_experiment_from_key(rollout.experiments[-1].get('key')),
231+
attributes,
232+
self.logger):
229233
# Determine bucketing ID to be used
230234
bucketing_id = self._get_bucketing_id(user_id, attributes)
231-
variation = self.bucketer.bucket(everyone_else_experiment, user_id, bucketing_id)
235+
variation = self.bucketer.bucket(project_config, everyone_else_experiment, user_id, bucketing_id)
232236
if variation:
233237
self.logger.debug('User "%s" meets conditions for targeting rule "Everyone Else".' % user_id)
234238
return Decision(everyone_else_experiment, variation, enums.DecisionSources.ROLLOUT)
235239

236240
return Decision(None, None, enums.DecisionSources.ROLLOUT)
237241

238-
def get_experiment_in_group(self, group, bucketing_id):
242+
def get_experiment_in_group(self, project_config, group, bucketing_id):
239243
""" Determine which experiment in the group the user is bucketed into.
240244
241245
Args:
246+
project_config: Instance of ProjectConfig.
242247
group: The group to bucket the user into.
243248
bucketing_id: ID to be used for bucketing the user.
244249
245250
Returns:
246251
Experiment if the user is bucketed into an experiment in the specified group. None otherwise.
247252
"""
248253

249-
experiment_id = self.bucketer.find_bucket(bucketing_id, group.id, group.trafficAllocation)
254+
experiment_id = self.bucketer.find_bucket(project_config, bucketing_id, group.id, group.trafficAllocation)
250255
if experiment_id:
251-
experiment = self.config.get_experiment_from_id(experiment_id)
256+
experiment = project_config.get_experiment_from_id(experiment_id)
252257
if experiment:
253258
self.logger.info('User with bucketing ID "%s" is in experiment %s of group %s.' % (
254259
bucketing_id,
@@ -264,10 +269,11 @@ def get_experiment_in_group(self, group, bucketing_id):
264269

265270
return None
266271

267-
def get_variation_for_feature(self, feature, user_id, attributes=None):
272+
def get_variation_for_feature(self, project_config, feature, user_id, attributes=None):
268273
""" Returns the experiment/variation the user is bucketed in for the given feature.
269274
270275
Args:
276+
project_config: Instance of ProjectConfig.
271277
feature: Feature for which we are determining if it is enabled or not for the given user.
272278
user_id: ID for user.
273279
attributes: Dict representing user attributes.
@@ -276,17 +282,15 @@ def get_variation_for_feature(self, feature, user_id, attributes=None):
276282
Decision namedtuple consisting of experiment and variation for the user.
277283
"""
278284

279-
experiment = None
280-
variation = None
281285
bucketing_id = self._get_bucketing_id(user_id, attributes)
282286

283287
# First check if the feature is in a mutex group
284288
if feature.groupId:
285-
group = self.config.get_group(feature.groupId)
289+
group = project_config.get_group(feature.groupId)
286290
if group:
287-
experiment = self.get_experiment_in_group(group, bucketing_id)
291+
experiment = self.get_experiment_in_group(project_config, group, bucketing_id)
288292
if experiment and experiment.id in feature.experimentIds:
289-
variation = self.get_variation(experiment, user_id, attributes)
293+
variation = self.get_variation(project_config, experiment, user_id, attributes)
290294

291295
if variation:
292296
self.logger.debug('User "%s" is in variation %s of experiment %s.' % (
@@ -301,9 +305,9 @@ def get_variation_for_feature(self, feature, user_id, attributes=None):
301305
# Next check if the feature is being experimented on
302306
elif feature.experimentIds:
303307
# If an experiment is not in a group, then the feature can only be associated with one experiment
304-
experiment = self.config.get_experiment_from_id(feature.experimentIds[0])
308+
experiment = project_config.get_experiment_from_id(feature.experimentIds[0])
305309
if experiment:
306-
variation = self.get_variation(experiment, user_id, attributes)
310+
variation = self.get_variation(project_config, experiment, user_id, attributes)
307311

308312
if variation:
309313
self.logger.debug('User "%s" is in variation %s of experiment %s.' % (
@@ -315,7 +319,7 @@ def get_variation_for_feature(self, feature, user_id, attributes=None):
315319

316320
# Next check if user is part of a rollout
317321
if feature.rolloutId:
318-
rollout = self.config.get_rollout_from_id(feature.rolloutId)
319-
return self.get_variation_for_rollout(rollout, user_id, attributes)
322+
rollout = project_config.get_rollout_from_id(feature.rolloutId)
323+
return self.get_variation_for_rollout(project_config, rollout, user_id, attributes)
320324
else:
321325
return Decision(None, None, enums.DecisionSources.ROLLOUT)

0 commit comments

Comments
 (0)