Skip to content

Commit 193aba7

Browse files
committed
Merge pull request #112 from jkeyes/bulk-api
Adding Bulk API support.
2 parents 0ccdd42 + 89b38ac commit 193aba7

File tree

10 files changed

+349
-3
lines changed

10 files changed

+349
-3
lines changed

intercom/api_operations/bulk.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# -*- coding: utf-8 -*-
2+
"""Support for the Intercom Bulk API.
3+
4+
Ref: https://developers.intercom.io/reference#bulk-apis
5+
"""
6+
7+
from intercom import utils
8+
9+
10+
def item_for_api(method, data_type, item):
11+
"""Return a Bulk API item."""
12+
return {
13+
'method': method,
14+
'data_type': data_type,
15+
'data': item
16+
}
17+
18+
19+
class Submit(object):
20+
"""Provide Bulk API support to subclasses."""
21+
22+
def submit_bulk_job(self, create_items=[], delete_items=[], job_id=None):
23+
"""Submit a Bulk API job."""
24+
from intercom import event
25+
from intercom.errors import HttpError
26+
from intercom.job import Job
27+
28+
if self.collection_class == event.Event and delete_items:
29+
raise Exception("Events do not support bulk delete operations.")
30+
data_type = utils.resource_class_to_name(self.collection_class)
31+
collection_name = utils.resource_class_to_collection_name(self.collection_class)
32+
create_items = [item_for_api('post', data_type, item) for item in create_items]
33+
delete_items = [item_for_api('delete', data_type, item) for item in delete_items]
34+
35+
bulk_request = {
36+
'items': create_items + delete_items
37+
}
38+
if job_id:
39+
bulk_request['job'] = {'id': job_id}
40+
41+
response = self.client.post('/bulk/%s' % (collection_name), bulk_request)
42+
if not response:
43+
raise HttpError('HTTP Error - No response entity returned.')
44+
return Job().from_response(response)
45+
46+
47+
class LoadErrorFeed(object):
48+
"""Provide access to Bulk API error feed for a specific job."""
49+
50+
def errors(self, id):
51+
"""Return errors for the Bulk API job specified."""
52+
from intercom.errors import HttpError
53+
from intercom.job import Job
54+
response = self.client.get("/jobs/%s/error" % (id), {})
55+
if not response:
56+
raise HttpError('Http Error - No response entity returned.')
57+
return Job.from_api(response)

intercom/client.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,11 @@ def users(self):
6868
from intercom.service import user
6969
return user.User(self)
7070

71+
@property
72+
def jobs(self):
73+
from intercom.service import job
74+
return job.Job(self)
75+
7176
def _execute_request(self, request, params):
7277
result = request.execute(self.base_url, self._auth, params)
7378
self.rate_limit_details = request.rate_limit_details

intercom/job.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# -*- coding: utf-8 -*- # noqa
2+
3+
from intercom.traits.api_resource import Resource
4+
5+
6+
class Job(Resource):
7+
"""A Bulk API Job.
8+
9+
Ref: https://developers.intercom.io/reference#bulk-job-model
10+
"""

intercom/service/event.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
# -*- coding: utf-8 -*-
22

33
from intercom import event
4+
from intercom.api_operations.bulk import Submit
45
from intercom.api_operations.save import Save
56
from intercom.service.base_service import BaseService
67

78

8-
class Event(BaseService, Save):
9+
class Event(BaseService, Save, Submit):
910

1011
@property
1112
def collection_class(self):

intercom/service/job.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# -*- coding: utf-8 -*-
2+
3+
from intercom import job
4+
from intercom.api_operations.all import All
5+
from intercom.api_operations.bulk import LoadErrorFeed
6+
from intercom.api_operations.find import Find
7+
from intercom.api_operations.find_all import FindAll
8+
from intercom.api_operations.save import Save
9+
from intercom.api_operations.load import Load
10+
from intercom.service.base_service import BaseService
11+
12+
13+
class Job(BaseService, All, Find, FindAll, Save, Load, LoadErrorFeed):
14+
15+
@property
16+
def collection_class(self):
17+
return job.Job

intercom/service/user.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from intercom import user
44
from intercom.api_operations.all import All
5+
from intercom.api_operations.bulk import Submit
56
from intercom.api_operations.find import Find
67
from intercom.api_operations.find_all import FindAll
78
from intercom.api_operations.delete import Delete
@@ -10,7 +11,7 @@
1011
from intercom.service.base_service import BaseService
1112

1213

13-
class User(BaseService, All, Find, FindAll, Delete, Save, Load):
14+
class User(BaseService, All, Find, FindAll, Delete, Save, Load, Submit):
1415

1516
@property
1617
def collection_class(self):

intercom/traits/api_resource.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,13 @@ def to_datetime_value(value):
3333

3434

3535
class Resource(object):
36+
client = None
3637
changed_attributes = []
3738

38-
def __init__(_self, **params): # noqa
39+
def __init__(_self, *args, **params): # noqa
40+
if args:
41+
_self.client = args[0]
42+
3943
# intercom includes a 'self' field in the JSON, to avoid the naming
4044
# conflict we go with _self here
4145
_self.from_dict(params)

