-
Notifications
You must be signed in to change notification settings - Fork 36
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add Audiences to OptimizelyConfig and expose in OptimizelyExperiment #342
Changes from 37 commits
e80fdfb
d58b71a
a583ea5
5b2196c
f964f94
f60ab0a
df1b081
9c9bcea
61d5c24
13703ca
ef95cd8
b8828b9
c3bc816
f1bd533
0ad43d0
942bfe9
528b8f2
c3c3d2c
2fef52a
d3b2a46
dda6e76
4f2d7fb
f90e3e3
4471555
bb98741
48f604b
8179f2b
d694c7f
ec39096
a687a9b
11be430
ecf2e45
f568aad
3b8706f
2504f98
1cd6aab
bc98adc
98ff2b6
d6fa32d
16059d7
0320627
8187f6f
0804bb8
ba3bbef
2567bac
d401c72
6b4a00d
a1ed815
ec4218a
508f82f
26ad430
1f35dda
a4e497f
bf92fd7
2d6def3
bec791a
286f178
c3faba3
199a35f
4fa5d05
4356a4d
a273d11
c8d68f7
a9ac50c
6ce98e3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,13 +12,15 @@ | |
# limitations under the License. | ||
|
||
import copy | ||
from .helpers.condition import ConditionOperatorTypes | ||
|
||
from .project_config import ProjectConfig | ||
|
||
|
||
class OptimizelyConfig(object): | ||
def __init__(self, revision, experiments_map, features_map, datafile=None, | ||
sdk_key=None, environment_key=None, attributes=None, events=None): | ||
sdk_key=None, environment_key=None, attributes=None, events=None, | ||
audiences=None, delivery_rules=None): | ||
self.revision = revision | ||
self.experiments_map = experiments_map | ||
self.features_map = features_map | ||
|
@@ -27,6 +29,8 @@ def __init__(self, revision, experiments_map, features_map, datafile=None, | |
self.environment_key = environment_key | ||
self.attributes = attributes or [] | ||
self.events = events or [] | ||
self.audiences = audiences or [] | ||
self.delivery_rules = delivery_rules or [] | ||
The-inside-man marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
def get_datafile(self): | ||
""" Get the datafile associated with OptimizelyConfig. | ||
|
@@ -68,12 +72,29 @@ def get_events(self): | |
""" | ||
return self.events | ||
|
||
def get_audiences(self): | ||
""" Get the audiences associated with OptimizelyConfig | ||
|
||
returns: | ||
A list of audiences. | ||
""" | ||
return self.audiences | ||
|
||
def get_delivery_rules(self): | ||
The-inside-man marked this conversation as resolved.
Show resolved
Hide resolved
|
||
""" Get the delivery rules list associated with OptimizelyConfig | ||
|
||
Returns: | ||
List of OptimizelyExperiments as DeliveryRules from Rollouts | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Other comments have a period at the end. |
||
""" | ||
return self.delivery_rules | ||
|
||
|
||
class OptimizelyExperiment(object): | ||
def __init__(self, id, key, variations_map): | ||
self.id = id | ||
self.key = key | ||
self.variations_map = variations_map | ||
self.audiences = "" | ||
|
||
|
||
class OptimizelyFeature(object): | ||
|
@@ -113,6 +134,13 @@ def __init__(self, id, key, experiment_ids): | |
self.experiment_ids = experiment_ids | ||
|
||
|
||
class OptimizelyAudience(object): | ||
def __init__(self, id, name, conditions): | ||
self.id = id | ||
self.name = name | ||
self.conditions = conditions | ||
|
||
|
||
class OptimizelyConfigService(object): | ||
""" Class encapsulating methods to be used in creating instance of OptimizelyConfig. """ | ||
|
||
|
@@ -136,9 +164,130 @@ def __init__(self, project_config): | |
self.environment_key = project_config.environment_key | ||
self.attributes = project_config.attributes | ||
self.events = project_config.events | ||
self.rollouts = project_config.rollouts | ||
|
||
self._create_lookup_maps() | ||
|
||
''' | ||
Merging typed_audiences with audiences from project_config. | ||
The typed_audiences has higher presidence. | ||
The-inside-man marked this conversation as resolved.
Show resolved
Hide resolved
|
||
''' | ||
|
||
typed_audiences = project_config.typed_audiences[:] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @The-inside-man Minor, but we don't mutate typed_audiences and thus can do a reference copy. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good point! Thanks for catching that! |
||
optly_typed_audiences = [] | ||
for typed_audience in typed_audiences: | ||
optly_audience = OptimizelyAudience( | ||
typed_audience.get('id'), | ||
The-inside-man marked this conversation as resolved.
Show resolved
Hide resolved
|
||
typed_audience.get('name'), | ||
typed_audience.get('conditions') | ||
) | ||
optly_typed_audiences.append(optly_audience) | ||
|
||
for old_audience in project_config.audiences: | ||
# check if old_audience.id exists in new_audiences.id from typed_audiences | ||
if len([new_audience for new_audience in project_config.typed_audiences | ||
if new_audience.get('id') == old_audience.get('id')]) == 0: | ||
if old_audience.get('id') == "$opt_dummy_audience": | ||
continue | ||
else: | ||
# Convert audiences lists to OptimizelyAudience array | ||
optly_audience = OptimizelyAudience( | ||
old_audience.get('id'), | ||
old_audience.get('name'), | ||
old_audience.get('conditions') | ||
) | ||
optly_typed_audiences.append(optly_audience) | ||
|
||
self.audiences = optly_typed_audiences | ||
|
||
def replace_ids_with_names(self, conditions, audiences_map): | ||
''' | ||
Gets conditions and audiences_map [id:name] | ||
|
||
Returns: | ||
a string of conditions with id's swapped with names | ||
or None if no conditions found. | ||
|
||
''' | ||
if conditions is not None: | ||
return self.stringify_conditions(conditions, audiences_map) | ||
else: | ||
return None | ||
|
||
def lookup_name_from_id(self, audience_id, audiences_map): | ||
''' | ||
Gets and audience ID and audiences map | ||
|
||
Returns: | ||
The name corresponding to the ID | ||
or None if not found. | ||
''' | ||
name = "" | ||
try: | ||
name = audiences_map[audience_id] | ||
except KeyError: | ||
name = audience_id | ||
|
||
return name | ||
|
||
def stringify_conditions(self, conditions, audiences_map): | ||
''' | ||
Gets a list of conditions from an entities.Experiment | ||
and an audiences_map [id:name] | ||
|
||
Returns: | ||
A string of conditions and names for the provided | ||
list of conditions. | ||
''' | ||
ARGS = ConditionOperatorTypes.operators | ||
The-inside-man marked this conversation as resolved.
Show resolved
Hide resolved
|
||
condition = "" | ||
conditions_str = "" | ||
length = len(conditions) | ||
|
||
if length == 0: | ||
return | ||
if length == 1: | ||
# Lookup ID and replace with name | ||
audience_name = self.lookup_name_from_id(conditions[0], audiences_map) | ||
|
||
return '"' + audience_name + '"' | ||
|
||
if length == 2: | ||
if conditions[0] in ARGS: | ||
condition = conditions[0] | ||
audience_name = self.lookup_name_from_id(conditions[1], audiences_map) | ||
return condition.upper() + ' "' + audience_name + '"' | ||
else: | ||
condition = 'OR' | ||
name1 = self.lookup_name_from_id(conditions[0], audiences_map) | ||
name2 = self.lookup_name_from_id(conditions[1], audiences_map) | ||
return ('"' + name1 + '" ' + condition.upper() + ' "' + name2 + '"') | ||
|
||
if length > 2: | ||
for i in range(length): | ||
if conditions[i] in ARGS: | ||
condition = conditions[i].upper() | ||
else: | ||
if condition == "": | ||
condition = 'OR' | ||
if type(conditions[i]) == list: | ||
# If the next item is a list, recursively call function on list | ||
if i + 1 < length: | ||
conditions_str += '(' + self.stringify_conditions(conditions[i], audiences_map) + ') ' | ||
conditions_str += condition + ' ' | ||
else: | ||
conditions_str += '(' + self.stringify_conditions(conditions[i], audiences_map) + ')' | ||
else: | ||
# Handle IDs here - Lookup name based on ID | ||
audience_name = self.lookup_name_from_id(conditions[i], audiences_map) | ||
if audience_name is not None: | ||
if i + 1 < length: | ||
conditions_str += '"' + audience_name + '" ' + condition + ' ' | ||
else: | ||
conditions_str += '"' + audience_name + '"' | ||
|
||
return conditions_str + "" | ||
The-inside-man marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
def get_config(self): | ||
""" Gets instance of OptimizelyConfig | ||
|
||
|
@@ -159,8 +308,11 @@ def get_config(self): | |
self._datafile, | ||
self.sdk_key, | ||
self.environment_key, | ||
self.attributes, | ||
self.events) | ||
self._get_attributes_list(self.attributes), | ||
self._get_events_list(self.events), | ||
self.audiences, | ||
self._get_delivery_rules(self.rollouts) | ||
The-inside-man marked this conversation as resolved.
Show resolved
Hide resolved
|
||
) | ||
|
||
def _create_lookup_maps(self): | ||
""" Creates lookup maps to avoid redundant iteration of config objects. """ | ||
|
@@ -248,7 +400,8 @@ def _get_all_experiments(self): | |
return experiments | ||
|
||
def _get_experiments_maps(self): | ||
""" Gets maps for all the experiments in the project config. | ||
""" Gets maps for all the experiments in the project config and | ||
updates the experiment with updated experiment audiences string. | ||
|
||
Returns: | ||
dict, dict -- experiment key/id to OptimizelyExperiment maps. | ||
|
@@ -257,12 +410,21 @@ def _get_experiments_maps(self): | |
experiments_key_map = {} | ||
# Id map comes in handy to figure out feature experiment. | ||
experiments_id_map = {} | ||
# Audiences map to use for updating experiments with new audience conditions string | ||
audiences_map = {} | ||
|
||
# Build map from OptimizelyAudience array | ||
for optly_audience in self.audiences: | ||
audiences_map[optly_audience.id] = optly_audience.name | ||
|
||
all_experiments = self._get_all_experiments() | ||
for exp in all_experiments: | ||
optly_exp = OptimizelyExperiment( | ||
exp['id'], exp['key'], self._get_variations_map(exp) | ||
) | ||
# Updating each OptimizelyExperiment | ||
audiences = self.replace_ids_with_names(exp.get('audienceConditions', []), audiences_map) | ||
optly_exp.audiences = audiences | ||
|
||
experiments_key_map[exp['key']] = optly_exp | ||
experiments_id_map[exp['id']] = optly_exp | ||
|
@@ -295,3 +457,65 @@ def _get_features_map(self, experiments_id_map): | |
features_map[feature['key']] = optly_feature | ||
|
||
return features_map | ||
|
||
def _get_delivery_rules(self, rollouts): | ||
The-inside-man marked this conversation as resolved.
Show resolved
Hide resolved
|
||
""" Gets an array of rollouts for the project config | ||
|
||
returns: | ||
an array of OptimizelyExperiments as delivery rules. | ||
""" | ||
# Return list for delivery rules | ||
delivery_rules = [] | ||
# Audiences map to use for updating experiments with new audience conditions string | ||
audiences_map = {} | ||
|
||
# Build map from OptimizelyAudience array | ||
for optly_audience in self.audiences: | ||
audiences_map[optly_audience.id] = optly_audience.name | ||
|
||
for rollout in rollouts: | ||
experiments = rollout.get('experiments_map') | ||
if experiments: | ||
for experiment in experiments: | ||
optly_exp = OptimizelyExperiment( | ||
experiment['id'], experiment['key'], self._get_variations_map(experiment) | ||
) | ||
self.update_experiment(optly_exp, experiment.get('audienceConditions', []), audiences_map) | ||
delivery_rules.append(optly_exp) | ||
|
||
return delivery_rules | ||
|
||
def _get_attributes_list(self, attributes): | ||
""" Gets attributes list for the project config | ||
|
||
Returns: | ||
List - OptimizelyAttributes | ||
""" | ||
attributes_list = [] | ||
|
||
for attribute in attributes: | ||
optly_attribute = OptimizelyAttribute( | ||
attribute['id'], | ||
attribute['key'] | ||
) | ||
attributes_list.append(optly_attribute) | ||
|
||
return attributes_list | ||
|
||
def _get_events_list(self, events): | ||
""" Gets events list for the project_config | ||
|
||
Returns: | ||
List - OptimizelyEvents | ||
""" | ||
events_list = [] | ||
|
||
for event in events: | ||
optly_event = OptimizelyEvent( | ||
event['id'], | ||
event['key'], | ||
event['experimentIds'] | ||
) | ||
events_list.append(optly_event) | ||
|
||
return events_list |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Watch for indentation here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The lines above are indented as per Lint requirement for line breaking when too long.