Skip to content

Commit 65c1940

Browse files
Moving Attribute and Event to namedtuples (#20)
1 parent 3f324c9 commit 65c1940

File tree

4 files changed

+85
-146
lines changed

4 files changed

+85
-146
lines changed

optimizely/event_builder.py

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -117,10 +117,10 @@ def _add_attributes(self, attributes):
117117
attribute_value = attributes.get(attribute_key)
118118
# Omit falsy attribute values
119119
if attribute_value:
120-
segment_id = self.config.get_segment_id(attribute_key)
121-
if segment_id:
120+
attribute = self.config.get_attribute(attribute_key)
121+
if attribute:
122122
self.params[self.ATTRIBUTE_PARAM_FORMAT.format(
123-
segment_prefix=self.EventParams.SEGMENT_PREFIX, segment_id=segment_id)] = attribute_value
123+
segment_prefix=self.EventParams.SEGMENT_PREFIX, segment_id=attribute.segmentId)] = attribute_value
124124

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

180-
event_id = self.config.get_event_id(event_key)
181-
event_ids = event_id
180+
event = self.config.get_event(event_key)
181+
event_ids = event.id
182182

183183
if event_value:
184-
event_ids = '{goal_id},{revenue_goal_id}'.format(goal_id=event_id,
185-
revenue_goal_id=self.config.get_revenue_goal_id())
184+
event_ids = '{goal_id},{revenue_goal_id}'.format(goal_id=event.id,
185+
revenue_goal_id=self.config.get_revenue_goal().id)
186186
self.params[self.EventParams.EVENT_VALUE] = event_value
187187

188188
self.params[self.EventParams.GOAL_ID] = event_ids
@@ -275,10 +275,10 @@ def _add_attributes(self, attributes):
275275
attribute_value = attributes.get(attribute_key)
276276
# Omit falsy attribute values
277277
if attribute_value:
278-
attribute_id = self.config.get_attribute_id(attribute_key)
279-
if attribute_id:
278+
attribute = self.config.get_attribute(attribute_key)
279+
if attribute:
280280
self.params[self.EventParams.USER_FEATURES].append({
281-
'id': attribute_id,
281+
'id': attribute.id,
282282
'name': attribute_key,
283283
'type': 'custom',
284284
'value': attribute_value,
@@ -346,7 +346,7 @@ def _add_required_params_for_conversion(self, event_key, user_id, event_value, v
346346
}
347347
})
348348

349-
self.params[self.EventParams.EVENT_ID] = self.config.get_event_id(event_key)
349+
self.params[self.EventParams.EVENT_ID] = self.config.get_event(event_key).id
350350
self.params[self.EventParams.EVENT_NAME] = event_key
351351

352352
def create_impression_event(self, experiment_key, variation_id, user_id, attributes):

optimizely/optimizely.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -137,14 +137,14 @@ def track(self, event_key, user_id, attributes=None, event_value=None):
137137
self.error_handler.handle_error(exceptions.InvalidAttributeException(enums.Errors.INVALID_ATTRIBUTE_FORMAT))
138138
return
139139

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

145145
# Filter out experiments that are not running or that do not include the user in audience conditions
146146
valid_experiments = []
147-
for experiment_id in experiment_ids:
147+
for experiment_id in event.experimentIds:
148148
experiment_key = self.config.get_experiment_key(experiment_id)
149149
if not self._validate_preconditions(experiment_key, user_id, attributes):
150150
self.logger.log(enums.LogLevels.INFO, 'Not tracking user "%s" for experiment "%s".' % (user_id, experiment_key))

optimizely/project_config.py

Lines changed: 42 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import json
2+
from collections import namedtuple
23

34
from .helpers import condition as condition_helper
45
from .helpers import enums
@@ -8,6 +9,11 @@
89
V1_CONFIG_VERSION = '1'
910
V2_CONFIG_VERSION = '2'
1011

12+
Event = namedtuple('Event', ['id', 'key', 'experimentIds'])
13+
AttributeV1 = namedtuple('Attribute', ['id', 'key', 'segmentId'])
14+
AttributeV2 = namedtuple('Attribute', ['id', 'key'])
15+
16+
1117
class ProjectConfig(object):
1218
""" Representation of the Optimizely project config. """
1319

