Skip to content

Moving audiences to use object #22

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

Merged
merged 1 commit into from
Sep 23, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 10 additions & 0 deletions optimizely/entities.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,16 @@ def __init__(self, id, key, segmentId=None):
self.segmentId = segmentId


class Audience(BaseEntity):

def __init__(self, id, name, conditions, conditionStructure=None, conditionList=None):
self.id = id
self.name = name
self.conditions = conditions
self.conditionStructure = conditionStructure
self.conditionList = conditionList


class Event(BaseEntity):

def __init__(self, id, key, experimentIds):
Expand Down
5 changes: 5 additions & 0 deletions optimizely/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ class InvalidAttributeException(Exception):
pass


class InvalidAudienceException(Exception):
""" Raised when provided audience is invalid. """
pass


class InvalidExperimentException(Exception):
""" Raised when provided experiment key is invalid. """
pass
Expand Down
6 changes: 3 additions & 3 deletions optimizely/helpers/audience.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ def is_match(audience, attributes):
Return:
Boolean representing if user satisfies audience conditions or not.
"""
condition_evaluator = condition_helper.ConditionEvaluator(audience.get('conditionList'), attributes)
return condition_evaluator.evaluate(audience.get('conditionStructure'))
condition_evaluator = condition_helper.ConditionEvaluator(audience.conditionList, attributes)
return condition_evaluator.evaluate(audience.conditionStructure)


def is_user_in_experiment(config, experiment, attributes):
Expand All @@ -37,7 +37,7 @@ def is_user_in_experiment(config, experiment, attributes):

# Return True if conditions for any one audience are met
for audience_id in experiment.audienceIds:
audience = config.get_audience_object_from_id(audience_id)
audience = config.get_audience(audience_id)

if is_match(audience, attributes):
return True
Expand Down
1 change: 1 addition & 0 deletions optimizely/helpers/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ class Errors(object):
INVALID_INPUT_ERROR = 'Provided "{}" is in an invalid format.'
INVALID_ATTRIBUTE_ERROR = 'Provided attribute is not in datafile.'
INVALID_ATTRIBUTE_FORMAT = 'Attributes provided are in an invalid format.'
INVALID_AUDIENCE_ERROR = 'Provided audience is not in datafile.'
INVALID_EXPERIMENT_KEY_ERROR = 'Provided experiment is not in datafile.'
INVALID_EVENT_KEY_ERROR = 'Provided event is not in datafile.'
INVALID_GROUP_ID_ERROR = 'Provided group is not in datafile.'
Expand Down
30 changes: 20 additions & 10 deletions optimizely/project_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ def __init__(self, datafile, logger, error_handler):
self.experiment_key_map = self._generate_key_map_entity(self.experiments, 'key', entities.Experiment)
self.event_key_map = self._generate_key_map_entity(self.events, 'key', entities.Event)
self.attribute_key_map = self._generate_key_map_entity(self.attributes, 'key', entities.Attribute)
self.audience_id_map = self._generate_key_map(self.audiences, 'id')
self.audience_id_map = self._generate_key_map_entity(self.audiences, 'id', entities.Audience)
self.audience_id_map = self._deserialize_audience(self.audience_id_map)
for group in self.group_id_map.values():
experiments_in_group_key_map = self._generate_key_map_entity(group['experiments'], 'key', entities.Experiment)
Expand Down Expand Up @@ -84,12 +84,13 @@ def _generate_key_map(list, key):
return key_map

@staticmethod
def _generate_key_map_entity(list, key, named_tuple):
def _generate_key_map_entity(list, key, entity_class):
""" Helper method to generate map from key to entity object for given list of dicts.

Args:
list: List consisting of dict.
key: Key in each dict which will be key in the map.
entity_class: Class representing the entity.

Returns:
Map mapping key to entity object.
Expand All @@ -98,24 +99,27 @@ def _generate_key_map_entity(list, key, named_tuple):
key_map = {}

for obj in list:
key_map[obj[key]] = named_tuple(**obj)
key_map[obj[key]] = entity_class(**obj)

return key_map

@staticmethod
def _deserialize_audience(audience_map):
""" Helper method to deserialize and populate audience map with the condition list and structure.
""" Helper method to de-serialize and populate audience map with the condition list and structure.

Args:
audience_map: Dict mapping audience ID to audience object.

Returns:
Dict additionally consisting of condition list and structure for every audience.
Dict additionally consisting of condition list and structure on every audience object.
"""

for audience_id in audience_map.keys():
audience_map[audience_id]['conditionStructure'], audience_map[audience_id]['conditionList'] = \
condition_helper.loads(audience_map[audience_id]['conditions'])
for audience in audience_map.values():
condition_structure, condition_list = condition_helper.loads(audience.conditions)
audience.__dict__.update({
'conditionStructure': condition_structure,
'conditionList': condition_list
})

return audience_map

Expand Down Expand Up @@ -184,7 +188,7 @@ def get_experiment_from_id(self, experiment_id):
self.error_handler.handle_error(exceptions.InvalidExperimentException(enums.Errors.INVALID_EXPERIMENT_KEY_ERROR))
return None

def get_audience_object_from_id(self, audience_id):
def get_audience(self, audience_id):
""" Get audience object for the provided audience ID.

