From 1be6b3dd0e8fd328418717b7b298e869db423a9e Mon Sep 17 00:00:00 2001 From: Anne Fleischer Date: Wed, 5 Dec 2018 16:32:26 +0100 Subject: [PATCH] add support for event log API (#135) --- closeio/closeio.py | 11 ++++- closeio/contrib/testing_stub.py | 87 +++++++++++++++++++++++++++++++++ closeio/utils.py | 26 ++++++++++ tests/test_end_to_end.py | 20 ++++++++ tests/test_testing_stub.py | 49 +++++++++++++++++++ tests/test_utils.py | 16 ++++++ 6 files changed, 208 insertions(+), 1 deletion(-) create mode 100644 tests/test_testing_stub.py create mode 100644 tests/test_utils.py diff --git a/closeio/closeio.py b/closeio/closeio.py index 02629dd..b58e074 100644 --- a/closeio/closeio.py +++ b/closeio/closeio.py @@ -6,7 +6,8 @@ from closeio.exceptions import CloseIOError from closeio.utils import ( - DummyCookieJar, convert, handle_errors, paginate, parse_response + DummyCookieJar, convert, handle_errors, paginate, paginate_via_cursor, + parse_response ) logger = logging.getLogger(__name__) @@ -507,3 +508,11 @@ def create_lead_export(self, query='*', format='json', fields=(), @handle_errors def get_export(self, id): return self._api.export(id).get() + + @parse_response + @handle_errors + def get_event_logs(self, **kwargs): + return paginate_via_cursor( + self._api.event.get, + **kwargs + ) diff --git a/closeio/contrib/testing_stub.py b/closeio/contrib/testing_stub.py index 822d6cc..b304894 100644 --- a/closeio/contrib/testing_stub.py +++ b/closeio/contrib/testing_stub.py @@ -2,14 +2,19 @@ import itertools import threading import uuid +from contextlib import contextmanager from datetime import datetime, timezone +from dateutil.parser import parse + from closeio.utils import CloseIOError, Item, parse_response threadlocal = threading.local() class CloseIOStub(object): + record_event_logs = False + def __init__(self, user_emails=None): users = self._data('users', []) if user_emails: @@ -36,6 +41,54 @@ def _data(self, attr, default=None): return storage[attr] + def _create_task_log(self, task): + return { + 'id': 'ev_{}'.format(uuid.uuid4().hex), + 'object_type': 'task.lead', + 'object_id': task['id'], + 'lead_id': task['lead_id'], + 'user_id': task['assigned_to'], + 'date_updated': datetime.utcnow().isoformat(), + } + + def _create_task_log_created(self, task): + logs = self._data('event_logs', []) + log = self._create_task_log(task) + log.update({ + 'action': 'created', + 'data': task, + 'previous_data': {}, + }) + logs.insert(0, log) + + def _create_task_log_deleted(self, task): + logs = self._data('event_logs', []) + log = self._create_task_log(task) + log.update({ + 'action': 'deleted', + 'data': {}, + 'previous_data': task, + }) + logs.insert(0, log) + + @contextmanager + def record_logs(self): + """Create event log data for certain operations. + + This context manager can be used to automatically create + event logs when calling certain methods on the stub. + Currently only event log data for task creation and deletion + is supported. + + Example usage:: + with stub.record_logs(): + task = stub.create_task(lead_id='x', assigned_to='y', text='z') + stub.delete_task(task_id=task['id']) + """ + self.record_event_logs = True + yield + self.record_event_logs = False + @parse_response def find_opportunity_status(self, label): opportunity_status = self._data('opportunity_status', []) @@ -233,6 +286,8 @@ def delete_task(self, task_id): for t in tasks_list: if t['id'] == task_id: tasks[lead_id].remove(t) + if self.record_event_logs: + self._create_task_log_deleted(t) return True raise CloseIOError() @@ -352,6 +407,9 @@ def create_task(self, lead_id, assigned_to, text, due_date=None, tasks[lead_id].append(task) + if self.record_event_logs: + self._create_task_log_created(task) + return task def _get_task(self, task_id): @@ -552,6 +610,35 @@ def delete_opportunity(self, opportunity_id): del opportunities[opportunity_id] + @parse_response + def get_event_logs(self, **kwargs): + logs = self._data('event_logs', []) + + for param in ['action', 'object_type', 'object_id', 'lead_id', 'user_id']: + if param in kwargs: + logs = [log for log in logs if log[param] == kwargs[param]] + + if 'date_updated__gt' in kwargs: + date_updated = parse(kwargs['date_updated__gt']) + logs = [log for log in logs if parse(log['date_updated']) > date_updated] + + if 'date_updated__gte' in kwargs: + date_updated = parse(kwargs['date_updated__gte']) + logs = [log for log in logs if parse(log['date_updated']) >= date_updated] + + if 'date_updated__lt' in kwargs: + date_updated = parse(kwargs['date_updated__lt']) + logs = [log for log in logs if parse(log['date_updated']) < date_updated] + + if 'date_updated__lte' in kwargs: + date_updated = parse(kwargs['date_updated__lte']) + logs = [log for log in logs if parse(log['date_updated']) <= date_updated] + + if '_limit' in kwargs: + logs = logs[:kwargs['_limit']] + + return logs + @parse_response def get_export(self, id): exports = self._data('exports', []) diff --git a/closeio/utils.py b/closeio/utils.py index 2b0b352..b4167b4 100644 --- a/closeio/utils.py +++ b/closeio/utils.py @@ -160,6 +160,32 @@ def paginate(func, *args, **kwargs): skip += limit +def paginate_via_cursor(func, *args, **kwargs): + cursor = '' + limit = 50 + + while True: + kwargs['_cursor'] = cursor + kwargs['_limit'] = limit + + with convert_errors(): + response = func(*args, **kwargs) + + if not isinstance(response, dict): + raise CloseIOError( + 'close.io response is not a dict, ' + 'so most likely could not be parsed. \n' + 'body "{}"'.format(response) + ) + + for item in response['data']: + yield item + + cursor = response['cursor_next'] + if not cursor: + break + + class DummyCookieJar(object): def __init__(self, policy=None): pass diff --git a/tests/test_end_to_end.py b/tests/test_end_to_end.py index e56e988..06e41b5 100644 --- a/tests/test_end_to_end.py +++ b/tests/test_end_to_end.py @@ -217,6 +217,26 @@ def test_delete_activity_note(self, client, lead): assert list(client.get_activity_note(lead['id'])) == [] + def test_get_event_logs(self, client, task): + task_id = task['id'] + + logs = client.get_event_logs(object_type='task.lead', action='created') + logs = list(logs) + assert len(logs) >= 1 + assert logs[0]['object_id'] == task_id + assert all(log['action'] == 'created' for log in logs) + + iso_now = datetime.datetime.utcnow().isoformat() + client.delete_task(task['id']) + time.sleep(1) # give closeio some time to create the event log on their system + + logs = client.get_event_logs(date_updated__gte=iso_now, object_type='task.lead', + action='deleted') + logs = list(logs) + assert len(logs) == 1 + assert logs[0]['object_id'] == task_id + assert logs[0]['action'] == 'deleted' + def test_create_lead_export(self, client, random_string): export = client.create_lead_export(random_string) assert export diff --git a/tests/test_testing_stub.py b/tests/test_testing_stub.py new file mode 100644 index 0000000..28b25c9 --- /dev/null +++ b/tests/test_testing_stub.py @@ -0,0 +1,49 @@ +import datetime + +from closeio.contrib.testing_stub import CloseIOStub + + +class TestCloseIOStub: + def test_record_and_retrieve_event_logs(self): + client = CloseIOStub() + + task1 = client.create_task(lead_id='x1', assigned_to='y1', text='z1') + client.delete_task(task_id=task1['id']) + + assert list(client.get_event_logs()) == [] + + with client.record_logs(): + task2 = client.create_task(lead_id='x2', assigned_to='y2', text='z2') + client.delete_task(task_id=task2['id']) + pause = datetime.datetime.utcnow().isoformat() + task3 = client.create_task(lead_id='x3', assigned_to='y3', text='z3') + client.delete_task(task_id=task3['id']) + + logs = list(client.get_event_logs()) + assert len(logs) == 4 + assert logs[0]['action'] == 'deleted' + assert logs[0]['object_id'] == task3['id'] + assert logs[1]['action'] == 'created' + assert logs[1]['object_id'] == task3['id'] + assert logs[2]['action'] == 'deleted' + assert logs[2]['object_id'] == task2['id'] + assert logs[3]['action'] == 'created' + assert logs[3]['object_id'] == task2['id'] + + logs = list(client.get_event_logs(action='created')) + assert len(logs) == 2 + + logs = list(client.get_event_logs(action='deleted')) + assert len(logs) == 2 + + logs = list(client.get_event_logs(object_type='task.lead')) + assert len(logs) == 4 + assert logs[0]['object_id'] == task3['id'] + assert logs[1]['object_id'] == task3['id'] + assert logs[2]['object_id'] == task2['id'] + assert logs[3]['object_id'] == task2['id'] + + logs = list(client.get_event_logs(object_type='task.lead', date_updated__gt=pause)) + assert len(logs) == 2 + assert logs[0]['object_id'] == task3['id'] + assert logs[1]['object_id'] == task3['id'] diff --git a/tests/test_utils.py b/tests/test_utils.py new file mode 100644 index 0000000..9ae5127 --- /dev/null +++ b/tests/test_utils.py @@ -0,0 +1,16 @@ +from closeio.utils import paginate_via_cursor + + +def test_paginate_via_cursor(): + responses = { + '': {'cursor_next': '11111', 'data': [{'id': 1}, {'id': 2}]}, + '11111': {'cursor_next': '22222', 'data': [{'id': 3}, {'id': 4}]}, + '22222': {'cursor_next': '', 'data': [{'id': 5}, {'id': 6}]} + } + + def test_function(**kwargs): + cursor = kwargs.get('_cursor') + return responses[cursor] + + response = paginate_via_cursor(test_function) + assert list(response) == [{'id': 1}, {'id': 2}, {'id': 3}, {'id': 4}, {'id': 5}, {'id': 6}]