diff --git a/intercom/api_operations/bulk.py b/intercom/api_operations/bulk.py new file mode 100644 index 00000000..7681ceb8 --- /dev/null +++ b/intercom/api_operations/bulk.py @@ -0,0 +1,57 @@ +# -*- coding: utf-8 -*- +"""Support for the Intercom Bulk API. + +Ref: https://developers.intercom.io/reference#bulk-apis +""" + +from intercom import utils + + +def item_for_api(method, data_type, item): + """Return a Bulk API item.""" + return { + 'method': method, + 'data_type': data_type, + 'data': item + } + + +class Submit(object): + """Provide Bulk API support to subclasses.""" + + def submit_bulk_job(self, create_items=[], delete_items=[], job_id=None): + """Submit a Bulk API job.""" + from intercom import event + from intercom.errors import HttpError + from intercom.job import Job + + if self.collection_class == event.Event and delete_items: + raise Exception("Events do not support bulk delete operations.") + data_type = utils.resource_class_to_name(self.collection_class) + collection_name = utils.resource_class_to_collection_name(self.collection_class) + create_items = [item_for_api('post', data_type, item) for item in create_items] + delete_items = [item_for_api('delete', data_type, item) for item in delete_items] + + bulk_request = { + 'items': create_items + delete_items + } + if job_id: + bulk_request['job'] = {'id': job_id} + + response = self.client.post('/bulk/%s' % (collection_name), bulk_request) + if not response: + raise HttpError('HTTP Error - No response entity returned.') + return Job().from_response(response) + + +class LoadErrorFeed(object): + """Provide access to Bulk API error feed for a specific job.""" + + def errors(self, id): + """Return errors for the Bulk API job specified.""" + from intercom.errors import HttpError + from intercom.job import Job + response = self.client.get("/jobs/%s/error" % (id), {}) + if not response: + raise HttpError('Http Error - No response entity returned.') + return Job.from_api(response) diff --git a/intercom/client.py b/intercom/client.py index f91a63c3..aba9741f 100644 --- a/intercom/client.py +++ b/intercom/client.py @@ -68,6 +68,11 @@ def users(self): from intercom.service import user return user.User(self) + @property + def jobs(self): + from intercom.service import job + return job.Job(self) + def _execute_request(self, request, params): result = request.execute(self.base_url, self._auth, params) self.rate_limit_details = request.rate_limit_details diff --git a/intercom/job.py b/intercom/job.py new file mode 100644 index 00000000..d501a0b1 --- /dev/null +++ b/intercom/job.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- # noqa + +from intercom.traits.api_resource import Resource + + +class Job(Resource): + """A Bulk API Job. + + Ref: https://developers.intercom.io/reference#bulk-job-model + """ diff --git a/intercom/service/event.py b/intercom/service/event.py index eb73dff0..2243e6fe 100644 --- a/intercom/service/event.py +++ b/intercom/service/event.py @@ -1,11 +1,12 @@ # -*- coding: utf-8 -*- from intercom import event +from intercom.api_operations.bulk import Submit from intercom.api_operations.save import Save from intercom.service.base_service import BaseService -class Event(BaseService, Save): +class Event(BaseService, Save, Submit): @property def collection_class(self): diff --git a/intercom/service/job.py b/intercom/service/job.py new file mode 100644 index 00000000..0dcda25c --- /dev/null +++ b/intercom/service/job.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- + +from intercom import job +from intercom.api_operations.all import All +from intercom.api_operations.bulk import LoadErrorFeed +from intercom.api_operations.find import Find +from intercom.api_operations.find_all import FindAll +from intercom.api_operations.save import Save +from intercom.api_operations.load import Load +from intercom.service.base_service import BaseService + + +class Job(BaseService, All, Find, FindAll, Save, Load, LoadErrorFeed): + + @property + def collection_class(self): + return job.Job diff --git a/intercom/service/user.py b/intercom/service/user.py index a604c9a3..e219f2b3 100644 --- a/intercom/service/user.py +++ b/intercom/service/user.py @@ -2,6 +2,7 @@ from intercom import user from intercom.api_operations.all import All +from intercom.api_operations.bulk import Submit from intercom.api_operations.find import Find from intercom.api_operations.find_all import FindAll from intercom.api_operations.delete import Delete @@ -10,7 +11,7 @@ from intercom.service.base_service import BaseService -class User(BaseService, All, Find, FindAll, Delete, Save, Load): +class User(BaseService, All, Find, FindAll, Delete, Save, Load, Submit): @property def collection_class(self): diff --git a/intercom/traits/api_resource.py b/intercom/traits/api_resource.py index 23641e0b..524efbc7 100644 --- a/intercom/traits/api_resource.py +++ b/intercom/traits/api_resource.py @@ -33,9 +33,13 @@ def to_datetime_value(value): class Resource(object): + client = None changed_attributes = [] - def __init__(_self, **params): # noqa + def __init__(_self, *args, **params): # noqa + if args: + _self.client = args[0] + # intercom includes a 'self' field in the JSON, to avoid the naming # conflict we go with _self here _self.from_dict(params) diff --git a/tests/unit/test_event.py b/tests/unit/test_event.py index 9b134188..2b953413 100644 --- a/tests/unit/test_event.py +++ b/tests/unit/test_event.py @@ -48,3 +48,103 @@ def it_creates_an_event_without_metadata(self): with patch.object(Client, 'post', return_value=data) as mock_method: self.client.events.create(**data) mock_method.assert_called_once_with('/events/', data) + +class DescribeBulkOperations(unittest.TestCase): # noqa + def setUp(self): # noqa + self.client = Client() + + self.job = { + "app_id": "app_id", + "id": "super_awesome_job", + "created_at": 1446033421, + "completed_at": 1446048736, + "closing_at": 1446034321, + "updated_at": 1446048736, + "name": "api_bulk_job", + "state": "completed", + "links": { + "error": "https://api.intercom.io/jobs/super_awesome_job/error", + "self": "https://api.intercom.io/jobs/super_awesome_job" + }, + "tasks": [ + { + "id": "super_awesome_task", + "item_count": 2, + "created_at": 1446033421, + "started_at": 1446033709, + "completed_at": 1446033709, + "state": "completed" + } + ] + } + + self.bulk_request = { + "items": [ + { + "method": "post", + "data_type": "event", + "data": { + "event_name": "ordered-item", + "created_at": 1438944980, + "user_id": "314159", + "metadata": { + "order_date": 1438944980, + "stripe_invoice": "inv_3434343434" + } + } + }, + { + "method": "post", + "data_type": "event", + "data": { + "event_name": "invited-friend", + "created_at": 1438944979, + "user_id": "314159", + "metadata": { + "invitee_email": "pi@example.org", + "invite_code": "ADDAFRIEND" + } + } + } + ] + } + + self.events = [ + { + "event_name": "ordered-item", + "created_at": 1438944980, + "user_id": "314159", + "metadata": { + "order_date": 1438944980, + "stripe_invoice": "inv_3434343434" + } + }, + { + "event_name": "invited-friend", + "created_at": 1438944979, + "user_id": "314159", + "metadata": { + "invitee_email": "pi@example.org", + "invite_code": "ADDAFRIEND" + } + } + ] + + @istest + def it_submits_a_bulk_job(self): # noqa + with patch.object(Client, 'post', return_value=self.job) as mock_method: # noqa + self.client.events.submit_bulk_job(create_items=self.events) + mock_method.assert_called_once_with('/bulk/events', self.bulk_request) + + @istest + def it_adds_events_to_an_existing_bulk_job(self): # noqa + self.bulk_request['job'] = {'id': 'super_awesome_job'} + with patch.object(Client, 'post', return_value=self.job) as mock_method: # noqa + self.client.events.submit_bulk_job( + create_items=self.events, job_id='super_awesome_job') + mock_method.assert_called_once_with('/bulk/events', self.bulk_request) + + @istest + def it_does_not_submit_delete_jobs(self): # noqa + with self.assertRaises(Exception): + self.client.events.submit_bulk_job(delete_items=self.events) diff --git a/tests/unit/test_job.py b/tests/unit/test_job.py new file mode 100644 index 00000000..337b1471 --- /dev/null +++ b/tests/unit/test_job.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- # noqa + +import unittest + +from intercom.client import Client +from mock import patch +from nose.tools import istest + + +class DescribeJobs(unittest.TestCase): # noqa + def setUp(self): # noqa + self.client = Client() + + self.job = { + "app_id": "app_id", + "id": "super_awesome_job", + "created_at": 1446033421, + "completed_at": 1446048736, + "closing_at": 1446034321, + "updated_at": 1446048736, + "name": "api_bulk_job", + "state": "completed", + "links": { + "error": "https://api.intercom.io/jobs/super_awesome_job/error", + "self": "https://api.intercom.io/jobs/super_awesome_job" + }, + "tasks": [ + { + "id": "super_awesome_task", + "item_count": 2, + "created_at": 1446033421, + "started_at": 1446033709, + "completed_at": 1446033709, + "state": "completed" + } + ] + } + + self.error_feed = { + "app_id": "app_id", + "job_id": "super_awesome_job", + "pages": {}, + "items": [] + } + + @istest + def it_gets_a_job(self): # noqa + with patch.object(Client, 'get', return_value=self.job) as mock_method: # noqa + self.client.jobs.find(id='super_awesome_job') + mock_method.assert_called_once_with('/jobs/super_awesome_job', {}) + + @istest + def it_gets_a_jobs_error_feed(self): # noqa + with patch.object(Client, 'get', return_value=self.error_feed) as mock_method: # noqa + self.client.jobs.errors(id='super_awesome_job') + mock_method.assert_called_once_with('/jobs/super_awesome_job/error', {}) diff --git a/tests/unit/test_user.py b/tests/unit/test_user.py index c5dc10a4..3802f805 100644 --- a/tests/unit/test_user.py +++ b/tests/unit/test_user.py @@ -445,3 +445,98 @@ def it_can_save_after_increment(self): # noqa user.increment('mad') eq_(user.to_dict()['custom_attributes']['mad'], 1) self.client.users.save(user) + + +class DescribeBulkOperations(unittest.TestCase): # noqa + + def setUp(self): # noqa + self.client = Client() + + self.job = { + "app_id": "app_id", + "id": "super_awesome_job", + "created_at": 1446033421, + "completed_at": 1446048736, + "closing_at": 1446034321, + "updated_at": 1446048736, + "name": "api_bulk_job", + "state": "completed", + "links": { + "error": "https://api.intercom.io/jobs/super_awesome_job/error", + "self": "https://api.intercom.io/jobs/super_awesome_job" + }, + "tasks": [ + { + "id": "super_awesome_task", + "item_count": 2, + "created_at": 1446033421, + "started_at": 1446033709, + "completed_at": 1446033709, + "state": "completed" + } + ] + } + + self.bulk_request = { + "items": [ + { + "method": "post", + "data_type": "user", + "data": { + "user_id": 25, + "email": "alice@example.com" + } + }, + { + "method": "delete", + "data_type": "user", + "data": { + "user_id": 26, + "email": "bob@example.com" + } + } + ] + } + + self.users_to_create = [ + { + "user_id": 25, + "email": "alice@example.com" + } + ] + + self.users_to_delete = [ + { + "user_id": 26, + "email": "bob@example.com" + } + ] + + created_at = datetime.utcnow() + params = { + 'email': 'jo@example.com', + 'user_id': 'i-1224242', + 'custom_attributes': { + 'mad': 123, + 'another': 432, + 'other': time.mktime(created_at.timetuple()), + 'thing': 'yay' + } + } + self.user = User(**params) + + @istest + def it_submits_a_bulk_job(self): # noqa + with patch.object(Client, 'post', return_value=self.job) as mock_method: # noqa + self.client.users.submit_bulk_job( + create_items=self.users_to_create, delete_items=self.users_to_delete) + mock_method.assert_called_once_with('/bulk/users', self.bulk_request) + + @istest + def it_adds_users_to_an_existing_bulk_job(self): # noqa + self.bulk_request['job'] = {'id': 'super_awesome_job'} + with patch.object(Client, 'post', return_value=self.job) as mock_method: # noqa + self.client.users.submit_bulk_job( + create_items=self.users_to_create, delete_items=self.users_to_delete, + job_id='super_awesome_job') + mock_method.assert_called_once_with('/bulk/users', self.bulk_request)