Args:
Expand All @@ -194,7 +198,13 @@ def get_audience_object_from_id(self, audience_id):
Dict representing the audience.
"""

return self.audience_id_map.get(audience_id)
audience = self.audience_id_map.get(audience_id)

if audience:
return audience

self.logger.log(enums.LogLevels.ERROR, 'Audience ID "%s" is not in datafile.' % audience_id)
self.error_handler.handle_error(exceptions.InvalidAudienceException((enums.Errors.INVALID_AUDIENCE_ERROR)))

def get_variation_key_from_id(self, experiment_key, variation_id):
""" Get variation key given experiment key and variation ID.
Expand Down
8 changes: 4 additions & 4 deletions tests/helpers_tests/test_audience.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ def test_is_match__audience_condition_matches(self):
'location': 'San Francisco'
}

self.assertTrue(audience.is_match(self.optimizely.config.audiences[0], user_attributes))
self.assertTrue(audience.is_match(self.optimizely.config.get_audience('11154'), user_attributes))

def test_is_match__audience_condition_does_not_match(self):
""" Test that is_match returns False when audience conditions are not met. """
Expand All @@ -26,7 +26,7 @@ def test_is_match__audience_condition_does_not_match(self):
'location': 'San Francisco'
}

self.assertFalse(audience.is_match(self.optimizely.config.audiences[0], user_attributes))
self.assertFalse(audience.is_match(self.optimizely.config.get_audience('11154'), user_attributes))

def test_is_user_in_experiment__no_audience(self):
""" Test that is_user_in_experiment returns True when experiment is using no audience. """
Expand Down Expand Up @@ -54,7 +54,7 @@ def test_is_user_in_experiment__audience_conditions_are_met(self):
self.assertTrue(audience.is_user_in_experiment(self.project_config,
self.project_config.get_experiment_from_key('test_experiment'),
user_attributes))
mock_is_match.assert_called_once_with(self.optimizely.config.audiences[0], user_attributes)
mock_is_match.assert_called_once_with(self.optimizely.config.get_audience('11154'), user_attributes)

def test_is_user_in_experiment__audience_conditions_not_met(self):
""" Test that is_user_in_experiment returns False when audience conditions are not met. """
Expand All @@ -69,4 +69,4 @@ def test_is_user_in_experiment__audience_conditions_not_met(self):
self.assertFalse(audience.is_user_in_experiment(self.project_config,
self.project_config.get_experiment_from_key('test_experiment'),
user_attributes))
mock_is_match.assert_called_once_with(self.optimizely.config.audiences[0], user_attributes)
mock_is_match.assert_called_once_with(self.optimizely.config.get_audience('11154'), user_attributes)
34 changes: 25 additions & 9 deletions tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,12 +94,13 @@ def test_init(self):
'test_attribute': entities.Attribute('111094', 'test_attribute', segmentId='11133')
}
expected_audience_id_map = {
'11154': self.config_dict['audiences'][0]
'11154': entities.Audience(
'11154', 'Test attribute users',
'["and", ["or", ["or", {"name": "test_attribute", "type": "custom_dimension", "value": "test_value"}]]]',
conditionStructure=['and', ['or', ['or', 0]]],
conditionList=[['test_attribute', 'test_value']]
)
}
expected_audience_id_map['11154'].update({
'conditionList': [['test_attribute', 'test_value']],
'conditionStructure': ['and', ['or', ['or', 0]]]
})
expected_variation_key_map = {
'test_experiment': {
'control': {
Expand Down Expand Up @@ -243,16 +244,16 @@ def test_get_experiment_from_id__invalid_id(self):

self.assertIsNone(self.project_config.get_experiment_from_id('invalid_id'))

def test_get_audience_object_from_id__valid_id(self):
def test_get_audience__valid_id(self):
""" Test that audience object is retrieved correctly given a valid audience ID. """

self.assertEqual(self.project_config.audience_id_map['11154'],
self.project_config.get_audience_object_from_id('11154'))
self.project_config.get_audience('11154'))

def test_get_audience_object_from_id__invalid_id(self):
def test_get_audience__invalid_id(self):
""" Test that None is returned for an invalid audience ID. """

self.assertIsNone(self.project_config.get_audience_object_from_id('42'))
self.assertIsNone(self.project_config.get_audience('42'))

def test_get_variation_key_from_id__valid_experiment_key(self):
""" Test that variation key is retrieved correctly when valid experiment key and variation ID are provided. """
Expand Down Expand Up @@ -397,6 +398,14 @@ def test_get_experiment_from_key__invalid_key(self):

mock_logging.assert_called_once_with(enums.LogLevels.ERROR, 'Experiment key "invalid_key" is not in datafile.')

def test_get_audience__invalid_id(self):
""" Test that message is logged when provided audience ID is invalid. """

with mock.patch('optimizely.logger.SimpleLogger.log') as mock_logging:
self.project_config.get_audience('42')

mock_logging.assert_called_once_with(enums.LogLevels.ERROR, 'Audience ID "42" is not in datafile.')

def test_get_variation_key_from_id__invalid_variation_id(self):
""" Test that message is logged when provided variation ID is invalid. """

Expand Down Expand Up @@ -453,6 +462,13 @@ def test_get_experiment_from_key__invalid_key(self):
enums.Errors.INVALID_EXPERIMENT_KEY_ERROR,
self.project_config.get_experiment_from_key, 'invalid_key')

def test_get_audience__invalid_id(self):
""" Test that message is logged when provided audience ID is invalid. """

self.assertRaisesRegexp(exceptions.InvalidAudienceException,
enums.Errors.INVALID_AUDIENCE_ERROR,
self.project_config.get_audience, '42')

def test_get_variation_key_from_id__invalid_variation_id(self):
""" Test that exception is raised when provided variation ID is invalid. """

Expand Down