Skip to content

Commit 84f1a48

Browse files
authored
Merge pull request intercom#156 from jkeyes/scroll-api
Adding support for Scroll API.
2 parents 407ab14 + 56db21e commit 84f1a48

File tree

5 files changed

+210
-1
lines changed

5 files changed

+210
-1
lines changed

intercom/api_operations/scroll.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# -*- coding: utf-8 -*-
2+
"""Operation to scroll through users."""
3+
4+
from intercom import utils
5+
from intercom.scroll_collection_proxy import ScrollCollectionProxy
6+
7+
8+
class Scroll(object):
9+
"""A mixin that provides `scroll` functionality."""
10+
11+
def scroll(self, **params):
12+
"""Find all instances of the resource based on the supplied parameters."""
13+
collection_name = utils.resource_class_to_collection_name(
14+
self.collection_class)
15+
finder_url = "/{}/scroll".format(collection_name)
16+
return ScrollCollectionProxy(
17+
self.client, self.collection_class, collection_name, finder_url)

intercom/scroll_collection_proxy.py

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
# -*- coding: utf-8 -*-
2+
"""Proxy for the Scroll API."""
3+
import six
4+
from intercom import HttpError
5+
6+
7+
class ScrollCollectionProxy(six.Iterator):
8+
"""A proxy to iterate over resources returned by the Scroll API."""
9+
10+
def __init__(self, client, resource_class, resource_name, scroll_url):
11+
"""Initialise the proxy."""
12+
self.client = client
13+
14+
# resource name
15+
self.resource_name = resource_name
16+
17+
# resource class
18+
self.resource_class = resource_class
19+
20+
# the original URL to retrieve the resources
21+
self.scroll_url = scroll_url
22+
23+
# the identity of the scroll, extracted from the response
24+
self.scroll_param = None
25+
26+
# an iterator over the resources found in the response
27+
self.resources = None
28+
29+
# a link to the next page of results
30+
self.next_page = None
31+
32+
def __iter__(self):
33+
"""Return self as the proxy has __next__ implemented."""
34+
return self
35+
36+
def __next__(self):
37+
"""Return the next resource from the response."""
38+
if self.resources is None:
39+
# get the first page of results
40+
self.get_first_page()
41+
42+
# try to get a resource if there are no more in the
43+
# current resource iterator (StopIteration is raised)
44+
# try to get the next page of results first
45+
try:
46+
resource = six.next(self.resources)
47+
except StopIteration:
48+
self.get_next_page()
49+
resource = six.next(self.resources)
50+
51+
instance = self.resource_class(**resource)
52+
return instance
53+
54+
def __getitem__(self, index):
55+
"""Return an exact item from the proxy."""
56+
for i in range(index):
57+
six.next(self)
58+
return six.next(self)
59+
60+
def get_first_page(self):
61+
"""Return the first page of results."""
62+
return self.get_page(self.scroll_param)
63+
64+
def get_next_page(self):
65+
"""Return the next page of results."""
66+
return self.get_page(self.scroll_param)
67+
68+
def get_page(self, scroll_param=None):
69+
"""Retrieve a page of results from the Scroll API."""
70+
if scroll_param is None:
71+
response = self.client.get(self.scroll_url, {})
72+
else:
73+
response = self.client.get(self.scroll_url, {'scroll_param': scroll_param})
74+
75+
if response is None:
76+
raise HttpError('Http Error - No response entity returned')
77+
78+
# create the resource iterator
79+
collection = response[self.resource_name]
80+
self.resources = iter(collection)
81+
# grab the next page URL if one exists
82+
self.scroll_param = self.extract_scroll_param(response)
83+
84+
def records_present(self, response):
85+
"""Return whether there are resources in the response."""
86+
return len(response.get(self.resource_name)) > 0
87+
88+
def extract_scroll_param(self, response):
89+
"""Extract the scroll_param from the response."""
90+
if self.records_present(response):
91+
return response.get('scroll_param')

intercom/service/user.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,12 @@
88
from intercom.api_operations.delete import Delete
99
from intercom.api_operations.save import Save
1010
from intercom.api_operations.load import Load
11+
from intercom.api_operations.scroll import Scroll
1112
from intercom.extended_api_operations.tags import Tags
1213
from intercom.service.base_service import BaseService
1314

1415

15-
class User(BaseService, All, Find, FindAll, Delete, Save, Load, Submit, Tags):
16+
class User(BaseService, All, Find, FindAll, Delete, Save, Load, Submit, Tags, Scroll):
1617

