Skip to content

Commit

Permalink
Moving audiences to object
Browse files Browse the repository at this point in the history
  • Loading branch information
aliabbasrizvi committed Sep 22, 2016
1 parent d37cb9f commit 44906e7
Show file tree
Hide file tree
Showing 7 changed files with 68 additions and 26 deletions.
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

0 comments on commit 44906e7

Please sign in to comment.