tests/unit/test_event.py

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,103 @@ def it_creates_an_event_without_metadata(self):
4848
with patch.object(Client, 'post', return_value=data) as mock_method:
4949
self.client.events.create(**data)
5050
mock_method.assert_called_once_with('/events/', data)
51+
52+
class DescribeBulkOperations(unittest.TestCase): # noqa
53+
def setUp(self): # noqa
54+
self.client = Client()
55+
56+
self.job = {
57+
"app_id": "app_id",
58+
"id": "super_awesome_job",
59+
"created_at": 1446033421,
60+
"completed_at": 1446048736,
61+
"closing_at": 1446034321,
62+
"updated_at": 1446048736,
63+
"name": "api_bulk_job",
64+
"state": "completed",
65+
"links": {
66+
"error": "https://api.intercom.io/jobs/super_awesome_job/error",
67+
"self": "https://api.intercom.io/jobs/super_awesome_job"
68+
},
69+
"tasks": [
70+
{
71+
"id": "super_awesome_task",
72+
"item_count": 2,
73+
"created_at": 1446033421,
74+
"started_at": 1446033709,
75+
"completed_at": 1446033709,
76+
"state": "completed"
77+
}
78+
]
79+
}
80+
81+
self.bulk_request = {
82+
"items": [
83+
{
84+
"method": "post",
85+
"data_type": "event",
86+
"data": {
87+
"event_name": "ordered-item",
88+
"created_at": 1438944980,
89+
"user_id": "314159",
90+
"metadata": {
91+
"order_date": 1438944980,
92+
"stripe_invoice": "inv_3434343434"
93+
}
94+
}
95+
},
96+
{
97+
"method": "post",
98+
"data_type": "event",
99+
"data": {
100+
"event_name": "invited-friend",
101+
"created_at": 1438944979,
102+
"user_id": "314159",
103+
"metadata": {
104+
"invitee_email": "pi@example.org",
105+
"invite_code": "ADDAFRIEND"
106+
}
107+
}
108+
}
109+
]
110+
}
111+
112+
self.events = [
113+
{
114+
"event_name": "ordered-item",
115+
"created_at": 1438944980,
116+
"user_id": "314159",
117+
"metadata": {
118+
"order_date": 1438944980,
119+
"stripe_invoice": "inv_3434343434"
120+
}
121+
},
122+
{
123+
"event_name": "invited-friend",
124+
"created_at": 1438944979,
125+
"user_id": "314159",
126+
"metadata": {
127+
"invitee_email": "pi@example.org",
128+
"invite_code": "ADDAFRIEND"
129+
}
130+
}
131+
]
132+
133+
@istest
134+
def it_submits_a_bulk_job(self): # noqa
135+
with patch.object(Client, 'post', return_value=self.job) as mock_method: # noqa
136+
self.client.events.submit_bulk_job(create_items=self.events)
137+
mock_method.assert_called_once_with('/bulk/events', self.bulk_request)
138+
139+
@istest
140+
def it_adds_events_to_an_existing_bulk_job(self): # noqa
141+
self.bulk_request['job'] = {'id': 'super_awesome_job'}
142+
with patch.object(Client, 'post', return_value=self.job) as mock_method: # noqa
143+
self.client.events.submit_bulk_job(
144+
create_items=self.events, job_id='super_awesome_job')
145+
mock_method.assert_called_once_with('/bulk/events', self.bulk_request)
146+
147+
@istest
148+
def it_does_not_submit_delete_jobs(self): # noqa
149+
with self.assertRaises(Exception):
150+
self.client.events.submit_bulk_job(delete_items=self.events)

tests/unit/test_job.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# -*- coding: utf-8 -*- # noqa
2+
3+
import unittest
4+
5+
from intercom.client import Client
6+
from mock import patch
7+
from nose.tools import istest
8+
9+
10+
class DescribeJobs(unittest.TestCase): # noqa
11+
def setUp(self): # noqa
12+
self.client = Client()
13+
14+
self.job = {
15+
"app_id": "app_id",
16+
"id": "super_awesome_job",
17+
"created_at": 1446033421,
18+
"completed_at": 1446048736,
19+
"closing_at": 1446034321,
20+
"updated_at": 1446048736,
21+
"name": "api_bulk_job",
22+
"state": "completed",
23+
"links": {
24+
"error": "https://api.intercom.io/jobs/super_awesome_job/error",
25+
"self": "https://api.intercom.io/jobs/super_awesome_job"
26+
},
27+
"tasks": [
28+
{
29+
"id": "super_awesome_task",
30+
"item_count": 2,
31+
"created_at": 1446033421,
32+
"started_at": 1446033709,
33+
"completed_at": 1446033709,
34+
"state": "completed"
35+
}
36+
]
37+
}
38+
39+
self.error_feed = {
40+
"app_id": "app_id",
41+
"job_id": "super_awesome_job",
42+
"pages": {},
43+
"items": []
44+
}
45+
46+
@istest
47+
def it_gets_a_job(self): # noqa
48+
with patch.object(Client, 'get', return_value=self.job) as mock_method: # noqa
49+
self.client.jobs.find(id='super_awesome_job')
50+
mock_method.assert_called_once_with('/jobs/super_awesome_job', {})
51+
52+
@istest
53+
def it_gets_a_jobs_error_feed(self): # noqa
54+
with patch.object(Client, 'get', return_value=self.error_feed) as mock_method: # noqa
55+
self.client.jobs.errors(id='super_awesome_job')
56+
mock_method.assert_called_once_with('/jobs/super_awesome_job/error', {})

0 commit comments

Comments
 (0)