Skip to content

Moving Attribute and Event to namedtuples #20

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 4 commits into from
Sep 20, 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
22 changes: 11 additions & 11 deletions optimizely/event_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,10 +117,10 @@ def _add_attributes(self, attributes):
attribute_value = attributes.get(attribute_key)
# Omit falsy attribute values
if attribute_value:
segment_id = self.config.get_segment_id(attribute_key)
if segment_id:
attribute = self.config.get_attribute(attribute_key)
if attribute:
self.params[self.ATTRIBUTE_PARAM_FORMAT.format(
segment_prefix=self.EventParams.SEGMENT_PREFIX, segment_id=segment_id)] = attribute_value
segment_prefix=self.EventParams.SEGMENT_PREFIX, segment_id=attribute.segmentId)] = attribute_value

def _add_source(self):
""" Add source information to the event. """
Expand Down Expand Up @@ -177,12 +177,12 @@ def _add_conversion_goal(self, event_key, event_value):
event_value: Value associated with the event. Can be used to represent revenue in cents.
"""

event_id = self.config.get_event_id(event_key)
event_ids = event_id
event = self.config.get_event(event_key)
event_ids = event.id

if event_value:
event_ids = '{goal_id},{revenue_goal_id}'.format(goal_id=event_id,
revenue_goal_id=self.config.get_revenue_goal_id())
event_ids = '{goal_id},{revenue_goal_id}'.format(goal_id=event.id,
revenue_goal_id=self.config.get_revenue_goal().id)
self.params[self.EventParams.EVENT_VALUE] = event_value

self.params[self.EventParams.GOAL_ID] = event_ids
Expand Down Expand Up @@ -275,10 +275,10 @@ def _add_attributes(self, attributes):
attribute_value = attributes.get(attribute_key)
# Omit falsy attribute values
if attribute_value:
attribute_id = self.config.get_attribute_id(attribute_key)
if attribute_id:
attribute = self.config.get_attribute(attribute_key)
if attribute:
self.params[self.EventParams.USER_FEATURES].append({
'id': attribute_id,
'id': attribute.id,
'name': attribute_key,
'type': 'custom',
'value': attribute_value,
Expand Down Expand Up @@ -346,7 +346,7 @@ def _add_required_params_for_conversion(self, event_key, user_id, event_value, v
}
})

self.params[self.EventParams.EVENT_ID] = self.config.get_event_id(event_key)
self.params[self.EventParams.EVENT_ID] = self.config.get_event(event_key).id
self.params[self.EventParams.EVENT_NAME] = event_key

def create_impression_event(self, experiment_key, variation_id, user_id, attributes):
Expand Down
6 changes: 3 additions & 3 deletions optimizely/optimizely.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,14 +137,14 @@ def track(self, event_key, user_id, attributes=None, event_value=None):
self.error_handler.handle_error(exceptions.InvalidAttributeException(enums.Errors.INVALID_ATTRIBUTE_FORMAT))
return

experiment_ids = self.config.get_experiment_ids_for_event(event_key)
if not experiment_ids:
event = self.config.get_event(event_key)
if not event.experimentIds:
self.logger.log(enums.LogLevels.INFO, 'Not tracking user "%s" for event "%s".' % (user_id, event_key))
return

# Filter out experiments that are not running or that do not include the user in audience conditions
valid_experiments = []
for experiment_id in experiment_ids:
for experiment_id in event.experimentIds:
experiment_key = self.config.get_experiment_key(experiment_id)
if not self._validate_preconditions(experiment_key, user_id, attributes):
self.logger.log(enums.LogLevels.INFO, 'Not tracking user "%s" for experiment "%s".' % (user_id, experiment_key))
Expand Down
96 changes: 42 additions & 54 deletions optimizely/project_config.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import json
from collections import namedtuple

from .helpers import condition as condition_helper
from .helpers import enums
Expand All @@ -8,6 +9,11 @@
V1_CONFIG_VERSION = '1'
V2_CONFIG_VERSION = '2'

Event = namedtuple('Event', ['id', 'key', 'experimentIds'])
AttributeV1 = namedtuple('Attribute', ['id', 'key', 'segmentId'])
AttributeV2 = namedtuple('Attribute', ['id', 'key'])


class ProjectConfig(object):
""" Representation of the Optimizely project config. """

Expand Down Expand Up @@ -38,8 +44,9 @@ def __init__(self, datafile, logger, error_handler):
self.group_id_map = self._generate_key_map(self.groups, 'id')
self.experiment_key_map = self._generate_key_map(self.experiments, 'key')
self.experiment_id_map = self._generate_key_map(self.experiments, 'id')
self.event_key_map = self._generate_key_map(self.events, 'key')
self.attribute_key_map = self._generate_key_map(self.attributes, 'key')
self.event_key_map = self._generate_key_map_named_tuple(self.events, 'key', Event)
self.attribute_key_map = self._generate_key_map_named_tuple(self.attributes, 'key', AttributeV1) \
if self.version == V1_CONFIG_VERSION else self._generate_key_map_named_tuple(self.attributes, 'key', AttributeV2)
self.audience_id_map = self._generate_key_map(self.audiences, 'id')
self.audience_id_map = self._deserialize_audience(self.audience_id_map)
for group in self.group_id_map.values():
Expand Down Expand Up @@ -80,6 +87,25 @@ def _generate_key_map(list, key):

return key_map

@staticmethod
def _generate_key_map_named_tuple(list, key, named_tuple):
""" Helper method to generate map from key to dict in list of dicts.

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