@@ -38,8 +44,9 @@ def __init__(self, datafile, logger, error_handler):
3844
self.group_id_map = self._generate_key_map(self.groups, 'id')
3945
self.experiment_key_map = self._generate_key_map(self.experiments, 'key')
4046
self.experiment_id_map = self._generate_key_map(self.experiments, 'id')
41-
self.event_key_map = self._generate_key_map(self.events, 'key')
42-
self.attribute_key_map = self._generate_key_map(self.attributes, 'key')
47+
self.event_key_map = self._generate_key_map_named_tuple(self.events, 'key', Event)
48+
self.attribute_key_map = self._generate_key_map_named_tuple(self.attributes, 'key', AttributeV1) \
49+
if self.version == V1_CONFIG_VERSION else self._generate_key_map_named_tuple(self.attributes, 'key', AttributeV2)
4350
self.audience_id_map = self._generate_key_map(self.audiences, 'id')
4451
self.audience_id_map = self._deserialize_audience(self.audience_id_map)
4552
for group in self.group_id_map.values():
@@ -80,6 +87,25 @@ def _generate_key_map(list, key):
8087

8188
return key_map
8289

90+
@staticmethod
91+
def _generate_key_map_named_tuple(list, key, named_tuple):
92+
""" Helper method to generate map from key to dict in list of dicts.
93+
94+
Args:
95+
list: List consisting of dict.
96+
key: Key in each dict which will be key in the map.
97+
98+
Returns:
99+
Map mapping key to dict.
100+
"""
101+
102+
key_map = {}
103+
104+
for obj in list:
105+
key_map[obj[key]] = named_tuple(**obj)
106+
107+
return key_map
108+
83109
@staticmethod
84110
def _deserialize_audience(audience_map):
85111
""" Helper method to deserialize and populate audience map with the condition list and structure.
@@ -340,86 +366,48 @@ def get_variation_id(self, experiment_key, variation_key):
340366
self.error_handler.handle_error(exceptions.InvalidExperimentException(enums.Errors.INVALID_EXPERIMENT_KEY_ERROR))
341367
return None
342368

343-
def get_event_id(self, event_key):
344-
""" Get event ID for the provided event key.
369+
def get_event(self, event_key):
370+
""" Get event for the provided event key.
345371
346372
Args:
347-
event_key: Event key for which ID is to be determined.
373+
event_key: Event key for which event is to be determined.
348374
349375
Returns:
350-
Event ID corresponding to the provided event key.
376+
Event corresponding to the provided event key.
351377
"""
352378

353379
event = self.event_key_map.get(event_key)
354380

355381
if event:
356-
return event.get('id')
382+
return event
357383

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

362-
def get_revenue_goal_id(self):
363-
""" Get ID of the revenue goal for the project.
364-
365-
Returns:
366-
Revenue goal ID.
367-
"""
368-
369-
return self.get_event_id(REVENUE_GOAL_KEY)
370-
371-
def get_experiment_ids_for_event(self, event_key):
372-
""" Get experiment IDs for the provided event key.
373-
374-
Args:
375-
event_key: Goal key for which experiment IDs are to be retrieved.
388+
def get_revenue_goal(self):
389+
""" Get the revenue goal for the project.
376390
377391
Returns:
378-
List of all experiment IDs for the event.
392+
Revenue goal.
379393
"""
380394

381-
event = self.event_key_map.get(event_key)
382-
383-
if event:
384-
return event.get('experimentIds', [])
385-
386-
self.logger.log(enums.LogLevels.ERROR, 'Event "%s" is not in datafile.' % event_key)
387-
self.error_handler.handle_error(exceptions.InvalidEventException(enums.Errors.INVALID_EVENT_KEY_ERROR))
388-
return []
389-
390-
def get_attribute_id(self, attribute_key):
391-
""" Get attribute ID for the provided attribute key.
392-
393-
Args:
394-
attribute_key: Attribute key for which attribute ID is to be determined.
395-
396-
Returns:
397-
Attribute ID corresponding to the provided attribute key. None if attribute key is invalid.
398-
"""
399-
400-
attribute = self.attribute_key_map.get(attribute_key)
401-
402-
if attribute:
403-
return attribute.get('id')
404-
405-
self.logger.log(enums.LogLevels.ERROR, 'Attribute "%s" is not in datafile.' % attribute_key)
406-
self.error_handler.handle_error(exceptions.InvalidAttributeException(enums.Errors.INVALID_ATTRIBUTE_ERROR))
407-
return None
395+
return self.get_event(REVENUE_GOAL_KEY)
408396

409-
def get_segment_id(self, attribute_key):
410-
""" Get segment ID for the provided attribute key.
397+
def get_attribute(self, attribute_key):
398+
""" Get attribute for the provided attribute key.
411399
412400
Args:
413-
attribute_key: Attribute key for which segment ID is to be determined.
401+
attribute_key: Attribute key for which attribute is to be fetched.
414402
415403
Returns:
416-
Segment ID corresponding to the provided attribute key. None if attribute key is invalid.
404+
Attribute corresponding to the provided attribute key.
417405
"""
418406

