Skip to content
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

Merged
merged 65 commits into from
Jul 20, 2021
Merged
Show file tree
Hide file tree
Changes from 37 commits
Commits
Show all changes
65 commits
Select commit Hold shift + click to select a range
e80fdfb
added sdk and environment key
ozayr-zaviar Jun 10, 2021
d58b71a
[MAINTENANCE] Remove Deprecated warnings during build
The-inside-man Jun 3, 2021
a583ea5
[OASIS-7757] Fix spelling of environment to fix testcases from failing
The-inside-man Jun 15, 2021
5b2196c
[OASIS-7757] - Added additional test cases to test_optimizely and tes…
The-inside-man Jun 17, 2021
f964f94
[OASIS-7800] Updated optimizely_config with attributes and events
The-inside-man Jun 22, 2021
f60ab0a
[OASIS-7757] - update copyright years and add more testcases for sdk_…
The-inside-man Jun 23, 2021
df1b081
[OASIS-7800] Updated optimizely_config with attributes and events
The-inside-man Jun 23, 2021
9c9bcea
Run autopep8 to fix formatting issues after rebase
The-inside-man Jun 24, 2021
61d5c24
[OASIS-7800] - Added test cases for attribute map and events map
The-inside-man Jun 24, 2021
13703ca
Remove unused import from optimizely config
The-inside-man Jun 24, 2021
ef95cd8
Corrected comment wording in get functions for events.
The-inside-man Jun 25, 2021
b8828b9
[OASIS-7812] - Add Audiences to OpotimizelyConfig to expose to users
The-inside-man Jul 2, 2021
c3bc816
Remove unused import for Experiment in OptimizelyConfig
The-inside-man Jul 2, 2021
f1bd533
Update Formatting issues for lint
The-inside-man Jul 2, 2021
0ad43d0
Update Import for conditions in optimizely_config.py
The-inside-man Jul 2, 2021
942bfe9
Additional changes for audiences and added functions to create attrib…
The-inside-man Jul 6, 2021
528b8f2
Add delivery_rules to OptimizelyConfig and testcases to support
The-inside-man Jul 6, 2021
c3c3d2c
Formatting issues
The-inside-man Jul 6, 2021
2fef52a
Fix line length in comments and statement
The-inside-man Jul 6, 2021
d3b2a46
Shortened return statement
The-inside-man Jul 6, 2021
dda6e76
Updated list copy to use slice for backwards compatibility
The-inside-man Jul 6, 2021
4f2d7fb
Add in test datafile for typed audiences
The-inside-man Jul 7, 2021
f90e3e3
remove unused import
The-inside-man Jul 7, 2021
4471555
Merge branch 'master' into jbrown/oasis-7812
The-inside-man Jul 7, 2021
bb98741
Modify test_update_experiment to pass audiences as OptimizelyExperiment
The-inside-man Jul 7, 2021
48f604b
Fix linting issues
The-inside-man Jul 7, 2021
8179f2b
Style changes as requested to comments and variable naming.
The-inside-man Jul 7, 2021
d694c7f
Lint Update
The-inside-man Jul 7, 2021
ec39096
Fix line length
The-inside-man Jul 7, 2021
a687a9b
Move upper() to central location to only call once
The-inside-man Jul 7, 2021
11be430
Indent fix
The-inside-man Jul 7, 2021
ecf2e45
Broke statement into two for Lint issues
The-inside-man Jul 7, 2021
f568aad
Added test case for variation
The-inside-man Jul 9, 2021
3b8706f
Add changes to decision service to us experiment ID instead of KEY.
The-inside-man Jul 9, 2021
2504f98
Missing newline added
The-inside-man Jul 9, 2021
1cd6aab
Remove redundant update function. Ad
The-inside-man Jul 9, 2021
bc98adc
Rename testcase to remove replace_ids_with_names.
The-inside-man Jul 9, 2021
98ff2b6
Duplicate experiment Key issue with multi feature flag (#347)
The-inside-man Jul 9, 2021
d6fa32d
Update delivery rules test.
The-inside-man Jul 9, 2021
16059d7
Explenation added to flake8 for ignoring W504.
The-inside-man Jul 12, 2021
0320627
Changes made to move delivery rules and experiment rules into Optimiz…
The-inside-man Jul 12, 2021
8187f6f
Formatter on new changes
The-inside-man Jul 12, 2021
0804bb8
Merge branch 'master' into jbrown/oasis-7812
The-inside-man Jul 12, 2021
ba3bbef
Remove TODO and other dev comments
The-inside-man Jul 12, 2021
2567bac
Remove TODO and other dev comments
The-inside-man Jul 12, 2021
d401c72
Remove unused imoprt
The-inside-man Jul 12, 2021
6b4a00d
Added nl after imports to follow Flake8
The-inside-man Jul 12, 2021
a1ed815
Add except for if incorrect type used in lookup_name_from_id() to pre…
The-inside-man Jul 12, 2021
ec4218a
Correct except to assign audience_id instead of blank string
The-inside-man Jul 12, 2021
508f82f
Casting name to string in case non string type for testing
The-inside-man Jul 12, 2021
26ad430
Added test and modification to account for scenario when NOT is follo…
The-inside-man Jul 12, 2021
1f35dda
Update from isinstance to type and change List to list for python 3.4
The-inside-man Jul 12, 2021
a4e497f
Simplified stringifyConditions algorithm to be more readable and less…
The-inside-man Jul 13, 2021
bf92fd7
Updated changes - reuse test logic, modify stringify to proper handle…
The-inside-man Jul 13, 2021
2d6def3
Change to single test to run all cases for stringify.
The-inside-man Jul 14, 2021
bec791a
Move config_service out of loop.
The-inside-man Jul 14, 2021
286f178
Added missed step in delivery rules.
The-inside-man Jul 14, 2021
c3faba3
Update to properly refect datafile examples.
The-inside-man Jul 14, 2021
199a35f
Remove unused import
The-inside-man Jul 14, 2021
4fa5d05
Cleaned up get functions till overall cleanup of code.
The-inside-man Jul 14, 2021
4356a4d
Merge branch 'master' into jbrown/oasis-7812
The-inside-man Jul 15, 2021
a273d11
Merge branch 'master' into jbrown/oasis-7812
The-inside-man Jul 15, 2021
c8d68f7
Added comments to stringify_conditions, added testcase to test string…
The-inside-man Jul 19, 2021
a9ac50c
Correct spelling of precedence
The-inside-man Jul 19, 2021
6ce98e3
Update comment to be more readable.
The-inside-man Jul 20, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions optimizely/decision_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -342,7 +342,7 @@ def get_variation_for_rollout(self, project_config, rollout, user_id, attributes
if rollout and len(rollout.experiments) > 0:
for idx in range(len(rollout.experiments) - 1):
logging_key = str(idx + 1)
rollout_rule = project_config.get_experiment_from_key(rollout.experiments[idx].get('key'))
rollout_rule = project_config.get_experiment_from_id(rollout.experiments[idx].get('id'))

# Check if user meets audience conditions for targeting rule
audience_conditions = rollout_rule.get_audience_conditions_or_ids()
Expand Down Expand Up @@ -387,7 +387,7 @@ def get_variation_for_rollout(self, project_config, rollout, user_id, attributes
break

# Evaluate last rule i.e. "Everyone Else" rule
everyone_else_rule = project_config.get_experiment_from_key(rollout.experiments[-1].get('key'))
everyone_else_rule = project_config.get_experiment_from_id(rollout.experiments[-1].get('id'))
audience_conditions = everyone_else_rule.get_audience_conditions_or_ids()
audience_eval, audience_reasons = audience_helper.does_user_meet_audience_conditions(
project_config,
Expand Down
1 change: 1 addition & 0 deletions optimizely/helpers/condition.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ class ConditionOperatorTypes(object):
AND = 'and'
OR = 'or'
NOT = 'not'
operators = [AND, OR, NOT]


class ConditionMatchTypes(object):
Expand Down
232 changes: 228 additions & 4 deletions optimizely/optimizely_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Watch for indentation here

Copy link
Contributor Author

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.

self.revision = revision
self.experiments_map = experiments_map
self.features_map = features_map
Expand All @@ -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 []

def get_datafile(self):
""" Get the datafile associated with OptimizelyConfig.
Expand Down Expand Up @@ -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):
""" Get the delivery rules list associated with OptimizelyConfig

Returns:
List of OptimizelyExperiments as DeliveryRules from Rollouts
Copy link

Choose a reason for hiding this comment

The 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):
Expand Down Expand Up @@ -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. """

Expand All @@ -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.
'''

typed_audiences = project_config.typed_audiences[:]
Copy link

Choose a reason for hiding this comment

The 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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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'),
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
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 + ""

def get_config(self):
""" Gets instance of OptimizelyConfig

Expand All @@ -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)
)

def _create_lookup_maps(self):
""" Creates lookup maps to avoid redundant iteration of config objects. """
Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand Down Expand Up @@ -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):
""" 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
Loading