Returns:
Map mapping key to dict.
"""

key_map = {}

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

return key_map

@staticmethod
def _deserialize_audience(audience_map):
""" Helper method to deserialize and populate audience map with the condition list and structure.
Expand Down Expand Up @@ -340,86 +366,48 @@ def get_variation_id(self, experiment_key, variation_key):
self.error_handler.handle_error(exceptions.InvalidExperimentException(enums.Errors.INVALID_EXPERIMENT_KEY_ERROR))
return None

def get_event_id(self, event_key):
""" Get event ID for the provided event key.
def get_event(self, event_key):
""" Get event for the provided event key.

Args:
event_key: Event key for which ID is to be determined.
event_key: Event key for which event is to be determined.

Returns:
Event ID corresponding to the provided event key.
Event corresponding to the provided event key.
"""

event = self.event_key_map.get(event_key)

if event:
return event.get('id')
return event

self.logger.log(enums.LogLevels.ERROR, 'Event "%s" is not in datafile.' % event_key)
self.error_handler.handle_error(exceptions.InvalidEventException(enums.Errors.INVALID_EVENT_KEY_ERROR))
return None

def get_revenue_goal_id(self):
""" Get ID of the revenue goal for the project.

Returns:
Revenue goal ID.
"""

return self.get_event_id(REVENUE_GOAL_KEY)

def get_experiment_ids_for_event(self, event_key):
""" Get experiment IDs for the provided event key.

Args:
event_key: Goal key for which experiment IDs are to be retrieved.
def get_revenue_goal(self):
""" Get the revenue goal for the project.

Returns:
List of all experiment IDs for the event.
Revenue goal.
"""

event = self.event_key_map.get(event_key)

if event:
return event.get('experimentIds', [])

self.logger.log(enums.LogLevels.ERROR, 'Event "%s" is not in datafile.' % event_key)
self.error_handler.handle_error(exceptions.InvalidEventException(enums.Errors.INVALID_EVENT_KEY_ERROR))
return []

def get_attribute_id(self, attribute_key):
""" Get attribute ID for the provided attribute key.

Args:
attribute_key: Attribute key for which attribute ID is to be determined.

Returns:
Attribute ID corresponding to the provided attribute key. None if attribute key is invalid.
"""

attribute = self.attribute_key_map.get(attribute_key)

if attribute:
return attribute.get('id')