419407
attribute = self.attribute_key_map.get(attribute_key)
420408

421409
if attribute:
422-
return attribute.get('segmentId')
410+
return attribute
423411

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

tests/test_config.py

Lines changed: 29 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from optimizely import exceptions
66
from optimizely import logger
77
from optimizely import optimizely
8+
from optimizely import project_config
89
from optimizely.helpers import enums
910

1011
from . import base
@@ -42,11 +43,11 @@ def test_init(self):
4243
expected_experiment_id_map['32223']['groupId'] = '19228'
4344
expected_experiment_id_map['32223']['groupPolicy'] = 'random'
4445
expected_event_key_map = {
45-
'test_event': self.config_dict['events'][0],
46-
'Total Revenue': self.config_dict['events'][1]
46+
'test_event': project_config.Event(id='111095', key='test_event', experimentIds=['111127']),
47+
'Total Revenue': project_config.Event(id='111096', key='Total Revenue', experimentIds=['111127'])
4748
}
4849
expected_attribute_key_map = {
49-
'test_attribute': self.config_dict['dimensions'][0]
50+
'test_attribute': project_config.AttributeV1(id='111094', key='test_attribute', segmentId='11133')
5051
}
5152
expected_audience_id_map = {
5253
'11154': self.config_dict['audiences'][0]
@@ -271,53 +272,33 @@ def test_get_variation_id__invalid_variation_key(self):
271272

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

274-
def test_get_event_id__valid_key(self):
275-
""" Test that event ID is retrieved correctly for valid event key. """
275+
def test_get_event__valid_key(self):
276+
""" Test that event is retrieved correctly for valid event key. """
276277

277-
self.assertEqual(self.config_dict['events'][0]['id'], self.project_config.get_event_id('test_event'))
278+
self.assertEqual(project_config.Event(id='111095', key='test_event', experimentIds=['111127']),
279+
self.project_config.get_event('test_event'))
278280

279-
def test_get_event_id__invalid_key(self):
280-
""" Test that None is returned when provided event key is invalid. """
281+
def test_get_event__invalid_key(self):
282+
""" Test that None is returned when provided goal key is invalid. """
281283

282-
self.assertIsNone(self.project_config.get_event_id('invalid_key'))
284+
self.assertIsNone(self.project_config.get_event('invalid_key'))
283285

284-
def test_get_revenue_goal_id(self):
285-
""" Test that revenue goal ID can be retrieved as expected. """
286+
def test_get_revenue_goal(self):
287+
""" Test that revenue goal can be retrieved as expected. """
286288

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

289-
def test_get_experiment_ids_for_event__valid_key(self):
290-
""" Test that experiment IDs are retrieved as expected for valid event key. """
292+
def test_get_attribute__valid_key(self):
293+
""" Test that attribute is retrieved correctly for valid attribute key. """
291294

292-
self.assertEqual(self.config_dict['events'][0]['experimentIds'],
293-
self.project_config.get_experiment_ids_for_event('test_event'))
295+
self.assertEqual(project_config.AttributeV1(id='111094', key='test_attribute', segmentId='11133'),
296+
self.project_config.get_attribute('test_attribute'))
294297

295-
def test_get_experiment_ids_for_event__invalid_key(self):
296-
""" Test that empty list is returned when provided event key is invalid. """
297-
298-
self.assertEqual([], self.project_config.get_experiment_ids_for_event('invalid_key'))
299-
300-
def test_get_attribute_id__valid_key(self):
301-
""" Test that attribute ID is retrieved correctly for valid attribute key. """
302-
303-
self.assertEqual(self.config_dict['dimensions'][0]['id'],
304-
self.project_config.get_attribute_id('test_attribute'))
305-
306-
def test_get_attribute_id__invalid_key(self):
307-
""" Test that None is returned when provided attribute key is invalid. """
308-
309-
self.assertIsNone(self.project_config.get_attribute_id('invalid_key'))
310-
311-
def test_get_segment_id__valid_key(self):
312-
""" Test that segment ID is retrieved correctly for valid attribute key. """
313-
314-
self.assertEqual(self.config_dict['dimensions'][0]['segmentId'],
315-
self.project_config.get_segment_id('test_attribute'))
316-
317-
def test_get_segment_id__invalid_key(self):
298+
def test_get_attribute__invalid_key(self):
318299
""" Test that None is returned when provided attribute key is invalid. """
319300

320-
self.assertIsNone(self.project_config.get_segment_id('invalid_key'))
301+
self.assertIsNone(self.project_config.get_attribute('invalid_key'))
321302

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

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

441-
def test_get_event_id__invalid_key(self):
442-
""" Test that message is logged when provided event key is invalid. """
443-
444-
with mock.patch('optimizely.logger.SimpleLogger.log') as mock_logging:
445-
self.project_config.get_event_id('invalid_key')
446-
447-
mock_logging.assert_called_once_with(enums.LogLevels.ERROR, 'Event "invalid_key" is not in datafile.')
448-
449-
def test_get_experiment_ids_for_event__invalid_key(self):
422+
def test_get_event__invalid_key(self):
450423
""" Test that message is logged when provided event key is invalid. """
451424

452425
with mock.patch('optimizely.logger.SimpleLogger.log') as mock_logging:
453-
self.project_config.get_experiment_ids_for_event('invalid_key')
426+
self.project_config.get_event('invalid_key')
454427

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

457-
def test_get_attribute_id__invalid_key(self):
430+
def test_get_attribute__invalid_key(self):
458431
""" Test that message is logged when provided attribute key is invalid. """
459432

460433
with mock.patch('optimizely.logger.SimpleLogger.log') as mock_logging:
461-
self.project_config.get_attribute_id('invalid_key')
462-
463-
mock_logging.assert_called_once_with(enums.LogLevels.ERROR, 'Attribute "invalid_key" is not in datafile.')
464-
465-
def test_get_segment_id__invalid_key(self):
466-
""" Test that message is logged when provided attribute key is invalid. """
467-
468-
with mock.patch('optimizely.logger.SimpleLogger.log') as mock_logging:
469-
self.project_config.get_segment_id('invalid_key')
434+
self.project_config.get_attribute('invalid_key')
470435

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

@@ -549,30 +514,16 @@ def test_get_variation_id__invalid_variation_key(self):
549514
enums.Errors.INVALID_VARIATION_ERROR,
550515
self.project_config.get_variation_id, 'test_experiment', 'invalid_key')
551516

