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/entity/__init__.py b/optimizely/event/entity/__init__.py new file mode 100644 index 00000000..d6094e5a --- /dev/null +++ b/optimizely/event/entity/__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/entity/conversion_event.py b/optimizely/event/entity/conversion_event.py new file mode 100644 index 00000000..e6cd746d --- /dev/null +++ b/optimizely/event/entity/conversion_event.py @@ -0,0 +1,25 @@ +# 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 .user_event import UserEvent + + +class ConversionEvent(UserEvent): + """ Class representing Conversion Event. """ + + def __init__(self, event_context, event, user_id, visitor_attributes, event_tags, bot_filtering=None): + 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 diff --git a/optimizely/event/entity/decision.py b/optimizely/event/entity/decision.py new file mode 100644 index 00000000..60b965dc --- /dev/null +++ b/optimizely/event/entity/decision.py @@ -0,0 +1,19 @@ +# 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. + + +class Decision(object): + def __init__(self, compaign_id, experiment_id, variation_id): + self.campaign_id = compaign_id + self.experiment_id = experiment_id + self.variation_id = variation_id diff --git a/optimizely/event/entity/event_batch.py b/optimizely/event/entity/event_batch.py new file mode 100644 index 00000000..4bdf008c --- /dev/null +++ b/optimizely/event/entity/event_batch.py @@ -0,0 +1,25 @@ +# 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. + + +class EventBatch(object): + 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 diff --git a/optimizely/event/entity/event_context.py b/optimizely/event/entity/event_context.py new file mode 100644 index 00000000..5d7efcc5 --- /dev/null +++ b/optimizely/event/entity/event_context.py @@ -0,0 +1,27 @@ +# 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 .. import version + +SDK_VERSION = 'python-sdk' + + +class EventContext(object): + """ Class respresenting 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 = SDK_VERSION + self.client_version = version.__version__ + self.anonymize_ip = anonymize_ip diff --git a/optimizely/event/entity/impression_event.py b/optimizely/event/entity/impression_event.py new file mode 100644 index 00000000..044fb163 --- /dev/null +++ b/optimizely/event/entity/impression_event.py @@ -0,0 +1,25 @@ +# 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 .user_event import UserEvent + + +class ImpressionEvent(UserEvent): + """ Class representing Impression Event. """ + + def __init__(self, event_context, user_id, experiment, visitor_attributes, variation, bot_filtering=None): + 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 diff --git a/optimizely/event/entity/snapshot.py b/optimizely/event/entity/snapshot.py new file mode 100644 index 00000000..726eccdb --- /dev/null +++ b/optimizely/event/entity/snapshot.py @@ -0,0 +1,18 @@ +# 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. + + +class Snapshot(object): + def __init__(self, events, decisions=None): + self.events = events + self.decisions = decisions diff --git a/optimizely/event/entity/snapshot_event.py b/optimizely/event/entity/snapshot_event.py new file mode 100644 index 00000000..ef2bdf8a --- /dev/null +++ b/optimizely/event/entity/snapshot_event.py @@ -0,0 +1,25 @@ +# 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. + + +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 diff --git a/optimizely/event/entity/user_event.py b/optimizely/event/entity/user_event.py new file mode 100644 index 00000000..a6343d0d --- /dev/null +++ b/optimizely/event/entity/user_event.py @@ -0,0 +1,29 @@ +# 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 + + +class UserEvent(object): + """ Class respresenting Event Context. """ + + 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()) diff --git a/optimizely/event/entity/visitor.py b/optimizely/event/entity/visitor.py new file mode 100644 index 00000000..d9886b0e --- /dev/null +++ b/optimizely/event/entity/visitor.py @@ -0,0 +1,19 @@ +# 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. + + +class Visitor(object): + def __init__(self, snapshots, attributes, visitor_id): + self.snapshots = snapshots + self.attributes = attributes + self.visitor_id = visitor_id diff --git a/optimizely/event/entity/visitor_attribute.py b/optimizely/event/entity/visitor_attribute.py new file mode 100644 index 00000000..cafe58c5 --- /dev/null +++ b/optimizely/event/entity/visitor_attribute.py @@ -0,0 +1,20 @@ +# 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. + + +class VisitorAttribute(object): + def __init__(self, entity_id, key, event_type, value): + self.entity_id = entity_id + self.key = key + self.type = event_type + self.value = value diff --git a/tests/test_event_entities.py b/tests/test_event_entities.py new file mode 100644 index 00000000..8b12d461 --- /dev/null +++ b/tests/test_event_entities.py @@ -0,0 +1,162 @@ +# 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 + +from operator import itemgetter + +from optimizely import version +from optimizely.event.entity import event_batch +from optimizely.event.entity import visitor_attribute +from optimizely.event.entity import snapshot_event +from optimizely.event.entity import visitor +from optimizely.event.entity import decision +from optimizely.event.entity import snapshot +from . import base + + +class EventEntitiesTest(base.BaseTest): + def _validate_event_object(self, expected_params, event_obj): + """ Helper method to validate properties of the event object. """ + + expected_params['visitors'][0]['attributes'] = \ + sorted(expected_params['visitors'][0]['attributes'], key=itemgetter('key')) + event_obj['visitors'][0]['attributes'] = \ + sorted(event_obj['visitors'][0]['attributes'], key=itemgetter('key')) + self.assertEqual(expected_params, event_obj) + + 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']: + continue + else: + result[k] = v + return result + + def TestImpressionEventEqualsSerializedPayload(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 = event_batch.EventBatch("12001", "111001", "42", "python-sdk", version.__version__, + False, True) + visitor_attr = visitor_attribute.VisitorAttribute("111094", "test_attribute", "custom", "test_value") + event = snapshot_event.SnapshotEvent("111182", "a68cf1ad-0393-4e18-af87-efe8f01a7c9c", "campaign_activated", + 42123) + event_decision = decision.Decision("111182", "111127", "111129") + + snapshots = snapshot.Snapshot([event], [event_decision]) + user = visitor.Visitor([snapshots], [visitor_attr], "test_user") + + batch.visitors = [user] + + self.maxDiff = None + self._validate_event_object(expected_params, + json.loads( + json.dumps(batch.__dict__, default=lambda o: o.__dict__), + object_pairs_hook=self.dict_clean + )) + + def TestConversionEventEqualsSerializedPayload(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': [{ + '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', + '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 = event_batch.EventBatch("12001", "111001", "42", "python-sdk", version.__version__, + False, True) + visitor_attr_1 = visitor_attribute.VisitorAttribute("111094", "test_attribute", "custom", "test_value") + visitor_attr_2 = visitor_attribute.VisitorAttribute("111095", "test_attribute2", "custom", "test_value2") + event = snapshot_event.SnapshotEvent("111182", "a68cf1ad-0393-4e18-af87-efe8f01a7c9c", "campaign_activated", + 42123, 4200, 1.234, {'revenue': 4200, 'value': 1.234, 'non-revenue': 'abc'}) + event_decision = decision.Decision("111182", "111127", "111129") + + snapshots = snapshot.Snapshot([event], [event_decision]) + user = visitor.Visitor([snapshots], [visitor_attr_1, visitor_attr_2], "test_user") + + batch.visitors = [user] + + self._validate_event_object(expected_params, + json.loads( + json.dumps(batch.__dict__, default=lambda o: o.__dict__), + object_pairs_hook=self.dict_clean + ))