self.logger.log(enums.LogLevels.ERROR, 'Attribute "%s" is not in datafile.' % attribute_key)
self.error_handler.handle_error(exceptions.InvalidAttributeException(enums.Errors.INVALID_ATTRIBUTE_ERROR))
return None
return self.get_event(REVENUE_GOAL_KEY)

def get_segment_id(self, attribute_key):
""" Get segment ID for the provided attribute key.
def get_attribute(self, attribute_key):
""" Get attribute for the provided attribute key.

Args:
attribute_key: Attribute key for which segment ID is to be determined.
attribute_key: Attribute key for which attribute is to be fetched.

Returns:
Segment ID corresponding to the provided attribute key. None if attribute key is invalid.
Attribute corresponding to the provided attribute key.
"""

attribute = self.attribute_key_map.get(attribute_key)

if attribute:
return attribute.get('segmentId')
return attribute

self.logger.log(enums.LogLevels.ERROR, 'Attribute "%s" is not in datafile.' % attribute_key)
self.error_handler.handle_error(exceptions.InvalidAttributeException(enums.Errors.INVALID_ATTRIBUTE_ERROR))
Expand Down
107 changes: 29 additions & 78 deletions tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from optimizely import exceptions
from optimizely import logger
from optimizely import optimizely
from optimizely import project_config
from optimizely.helpers import enums

from . import base
Expand Down Expand Up @@ -42,11 +43,11 @@ def test_init(self):
expected_experiment_id_map['32223']['groupId'] = '19228'
expected_experiment_id_map['32223']['groupPolicy'] = 'random'
expected_event_key_map = {
'test_event': self.config_dict['events'][0],
'Total Revenue': self.config_dict['events'][1]
'test_event': project_config.Event(id='111095', key='test_event', experimentIds=['111127']),
'Total Revenue': project_config.Event(id='111096', key='Total Revenue', experimentIds=['111127'])
}
expected_attribute_key_map = {
'test_attribute': self.config_dict['dimensions'][0]
'test_attribute': project_config.AttributeV1(id='111094', key='test_attribute', segmentId='11133')
}
expected_audience_id_map = {
'11154': self.config_dict['audiences'][0]
Expand Down Expand Up @@ -271,53 +272,33 @@ def test_get_variation_id__invalid_variation_key(self):

self.assertIsNone(self.project_config.get_variation_id(self.config_dict['experiments'][0]['key'], 'invalid_key'))

def test_get_event_id__valid_key(self):
""" Test that event ID is retrieved correctly for valid event key. """
def test_get_event__valid_key(self):
""" Test that event is retrieved correctly for valid event key. """

self.assertEqual(self.config_dict['events'][0]['id'], self.project_config.get_event_id('test_event'))
self.assertEqual(project_config.Event(id='111095', key='test_event', experimentIds=['111127']),
self.project_config.get_event('test_event'))

def test_get_event_id__invalid_key(self):
""" Test that None is returned when provided event key is invalid. """
def test_get_event__invalid_key(self):
""" Test that None is returned when provided goal key is invalid. """

self.assertIsNone(self.project_config.get_event_id('invalid_key'))
self.assertIsNone(self.project_config.get_event('invalid_key'))

def test_get_revenue_goal_id(self):
""" Test that revenue goal ID can be retrieved as expected. """
def test_get_revenue_goal(self):
""" Test that revenue goal can be retrieved as expected. """

self.assertEqual(self.config_dict['events'][1]['id'], self.project_config.get_revenue_goal_id())
self.assertEqual(project_config.Event(id='111096', key='Total Revenue', experimentIds=['111127']),
self.project_config.get_revenue_goal())

def test_get_experiment_ids_for_event__valid_key(self):
""" Test that experiment IDs are retrieved as expected for valid event key. """
def test_get_attribute__valid_key(self):
""" Test that attribute is retrieved correctly for valid attribute key. """

self.assertEqual(self.config_dict['events'][0]['experimentIds'],
self.project_config.get_experiment_ids_for_event('test_event'))
self.assertEqual(project_config.AttributeV1(id='111094', key='test_attribute', segmentId='11133'),
self.project_config.get_attribute('test_attribute'))

def test_get_experiment_ids_for_event__invalid_key(self):
""" Test that empty list is returned when provided event key is invalid. """

self.assertEqual([], self.project_config.get_experiment_ids_for_event('invalid_key'))

def test_get_attribute_id__valid_key(self):
""" Test that attribute ID is retrieved correctly for valid attribute key. """

self.assertEqual(self.config_dict['dimensions'][0]['id'],
self.project_config.get_attribute_id('test_attribute'))

def test_get_attribute_id__invalid_key(self):
""" Test that None is returned when provided attribute key is invalid. """

self.assertIsNone(self.project_config.get_attribute_id('invalid_key'))

def test_get_segment_id__valid_key(self):
""" Test that segment ID is retrieved correctly for valid attribute key. """

self.assertEqual(self.config_dict['dimensions'][0]['segmentId'],
self.project_config.get_segment_id('test_attribute'))

def test_get_segment_id__invalid_key(self):
def test_get_attribute__invalid_key(self):
""" Test that None is returned when provided attribute key is invalid. """

self.assertIsNone(self.project_config.get_segment_id('invalid_key'))
self.assertIsNone(self.project_config.get_attribute('invalid_key'))

def test_get_traffic_allocation__valid_key(self):
""" Test that trafficAllocation is retrieved correctly for valid experiment key or group ID. """
Expand Down Expand Up @@ -438,35 +419,19 @@ def test_get_variation_id__invalid_variation_key(self):

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

def test_get_event_id__invalid_key(self):
""" Test that message is logged when provided event key is invalid. """

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

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

def test_get_experiment_ids_for_event__invalid_key(self):
def test_get_event__invalid_key(self):
""" Test that message is logged when provided event key is invalid. """

with mock.patch('optimizely.logger.SimpleLogger.log') as mock_logging:
self.project_config.get_experiment_ids_for_event('invalid_key')
self.project_config.get_event('invalid_key')

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

def test_get_attribute_id__invalid_key(self):
def test_get_attribute__invalid_key(self):
""" Test that message is logged when provided attribute key is invalid. """

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

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

def test_get_segment_id__invalid_key(self):
""" Test that message is logged when provided attribute key is invalid. """

with mock.patch('optimizely.logger.SimpleLogger.log') as mock_logging:
self.project_config.get_segment_id('invalid_key')
self.project_config.get_attribute('invalid_key')

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

Expand Down Expand Up @@ -549,30 +514,16 @@ def test_get_variation_id__invalid_variation_key(self):
enums.Errors.INVALID_VARIATION_ERROR,
self.project_config.get_variation_id, 'test_experiment', 'invalid_key')

def test_get_event_id__invalid_key(self):
def test_get_event__invalid_key(self):
""" Test that exception is raised when provided event key is invalid. """

self.assertRaisesRegexp(exceptions.InvalidEventException,
enums.Errors.INVALID_EVENT_KEY_ERROR,
self.project_config.get_event_id, 'invalid_key')

def test_get_experiment_ids_for_event__invalid_key(self):
""" Test that exception is raised when provided event key is invalid. """

self.assertRaisesRegexp(exceptions.InvalidEventException,
enums.Errors.INVALID_EVENT_KEY_ERROR,
self.project_config.get_experiment_ids_for_event, 'invalid_key')

def test_get_attribute_id__invalid_key(self):
""" Test that exception is raised when provided attribute key is invalid. """

self.assertRaisesRegexp(exceptions.InvalidAttributeException,
enums.Errors.INVALID_ATTRIBUTE_ERROR,
self.project_config.get_attribute_id, 'invalid_key')
self.project_config.get_event, 'invalid_key')

def test_get_segment_id__invalid_key(self):
def test_get_attribute__invalid_key(self):
""" Test that exception is raised when provided attribute key is invalid. """

self.assertRaisesRegexp(exceptions.InvalidAttributeException,
enums.Errors.INVALID_ATTRIBUTE_ERROR,
self.project_config.get_segment_id, 'invalid_key')
self.project_config.get_attribute, 'invalid_key')