552-
def test_get_event_id__invalid_key(self):
517+
def test_get_event__invalid_key(self):
553518
""" Test that exception is raised when provided event key is invalid. """
554519

555520
self.assertRaisesRegexp(exceptions.InvalidEventException,
556521
enums.Errors.INVALID_EVENT_KEY_ERROR,
557-
self.project_config.get_event_id, 'invalid_key')
558-
559-
def test_get_experiment_ids_for_event__invalid_key(self):
560-
""" Test that exception is raised when provided event key is invalid. """
561-
562-
self.assertRaisesRegexp(exceptions.InvalidEventException,
563-
enums.Errors.INVALID_EVENT_KEY_ERROR,
564-
self.project_config.get_experiment_ids_for_event, 'invalid_key')
565-
566-
def test_get_attribute_id__invalid_key(self):
567-
""" Test that exception is raised when provided attribute key is invalid. """
568-
569-
self.assertRaisesRegexp(exceptions.InvalidAttributeException,
570-
enums.Errors.INVALID_ATTRIBUTE_ERROR,
571-
self.project_config.get_attribute_id, 'invalid_key')
522+
self.project_config.get_event, 'invalid_key')
572523

573-
def test_get_segment_id__invalid_key(self):
524+
def test_get_attribute__invalid_key(self):
574525
""" Test that exception is raised when provided attribute key is invalid. """
575526

576527
self.assertRaisesRegexp(exceptions.InvalidAttributeException,
577528
enums.Errors.INVALID_ATTRIBUTE_ERROR,
578-
self.project_config.get_segment_id, 'invalid_key')
529+
self.project_config.get_attribute, 'invalid_key')

0 commit comments

Comments
 (0)