Skip to content

Adding Bulk API support. #112

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 20, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions intercom/api_operations/bulk.py
Original file line number Diff line number Diff line change
@@ -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)
5 changes: 5 additions & 0 deletions intercom/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions intercom/job.py
Original file line number Diff line number Diff line change
@@ -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
"""
3 changes: 2 additions & 1 deletion intercom/service/event.py
Original file line number Diff line number Diff line change
@@ -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):
Expand Down
17 changes: 17 additions & 0 deletions intercom/service/job.py
Original file line number Diff line number Diff line change
@@ -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
3 changes: 2 additions & 1 deletion intercom/service/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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):
Expand Down
6 changes: 5 additions & 1 deletion intercom/traits/api_resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
100 changes: 100 additions & 0 deletions tests/unit/test_event.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
56 changes: 56 additions & 0 deletions tests/unit/test_job.py
Original file line number Diff line number Diff line change
@@ -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', {})
Loading