1718
@property
1819
def collection_class(self):

tests/unit/__init__.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,24 @@ def page_of_users(include_next_link=False):
196196
return page
197197

198198

199+
def users_scroll(include_users=False): # noqa
200+
# a "page" of results from the Scroll API
201+
if include_users:
202+
users = [
203+
get_user("[email protected]"),
204+
get_user("[email protected]"),
205+
get_user("[email protected]")
206+
]
207+
else:
208+
users = []
209+
210+
return {
211+
"type": "user.list",
212+
"scroll_param": "da6bbbac-25f6-4f07-866b-b911082d7",
213+
"users": users
214+
}
215+
216+
199217
def page_of_events(include_next_link=False):
200218
page = {
201219
"type": "event.list",
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
# -*- coding: utf-8 -*-
2+
"""Test module for Scroll Collection Proxy."""
3+
import unittest
4+
5+
from intercom import HttpError
6+
from intercom.client import Client
7+
from mock import call
8+
from mock import patch
9+
from nose.tools import assert_raises
10+
from nose.tools import eq_
11+
from nose.tools import istest
12+
from tests.unit import users_scroll
13+
14+
15+
class CollectionProxyTest(unittest.TestCase): # noqa
16+
17+
def setUp(self): # noqa
18+
self.client = Client()
19+
20+
@istest
21+
def it_stops_iterating_if_no_users_returned(self): # noqa
22+
body = users_scroll(include_users=False)
23+
with patch.object(Client, 'get', return_value=body) as mock_method:
24+
emails = [user.email for user in self.client.users.scroll()]
25+
mock_method.assert_called('/users/scroll', {})
26+
eq_(emails, []) # noqa
27+
28+
@istest
29+
def it_keeps_iterating_if_users_returned(self): # noqa
30+
page1 = users_scroll(include_users=True)
31+
page2 = users_scroll(include_users=False)
32+
side_effect = [page1, page2]
33+
with patch.object(Client, 'get', side_effect=side_effect) as mock_method: # noqa
34+
emails = [user.email for user in self.client.users.scroll()]
35+
eq_([call('/users/scroll', {}), call('/users/scroll', {'scroll_param': 'da6bbbac-25f6-4f07-866b-b911082d7'})], # noqa
36+
mock_method.mock_calls)
37+
38+
39+
@istest
40+
def it_supports_indexed_array_access(self): # noqa
41+
body = users_scroll(include_users=True)
42+
with patch.object(Client, 'get', return_value=body) as mock_method:
43+
eq_(self.client.users.scroll()[0].email, '[email protected]')
44+
mock_method.assert_called_once_with('/users/scroll', {})
45+
eq_(self.client.users.scroll()[1].email, '[email protected]')
46+
47+
@istest
48+
def it_returns_one_page_scroll(self): # noqa
49+
body = users_scroll(include_users=True)
50+
with patch.object(Client, 'get', return_value=body):
51+
scroll = self.client.users.scroll()
52+
scroll.get_next_page()
53+
emails = [user['email'] for user in scroll.resources]
54+
55+
56+
@istest
57+
def it_keeps_iterating_if_called_with_scroll_param(self): # noqa
58+
page1 = users_scroll(include_users=True)
59+
page2 = users_scroll(include_users=False)
60+
side_effect = [page1, page2]
61+
with patch.object(Client, 'get', side_effect=side_effect) as mock_method: # noqa
62+
scroll = self.client.users.scroll()
63+
scroll.get_page()
64+
scroll.get_page('da6bbbac-25f6-4f07-866b-b911082d7')
65+
emails = [user['email'] for user in scroll.resources]
66+
eq_(emails, []) # noqa
67+
68+
@istest
69+
def it_works_with_an_empty_list(self): # noqa
70+
body = users_scroll(include_users=False)
71+
with patch.object(Client, 'get', return_value=body) as mock_method: # noqa
72+
scroll = self.client.users.scroll()
73+
scroll.get_page()
74+
emails = [user['email'] for user in scroll.resources]
75+
eq_(emails, []) # noqa
76+
77+
@istest
78+
def it_raises_an_http_error(self): # noqa
79+
with patch.object(Client, 'get', return_value=None) as mock_method: # noqa
80+
scroll = self.client.users.scroll()
81+
with assert_raises(HttpError):
82+
scroll.get_page()

0 commit comments

Comments
 (0)