diff --git a/optimizely/event/__init__.py b/optimizely/event/__init__.py new file mode 100644 index 00000000..d6094e5a --- /dev/null +++ b/optimizely/event/__init__.py @@ -0,0 +1,12 @@ +# Copyright 2019, Optimizely +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/optimizely/event/payload.py b/optimizely/event/payload.py new file mode 100644 index 00000000..fa145d31 --- /dev/null +++ b/optimizely/event/payload.py @@ -0,0 +1,94 @@ +# Copyright 2019 Optimizely +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json + + +class EventBatch(object): + """ Class respresenting Event Batch. """ + + def __init__(self, account_id, project_id, revision, client_name, client_version, + anonymize_ip, enrich_decisions, visitors=None): + self.account_id = account_id + self.project_id = project_id + self.revision = revision + self.client_name = client_name + self.client_version = client_version + self.anonymize_ip = anonymize_ip + self.enrich_decisions = enrich_decisions + self.visitors = visitors + + def __eq__(self, other): + batch_obj = json.loads(json.dumps(self.__dict__, default=lambda o: o.__dict__), + object_pairs_hook=self._dict_clean) + return batch_obj == other + + def _dict_clean(self, obj): + """ Helper method to remove keys from dictionary with None values. """ + + result = {} + for k, v in obj: + if v is None and k in ['revenue', 'value', 'tags', 'decisions']: + continue + else: + result[k] = v + return result + + +class Decision(object): + """ Class respresenting Decision. """ + + def __init__(self, campaign_id, experiment_id, variation_id): + self.campaign_id = campaign_id + self.experiment_id = experiment_id + self.variation_id = variation_id + + +class Snapshot(object): + """ Class representing Snapshot. """ + + def __init__(self, events, decisions=None): + self.events = events + self.decisions = decisions + + +class SnapshotEvent(object): + """ Class representing Snapshot Event. """ + + def __init__(self, entity_id, uuid, key, timestamp, revenue=None, value=None, tags=None): + self.entity_id = entity_id + self.uuid = uuid + self.key = key + self.timestamp = timestamp + self.revenue = revenue + self.value = value + self.tags = tags + + +class Visitor(object): + """ Class representing Visitor. """ + + def __init__(self, snapshots, attributes, visitor_id): + self.snapshots = snapshots + self.attributes = attributes + self.visitor_id = visitor_id + + +class VisitorAttribute(object): + """ Class representing Visitor Attribute. """ + + def __init__(self, entity_id, key, attribute_type, value): + self.entity_id = entity_id + self.key = key + self.type = attribute_type + self.value = value diff --git a/optimizely/event/user_event.py b/optimizely/event/user_event.py new file mode 100644 index 00000000..b18c4ef5 --- /dev/null +++ b/optimizely/event/user_event.py @@ -0,0 +1,72 @@ +# Copyright 2019 Optimizely +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import time +import uuid + +from optimizely import version + +CLIENT_NAME = 'python-sdk' + + +class UserEvent(object): + """ Class respresenting User Event. """ + + def __init__(self, event_context): + self.event_context = event_context + self.uuid = self._get_uuid() + self.timestamp = self._get_time() + + def _get_time(self): + return int(round(time.time() * 1000)) + + def _get_uuid(self): + return str(uuid.uuid4()) + + +class ImpressionEvent(UserEvent): + """ Class representing Impression Event. """ + + def __init__(self, event_context, user_id, experiment, visitor_attributes, variation, bot_filtering=None): + super(ImpressionEvent, self).__init__(event_context) + self.event_context = event_context + self.user_id = user_id + self.experiment = experiment + self.visitor_attributes = visitor_attributes + self.variation = variation + self.bot_filtering = bot_filtering + + +class ConversionEvent(UserEvent): + """ Class representing Conversion Event. """ + + def __init__(self, event_context, event, user_id, visitor_attributes, event_tags, bot_filtering=None): + super(ConversionEvent, self).__init__(event_context) + self.event_context = event_context + self.event = event + self.user_id = user_id + self.visitor_attributes = visitor_attributes + self.event_tags = event_tags + self.bot_filtering = bot_filtering + + +class EventContext(object): + """ Class respresenting User Event Context. """ + + def __init__(self, account_id, project_id, revision, anonymize_ip): + self.account_id = account_id + self.project_id = project_id + self.revision = revision + self.client_name = CLIENT_NAME + self.client_version = version.__version__ + self.anonymize_ip = anonymize_ip diff --git a/tests/test_event_payload.py b/tests/test_event_payload.py new file mode 100644 index 00000000..b4a0c866 --- /dev/null +++ b/tests/test_event_payload.py @@ -0,0 +1,120 @@ +# Copyright 2019, Optimizely +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from optimizely import version +from optimizely.event.payload import Decision, EventBatch, Snapshot, SnapshotEvent, Visitor, VisitorAttribute +from . import base + + +class EventPayloadTest(base.BaseTest): + + def test_impression_event_equals_serialized_payload(self): + expected_params = { + 'account_id': '12001', + 'project_id': '111001', + 'visitors': [{ + 'visitor_id': 'test_user', + 'attributes': [{ + 'type': 'custom', + 'value': 'test_value', + 'entity_id': '111094', + 'key': 'test_attribute' + }], + 'snapshots': [{ + 'decisions': [{ + 'variation_id': '111129', + 'experiment_id': '111127', + 'campaign_id': '111182' + }], + 'events': [{ + 'timestamp': 42123, + 'entity_id': '111182', + 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', + 'key': 'campaign_activated' + }] + }] + }], + 'client_name': 'python-sdk', + 'client_version': version.__version__, + 'enrich_decisions': True, + 'anonymize_ip': False, + 'revision': '42' + } + + batch = EventBatch('12001', '111001', '42', 'python-sdk', version.__version__, + False, True) + visitor_attr = VisitorAttribute('111094', 'test_attribute', 'custom', 'test_value') + event = SnapshotEvent('111182', 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', 'campaign_activated', + 42123) + event_decision = Decision('111182', '111127', '111129') + + snapshots = Snapshot([event], [event_decision]) + user = Visitor([snapshots], [visitor_attr], 'test_user') + + batch.visitors = [user] + + self.assertEqual(batch, expected_params) + + def test_conversion_event_equals_serialized_payload(self): + expected_params = { + 'account_id': '12001', + 'project_id': '111001', + 'visitors': [{ + 'visitor_id': 'test_user', + 'attributes': [{ + 'type': 'custom', + 'value': 'test_value', + 'entity_id': '111094', + 'key': 'test_attribute' + }, { + 'type': 'custom', + 'value': 'test_value2', + 'entity_id': '111095', + 'key': 'test_attribute2' + }], + 'snapshots': [{ + 'events': [{ + 'timestamp': 42123, + 'entity_id': '111182', + 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', + 'key': 'campaign_activated', + 'revenue': 4200, + 'tags': { + 'non-revenue': 'abc', + 'revenue': 4200, + 'value': 1.234 + }, + 'value': 1.234 + }] + }] + }], + 'client_name': 'python-sdk', + 'client_version': version.__version__, + 'enrich_decisions': True, + 'anonymize_ip': False, + 'revision': '42' + } + + batch = EventBatch('12001', '111001', '42', 'python-sdk', version.__version__, + False, True) + visitor_attr_1 = VisitorAttribute('111094', 'test_attribute', 'custom', 'test_value') + visitor_attr_2 = VisitorAttribute('111095', 'test_attribute2', 'custom', 'test_value2') + event = SnapshotEvent('111182', 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', 'campaign_activated', + 42123, 4200, 1.234, {'revenue': 4200, 'value': 1.234, 'non-revenue': 'abc'}) + + snapshots = Snapshot([event]) + user = Visitor([snapshots], [visitor_attr_1, visitor_attr_2], 'test_user') + + batch.visitors = [user] + + self.assertEqual(batch, expected_params)