From 0f33ec8570f41d40492e480fbfe1012e0cc464b2 Mon Sep 17 00:00:00 2001 From: umarovt Date: Fri, 10 Apr 2020 17:55:37 +0200 Subject: [PATCH 1/9] Add constants from v2.0 --- .gitignore | 1 + intercom/client.py | 5 + intercom/collection_proxy.py | 16 +- intercom/contact.py | 14 + intercom/service/contact.py | 20 ++ tests/integration/__init__.py | 14 + tests/integration/test_contacts.py | 28 ++ tests/unit/__init__.py | 143 +++++++- tests/unit/test_contacts.py | 505 +++++++++++++++++++++++++++++ 9 files changed, 741 insertions(+), 5 deletions(-) create mode 100644 intercom/contact.py create mode 100644 intercom/service/contact.py create mode 100644 tests/integration/test_contacts.py create mode 100644 tests/unit/test_contacts.py diff --git a/.gitignore b/.gitignore index d0242b3f..1211ad96 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ dist venv +env .venv .coverage .env diff --git a/intercom/client.py b/intercom/client.py index ce37617b..361feb13 100644 --- a/intercom/client.py +++ b/intercom/client.py @@ -69,6 +69,11 @@ def tags(self): def users(self): from intercom.service import user return user.User(self) + + @property + def contacts(self): + from intercom.service import contact + return contact.Contact(self) @property def leads(self): diff --git a/intercom/collection_proxy.py b/intercom/collection_proxy.py index 2b419f30..8378ff70 100644 --- a/intercom/collection_proxy.py +++ b/intercom/collection_proxy.py @@ -82,7 +82,11 @@ def get_page(self, url, params={}): if response is None: raise HttpError('Http Error - No response entity returned') - collection = response[self.collection] + if self.collection in response: + collection = response[self.collection] + else: + collection = response['data'] + # if there are no resources in the response stop iterating if collection is None: raise StopIteration @@ -90,14 +94,18 @@ def get_page(self, url, params={}): # create the resource iterator self.resources = iter(collection) # grab the next page URL if one exists - self.next_page = self.extract_next_link(response) + self.next_page = self.extract_next_link(url, response) def paging_info_present(self, response): return 'pages' in response and 'type' in response['pages'] - def extract_next_link(self, response): + def extract_next_link(self, url, response): if self.paging_info_present(response): paging_info = response["pages"] - if paging_info["next"]: + if paging_info["next"] and isinstance(paging_info['next'], str) : next_parsed = six.moves.urllib.parse.urlparse(paging_info["next"]) return '{}?{}'.format(next_parsed.path, next_parsed.query) + else: + #cursor based pagination + return '{}?{}'.format(six.moves.urllib.parse.urlparse(url).path, paging_info['next']['starting_after']) + diff --git a/intercom/contact.py b/intercom/contact.py new file mode 100644 index 00000000..153bf728 --- /dev/null +++ b/intercom/contact.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- + +from intercom.traits.api_resource import Resource +from intercom.traits.incrementable_attributes import IncrementableAttributes + + +class Contact(Resource, IncrementableAttributes): + + update_verb = 'post' + identity_vars = ['id', 'email', 'role'] + + @property + def flat_store_attributes(self): + return ['custom_attributes'] diff --git a/intercom/service/contact.py b/intercom/service/contact.py new file mode 100644 index 00000000..45618c68 --- /dev/null +++ b/intercom/service/contact.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- + +from intercom import contact +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 +from intercom.api_operations.save import Save +from intercom.api_operations.load import Load +from intercom.api_operations.scroll import Scroll +from intercom.extended_api_operations.tags import Tags +from intercom.service.base_service import BaseService + + +class Contact(BaseService, All, Find, FindAll, Delete, Save, Load, Submit, Tags, Scroll): + + @property + def collection_class(self): + return contact.Contact diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py index 8db6f1aa..a98fa4cd 100644 --- a/tests/integration/__init__.py +++ b/tests/integration/__init__.py @@ -12,6 +12,20 @@ def get_timestamp(): now = datetime.utcnow() return int(time.mktime(now.timetuple())) +def get_or_create_contact(client, timestamp): + # get user + email = '%s@example.com' % (timestamp) + try: + user = client.contacts.find(email=email) + except ResourceNotFound: + # Create a user + contact = client.contacts.create( + email=email, + user_id=timestamp, + name="Ada %s" % (timestamp)) + time.sleep(5) + return contact + def get_or_create_user(client, timestamp): # get user diff --git a/tests/integration/test_contacts.py b/tests/integration/test_contacts.py new file mode 100644 index 00000000..6736ee35 --- /dev/null +++ b/tests/integration/test_contacts.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- + +import os +import unittest +from intercom.client import Client +from . import get_timestamp +from . import get_or_create_contact + +intercom = Client( + os.environ.get('INTERCOM_PERSONAL_ACCESS_TOKEN')) + + +class ContactsTest(unittest.TestCase): + + @classmethod + def setup_class(cls): + nowstamp = get_timestamp() + cls.user = get_or_create_contact(intercom, nowstamp) + cls.email = cls.user.email + + @classmethod + def teardown_class(cls): + delete_user(intercom, cls.user) + + def test_find_by_email(self): + # Find user by email + contact = intercom.contacts.find(email=self.email) + self.assertEqual(self.email, contact.email) diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py index a85c92f6..2aac46a8 100644 --- a/tests/unit/__init__.py +++ b/tests/unit/__init__.py @@ -136,6 +136,128 @@ def get_user(email="bob@example.com", name="Joe Schmoe"): } } +def get_contact(email="bob@example.com", name="Joe Schmoe"): + return { + "type": "contact", + "id": "id-from-customers-app", + "workspace_id": "ecahpwf5", + "external_id": "the-app-id", + "role": "user", + "email": email, + "phone": "+1123456789", + "name": name, + "avatar": "https://example.org/128Wash.jpg", + "owner_id": 127, + "social_profiles": { + "type": "list", + "data": [ + { + "type": "social_profile", + "name": "Twitter", + "url": "http://twitter.com/th1sland" + } + ] + }, + "has_hard_bounced": False, + "marked_email_as_spam": False, + "unsubscribed_from_emails": False, + "created_at": 1571672154, + "updated_at": 1571672158, + "signed_up_at": 1571069751, + "last_seen_at": 1571069751, + "last_replied_at": 1571672158, + "last_contacted_at": 1571672158, + "last_email_opened_at": 1571673478, + "last_email_clicked_at": 1571676789, + "language_override": None, + "browser": "chrome", + "browser_version": "77.0.3865.90", + "browser_language": "en", + "os": "OS X 10.14.6", + "location": { + "type": "location", + "country": "Ireland", + "region": "Dublin", + "city": "Dublin" + }, + "android_app_name": None, + "android_app_version": None, + "android_device": None, + "android_os_version": None, + "android_sdk_version": None, + "android_last_seen_at": None, + "ios_app_name": None, + "ios_app_version": None, + "ios_device": None, + "ios_os_version": None, + "ios_sdk_version": None, + "ios_last_seen_at": None, + "custom_attributes": { + "paid_subscriber": True, + "monthly_spend": 155.5, + "team_mates": 1 + }, + "tags": { + "type": "list", + "data": [ + { + "type": "tag", + "id": "2", + "url": "/tags/2" + }, + { + "type": "tag", + "id": "4", + "url": "/tags/4" + }, + { + "type": "tag", + "id": "5", + "url": "/tags/5" + } + ], + "url": "/contacts/5ba682d23d7cf92bef87bfd4/tags", + "total_count": 3, + "has_more": False + }, + "notes": { + "type": "list", + "data": [ + { + "type": "note", + "id": "20114858", + "url": "/notes/20114858" + } + ], + "url": "/contacts/5ba682d23d7cf92bef87bfd4/notes", + "total_count": 1, + "has_more": False + }, + "companies": { + "type": "list", + "data": [ + { + "type": "company", + "id": "5ba686093d7cf93552a3dc99", + "url": "/companies/5ba686093d7cf93552a3dc99" + + }, + { + "type": "company", + "id": "5cee64a03d7cf90c51b36f19", + "url": "/companies/5cee64a03d7cf90c51b36f19" + }, + { + "type": "company", + "id": "5d7668883d7cf944dbc5c791", + "url": "/companies/5d7668883d7cf944dbc5c791" + } + ], + "url": "/contacts/5ba682d23d7cf92bef87bfd4/companies", + "total_count": 3, + "has_more": False + } + } def get_company(name): return { @@ -160,7 +282,6 @@ def get_company(name): } } - def get_event(name="the-event-name"): return { "type": "event", @@ -195,6 +316,26 @@ def page_of_users(include_next_link=False): page["pages"]["next"] = "https://api.intercom.io/users?per_page=50&page=2" return page +def page_of_contacts(include_next_link=False): + page = { + "type": "contacts.list", + "pages": { + "type": "pages", + "page": 1, + "next": None, + "per_page": 50, + "total_pages": 7 + }, + "users": [ + get_contact("user1@example.com"), + get_contact("user2@example.com"), + get_contact("user3@example.com")], + "total_count": 314 + } + if include_next_link: + page["pages"]["next"] = "https://api.intercom.io/contacts?per_page=50&page=2" + return page + def users_scroll(include_users=False): # noqa # a "page" of results from the Scroll API diff --git a/tests/unit/test_contacts.py b/tests/unit/test_contacts.py new file mode 100644 index 00000000..c613fa04 --- /dev/null +++ b/tests/unit/test_contacts.py @@ -0,0 +1,505 @@ +# -*- coding: utf-8 -*- + +import calendar +import json +import mock +import time +import unittest + +from datetime import datetime +from intercom.collection_proxy import CollectionProxy +from intercom.lib.flat_store import FlatStore +from intercom.client import Client +from intercom.contacts import Contact +from intercom import MultipleMatchingUsersError +from intercom.utils import define_lightweight_class +from mock import patch +from nose.tools import assert_raises +from nose.tools import eq_ +from nose.tools import ok_ +from nose.tools import istest +from tests.unit import get_contact +from tests.unit import mock_response +from tests.unit import page_of_contacts + + +class ContactTest(unittest.TestCase): + + def setUp(self): + self.client = Client() + + @istest + def it_to_dict_itself(self): + created_at = datetime.utcnow() + contact = Contact( + email="jim@example.com", id="12345", + created_at=created_at, name="Jim Bob") + as_dict = contact.to_dict() + eq_(as_dict["email"], "jim@example.com") + eq_(as_dict["id"], "12345") + eq_(as_dict["created_at"], calendar.timegm(created_at.utctimetuple())) + eq_(as_dict["name"], "Jim Bob") + + @istest + def it_presents_created_at_and_last_impression_at_as_datetime(self): + now = datetime.utcnow() + now_ts = calendar.timegm(now.utctimetuple()) + contact = Contact.from_api( + {'created_at': now_ts, 'last_impression_at': now_ts}) + self.assertIsInstance(contact.created_at, datetime) + eq_(now.strftime('%c'), contact.created_at.strftime('%c')) + self.assertIsInstance(contact.last_impression_at, datetime) + eq_(now.strftime('%c'), contact.last_impression_at.strftime('%c')) + + @istest + def it_throws_an_attribute_error_on_trying_to_access_an_attribute_that_has_not_been_set(self): # noqa + with assert_raises(AttributeError): + contact = Contact() + contact.foo_property + + @istest + def it_presents_a_complete_contact_record_correctly(self): + contact = Contact.from_api(get_contact()) + eq_('id-from-customers-app', contact.id) + eq_('bob@example.com', contact.email) + eq_('Joe Schmoe', contact.name) + eq_('the-app-id', contact.external_id) + eq_(1571672154, calendar.timegm(contact.created_at.utctimetuple())) + eq_(1571672158, calendar.timegm(contact.updated_at.utctimetuple())) + +# @istest +# def it_allows_update_last_request_at(self): +# payload = { +# 'id': '1224242', +# 'update_last_request_at': True, +# 'custom_attributes': {} +# } +# with patch.object(Client, 'post', return_value=payload) as mock_method: +# self.client.contacts.create( +# id='1224242', update_last_request_at=True) +# mock_method.assert_called_once_with( +# '/contacts/', +# {'update_last_request_at': True, 'id': '1224242'}) + +# @istest +# def it_allows_easy_setting_of_custom_data(self): +# now = datetime.utcnow() +# now_ts = calendar.timegm(now.utctimetuple()) + +# contact = Contact() +# contact.custom_attributes["mad"] = 123 +# contact.custom_attributes["other"] = now_ts +# contact.custom_attributes["thing"] = "yay" +# attrs = {"mad": 123, "other": now_ts, "thing": "yay"} +# eq_(contact.to_dict()["custom_attributes"], attrs) + +# @istest +# def it_allows_easy_setting_of_multiple_companies(self): +# contact = Contact() +# companies = [ +# {"name": "Intercom", "company_id": "6"}, +# {"name": "Test", "company_id": "9"}, +# ] +# contact.companies = companies +# eq_(contact.to_dict()["companies"], companies) + +# @istest +# def it_rejects_nested_data_structures_in_custom_attributes(self): +# contact = Contact() +# with assert_raises(ValueError): +# contact.custom_attributes["thing"] = [1] + +# with assert_raises(ValueError): +# contact.custom_attributes["thing"] = {1: 2} + +# with assert_raises(ValueError): +# contact.custom_attributes = {1: {2: 3}} + +# contact = Contact.from_api(get_contact()) +# with assert_raises(ValueError): +# contact.custom_attributes["thing"] = [1] + +# @istest +# def it_fetches_a_contact(self): +# with patch.object(Client, 'get', return_value=get_contact()) as mock_method: # noqa +# contact = self.client.contacts.find(email='somebody@example.com') +# eq_(contact.email, 'bob@example.com') +# eq_(contact.name, 'Joe Schmoe') +# mock_method.assert_called_once_with( +# '/contacts', {'email': 'somebody@example.com'}) # noqa + +# @istest +# def it_gets_contacts_by_tag(self): +# with patch.object(Client, 'get', return_value=page_of_contacts(False)): +# contacts = self.client.contacts.by_tag(124) +# for contact in contacts: +# ok_(hasattr(contact, 'avatar')) + +# @istest +# def it_saves_a_contact_always_sends_custom_attributes(self): + +# body = { +# 'email': 'jo@example.com', +# 'id': 'i-1224242', +# 'custom_attributes': {} +# } + +# with patch.object(Client, 'post', return_value=body) as mock_method: +# contact = Contact(email="jo@example.com", id="i-1224242") +# self.client.contacts.save(contact) +# eq_(contact.email, 'jo@example.com') +# eq_(contact.custom_attributes, {}) +# mock_method.assert_called_once_with( +# '/contacts', +# {'email': "jo@example.com", 'id': "i-1224242", +# 'custom_attributes': {}}) + +# @istest +# def it_saves_a_contact_with_a_company(self): +# body = { +# 'email': 'jo@example.com', +# 'id': 'i-1224242', +# 'companies': [{ +# 'company_id': 6, +# 'name': 'Intercom' +# }] +# } +# with patch.object(Client, 'post', return_value=body) as mock_method: +# contact = Contact( +# email="jo@example.com", id="i-1224242", +# company={'company_id': 6, 'name': 'Intercom'}) +# self.client.contacts.save(contact) +# eq_(contact.email, 'jo@example.com') +# eq_(len(contact.companies), 1) +# mock_method.assert_called_once_with( +# '/contacts', +# {'email': "jo@example.com", 'id': "i-1224242", +# 'company': {'company_id': 6, 'name': 'Intercom'}, +# 'custom_attributes': {}}) + +# @istest +# def it_saves_a_contact_with_companies(self): +# body = { +# 'email': 'jo@example.com', +# 'id': 'i-1224242', +# 'companies': [{ +# 'company_id': 6, +# 'name': 'Intercom' +# }] +# } +# with patch.object(Client, 'post', return_value=body) as mock_method: +# contact = Contact( +# email="jo@example.com", id="i-1224242", +# companies=[{'company_id': 6, 'name': 'Intercom'}]) +# self.client.contacts.save(contact) +# eq_(contact.email, 'jo@example.com') +# eq_(len(contact.companies), 1) +# mock_method.assert_called_once_with( +# '/contacts', +# {'email': "jo@example.com", 'id': "i-1224242", +# 'companies': [{'company_id': 6, 'name': 'Intercom'}], +# 'custom_attributes': {}}) + +# @istest +# def it_can_save_a_contact_with_a_none_email(self): +# contact = Contact( +# email=None, id="i-1224242", +# companies=[{'company_id': 6, 'name': 'Intercom'}]) +# body = { +# 'custom_attributes': {}, +# 'email': None, +# 'id': 'i-1224242', +# 'companies': [{ +# 'company_id': 6, +# 'name': 'Intercom' +# }] +# } +# with patch.object(Client, 'post', return_value=body) as mock_method: +# self.client.contacts.save(contact) +# ok_(contact.email is None) +# eq_(contact.id, 'i-1224242') +# mock_method.assert_called_once_with( +# '/contacts', +# {'email': None, 'id': "i-1224242", +# 'companies': [{'company_id': 6, 'name': 'Intercom'}], +# 'custom_attributes': {}}) + +# @istest +# def it_deletes_a_contact(self): +# contact = Contact(id="1") +# with patch.object(Client, 'delete', return_value={}) as mock_method: +# contact = self.client.contacts.delete(contact) +# eq_(contact.id, "1") +# mock_method.assert_called_once_with('/contacts/1', {}) + +# @istest +# def it_can_use_contact_create_for_convenience(self): +# payload = { +# 'email': 'jo@example.com', +# 'id': 'i-1224242', +# 'custom_attributes': {} +# } +# with patch.object(Client, 'post', return_value=payload) as mock_method: # noqa +# contact = self.client.contacts.create(email="jo@example.com", id="i-1224242") # noqa +# eq_(payload, contact.to_dict()) +# mock_method.assert_called_once_with( +# '/contacts/', {'email': "jo@example.com", 'id': "i-1224242"}) # noqa + +# @istest +# def it_updates_the_contact_with_attributes_set_by_the_server(self): +# payload = { +# 'email': 'jo@example.com', +# 'id': 'i-1224242', +# 'custom_attributes': {}, +# 'session_count': 4 +# } +# with patch.object(Client, 'post', return_value=payload) as mock_method: # noqa +# contact = self.client.contacts.create(email="jo@example.com", id="i-1224242") # noqa +# eq_(payload, contact.to_dict()) +# mock_method.assert_called_once_with( +# '/contacts/', +# {'email': "jo@example.com", 'id': "i-1224242"}) # noqa + +# @istest +# def it_allows_setting_dates_to_none_without_converting_them_to_0(self): +# payload = { +# 'email': 'jo@example.com', +# 'custom_attributes': {}, +# 'remote_created_at': None +# } +# with patch.object(Client, 'post', return_value=payload) as mock_method: +# contact = self.client.contacts.create(email="jo@example.com", remote_created_at=None) # noqa +# ok_(contact.remote_created_at is None) +# mock_method.assert_called_once_with('/contacts/', {'email': "jo@example.com", 'remote_created_at': None}) # noqa + +# @istest +# def it_gets_sets_rw_keys(self): +# created_at = datetime.utcnow() +# payload = { +# 'email': 'me@example.com', +# 'id': 'abc123', +# 'name': 'Bob Smith', +# 'last_seen_ip': '1.2.3.4', +# 'last_seen_contact_agent': 'ie6', +# 'created_at': calendar.timegm(created_at.utctimetuple()) +# } +# contact = Contact(**payload) +# expected_keys = ['custom_attributes'] +# expected_keys.extend(list(payload.keys())) +# eq_(sorted(expected_keys), sorted(contact.to_dict().keys())) +# for key in list(payload.keys()): +# eq_(payload[key], contact.to_dict()[key]) + +# @istest +# def it_will_allow_extra_attributes_in_response_from_api(self): +# contact = Contact.from_api({'new_param': 'some value'}) +# eq_('some value', contact.new_param) + +# @istest +# def it_returns_a_collectionproxy_for_all_without_making_any_requests(self): +# with mock.patch('intercom.request.Request.send_request_to_path', new_callable=mock.NonCallableMock): # noqa +# res = self.client.contacts.all() +# self.assertIsInstance(res, CollectionProxy) + +# @istest +# def it_raises_a_multiple_matching_contacts_error_when_receiving_a_conflict(self): # noqa +# payload = { +# 'type': 'error.list', +# 'errors': [ +# { +# 'code': 'conflict', +# 'message': 'Multiple existing contacts match this email address - must be more specific using id' # noqa +# } +# ] +# } +# # create bytes content +# content = json.dumps(payload).encode('utf-8') +# # create mock response +# resp = mock_response(content) +# with patch('requests.sessions.Session.request') as mock_method: +# mock_method.return_value = resp +# with assert_raises(MultipleMatchingContactsError): +# self.client.get('/contacts', {}) + +# @istest +# def it_handles_accented_characters(self): +# # create a contact dict with a name that contains accented characters +# payload = get_contact(name='Jóe Schmö') +# # create bytes content +# content = json.dumps(payload).encode('utf-8') +# # create mock response +# resp = mock_response(content) +# with patch('requests.sessions.Session.request') as mock_method: +# mock_method.return_value = resp +# contact = self.client.contacts.find(email='bob@example.com') +# try: +# # Python 2 +# eq_(unicode('Jóe Schmö', 'utf-8'), contact.name) +# except NameError: +# # Python 3 +# eq_('Jóe Schmö', contact.name) + + +# class DescribeIncrementingCustomAttributeFields(unittest.TestCase): + +# def setUp(self): # noqa +# self.client = Client() + +# created_at = datetime.utcnow() +# params = { +# 'email': 'jo@example.com', +# 'id': 'i-1224242', +# 'custom_attributes': { +# 'mad': 123, +# 'another': 432, +# 'other': time.mktime(created_at.timetuple()), +# 'thing': 'yay', +# 'logins': None, +# } +# } +# self.contact = Contact(**params) + +# @istest +# def it_increments_up_by_1_with_no_args(self): +# self.contact.increment('mad') +# eq_(self.contact.to_dict()['custom_attributes']['mad'], 124) + +# @istest +# def it_increments_up_by_given_value(self): +# self.contact.increment('mad', 4) +# eq_(self.contact.to_dict()['custom_attributes']['mad'], 127) + +# @istest +# def it_increments_down_by_given_value(self): +# self.contact.increment('mad', -1) +# eq_(self.contact.to_dict()['custom_attributes']['mad'], 122) + +# @istest +# def it_can_increment_new_custom_data_fields(self): +# self.contact.increment('new_field', 3) +# eq_(self.contact.to_dict()['custom_attributes']['new_field'], 3) + +# @istest +# def it_can_increment_none_values(self): +# self.contact.increment('logins') +# eq_(self.contact.to_dict()['custom_attributes']['logins'], 1) + +# @istest +# def it_can_call_increment_on_the_same_key_twice_and_increment_by_2(self): # noqa +# self.contact.increment('mad') +# self.contact.increment('mad') +# eq_(self.contact.to_dict()['custom_attributes']['mad'], 125) + +# @istest +# def it_can_save_after_increment(self): # noqa +# contact = Contact( +# email=None, id="i-1224242", +# companies=[{'company_id': 6, 'name': 'Intercom'}]) +# body = { +# 'custom_attributes': {}, +# 'email': "", +# 'id': 'i-1224242', +# 'companies': [{ +# 'company_id': 6, +# 'name': 'Intercom' +# }] +# } +# with patch.object(Client, 'post', return_value=body) as mock_method: # noqa +# contact.increment('mad') +# eq_(contact.to_dict()['custom_attributes']['mad'], 1) +# self.client.contacts.save(contact) + + +# 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": "contact", +# "data": { +# "id": 25, +# "email": "alice@example.com" +# } +# }, +# { +# "method": "delete", +# "data_type": "contact", +# "data": { +# "id": 26, +# "email": "bob@example.com" +# } +# } +# ] +# } + +# self.contacts_to_create = [ +# { +# "id": 25, +# "email": "alice@example.com" +# } +# ] + +# self.contacts_to_delete = [ +# { +# "id": 26, +# "email": "bob@example.com" +# } +# ] + +# created_at = datetime.utcnow() +# params = { +# 'email': 'jo@example.com', +# 'id': 'i-1224242', +# 'custom_attributes': { +# 'mad': 123, +# 'another': 432, +# 'other': time.mktime(created_at.timetuple()), +# 'thing': 'yay' +# } +# } +# self.contact = Contact(**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.contacts.submit_bulk_job( +# create_items=self.contacts_to_create, delete_items=self.contacts_to_delete) +# mock_method.assert_called_once_with('/bulk/contacts', self.bulk_request) + +# @istest +# def it_adds_contacts_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.contacts.submit_bulk_job( +# create_items=self.contacts_to_create, delete_items=self.contacts_to_delete, +# job_id='super_awesome_job') +# mock_method.assert_called_once_with('/bulk/contacts', self.bulk_request) From a715628e012a790bbdbfb5f80e0e0cef4271b74a Mon Sep 17 00:00:00 2001 From: umarovt Date: Fri, 10 Apr 2020 18:28:07 +0200 Subject: [PATCH 2/9] Finish cursor based pagination fix --- intercom/collection_proxy.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/intercom/collection_proxy.py b/intercom/collection_proxy.py index 8378ff70..643bbb1c 100644 --- a/intercom/collection_proxy.py +++ b/intercom/collection_proxy.py @@ -102,10 +102,14 @@ def paging_info_present(self, response): def extract_next_link(self, url, response): if self.paging_info_present(response): paging_info = response["pages"] - if paging_info["next"] and isinstance(paging_info['next'], str) : + if 'next' not in paging_info: + return None + elif paging_info["next"] and isinstance(paging_info['next'], unicode): next_parsed = six.moves.urllib.parse.urlparse(paging_info["next"]) return '{}?{}'.format(next_parsed.path, next_parsed.query) else: #cursor based pagination - return '{}?{}'.format(six.moves.urllib.parse.urlparse(url).path, paging_info['next']['starting_after']) + print paging_info + return '{}?starting_after={}'.format(six.moves.urllib.parse.urlparse(url).path, paging_info['next']['starting_after']) + From ee177273d7aff85e9e85e0e558330fecc8042161 Mon Sep 17 00:00:00 2001 From: umarovt Date: Sat, 11 Apr 2020 09:48:34 +0200 Subject: [PATCH 3/9] Implememnt search over contacts --- intercom/api_operations/search.py | 20 ++++++++++++++++++++ intercom/service/contact.py | 4 ++-- 2 files changed, 22 insertions(+), 2 deletions(-) create mode 100644 intercom/api_operations/search.py diff --git a/intercom/api_operations/search.py b/intercom/api_operations/search.py new file mode 100644 index 00000000..dd43bbcf --- /dev/null +++ b/intercom/api_operations/search.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +"""Operation to search through contacts.""" + +from intercom import utils + + +class Search(object): + """A mixin that provides `search` functionality.""" + + def search(self, query, **params): + """Find all instances of the resource based on the supplied parameters.""" + collection_name = utils.resource_class_to_collection_name( + self.collection_class) + finder_url = "/{}/scroll".format(collection_name) + + response = self.client.post("/{}/search".format(collection_name), query) + + collection_data = response['data'] + + return map(lambda item: self.collection_class(**item), collection_data) \ No newline at end of file diff --git a/intercom/service/contact.py b/intercom/service/contact.py index 45618c68..d2f5d36e 100644 --- a/intercom/service/contact.py +++ b/intercom/service/contact.py @@ -5,15 +5,15 @@ 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.search import Search from intercom.api_operations.delete import Delete from intercom.api_operations.save import Save from intercom.api_operations.load import Load -from intercom.api_operations.scroll import Scroll from intercom.extended_api_operations.tags import Tags from intercom.service.base_service import BaseService -class Contact(BaseService, All, Find, FindAll, Delete, Save, Load, Submit, Tags, Scroll): +class Contact(BaseService, All, Find, FindAll, Delete, Save, Load, Submit, Tags, Search): @property def collection_class(self): From 3ed3f3c2e75114d30648c8c5bb1ab3b53f86e9a8 Mon Sep 17 00:00:00 2001 From: umarovt Date: Sat, 11 Apr 2020 10:38:44 +0200 Subject: [PATCH 4/9] Fix issue with passing params to collection proxy --- intercom/collection_proxy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/intercom/collection_proxy.py b/intercom/collection_proxy.py index 643bbb1c..aa7db30c 100644 --- a/intercom/collection_proxy.py +++ b/intercom/collection_proxy.py @@ -68,7 +68,7 @@ def get_first_page(self): def get_next_page(self): # get the next page of results - return self.get_page(self.next_page) + return self.get_page(self.next_page, self.finder_params) def get_page(self, url, params={}): # get a page of results From b5dce26d3f63f310305171ee7ee8972e6463944d Mon Sep 17 00:00:00 2001 From: umarovt Date: Sat, 11 Apr 2020 11:41:58 +0200 Subject: [PATCH 5/9] Fix issue with end statement for old pagination --- intercom/collection_proxy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/intercom/collection_proxy.py b/intercom/collection_proxy.py index aa7db30c..68db47de 100644 --- a/intercom/collection_proxy.py +++ b/intercom/collection_proxy.py @@ -102,7 +102,7 @@ def paging_info_present(self, response): def extract_next_link(self, url, response): if self.paging_info_present(response): paging_info = response["pages"] - if 'next' not in paging_info: + if 'next' not in paging_info or paging_info['next'] == None: return None elif paging_info["next"] and isinstance(paging_info['next'], unicode): next_parsed = six.moves.urllib.parse.urlparse(paging_info["next"]) From 48763496310b05ed359b85b6ae13cee7e4e1d760 Mon Sep 17 00:00:00 2001 From: umarovt Date: Sat, 11 Apr 2020 12:20:17 +0200 Subject: [PATCH 6/9] Fix continue iterating test --- intercom/collection_proxy.py | 2 +- tests/unit/test_contacts.py | 2 +- tests/unit/test_event.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/intercom/collection_proxy.py b/intercom/collection_proxy.py index 68db47de..3b675fbc 100644 --- a/intercom/collection_proxy.py +++ b/intercom/collection_proxy.py @@ -104,7 +104,7 @@ def extract_next_link(self, url, response): paging_info = response["pages"] if 'next' not in paging_info or paging_info['next'] == None: return None - elif paging_info["next"] and isinstance(paging_info['next'], unicode): + elif paging_info["next"] and (isinstance(paging_info['next'], unicode) or isinstance(paging_info['next'], str)): next_parsed = six.moves.urllib.parse.urlparse(paging_info["next"]) return '{}?{}'.format(next_parsed.path, next_parsed.query) else: diff --git a/tests/unit/test_contacts.py b/tests/unit/test_contacts.py index c613fa04..c5ceb9db 100644 --- a/tests/unit/test_contacts.py +++ b/tests/unit/test_contacts.py @@ -10,7 +10,7 @@ from intercom.collection_proxy import CollectionProxy from intercom.lib.flat_store import FlatStore from intercom.client import Client -from intercom.contacts import Contact +from intercom.contact import Contact from intercom import MultipleMatchingUsersError from intercom.utils import define_lightweight_class from mock import patch diff --git a/tests/unit/test_event.py b/tests/unit/test_event.py index 83fb4e3e..b0bcc0f1 100644 --- a/tests/unit/test_event.py +++ b/tests/unit/test_event.py @@ -44,7 +44,7 @@ def it_keeps_iterating_if_next_link(self): event_names = [event.event_name for event in self.client.events.find_all( type='user', email='joe@example.com')] eq_([call('/events', {'type': 'user', 'email': 'joe@example.com'}), - call('/events?type=user&intercom_user_id=55a3b&before=144474756550', {})], # noqa + call('/events?type=user&intercom_user_id=55a3b&before=144474756550', {'type': 'user', 'email': 'joe@example.com'})], # noqa mock_method.mock_calls) eq_(event_names, ['invited-friend', 'bought-sub'] * 2) # noqa From cfe72949ebad5a5416cf37c25280c6b6ad7d3e7c Mon Sep 17 00:00:00 2001 From: umarovt Date: Sat, 11 Apr 2020 12:23:40 +0200 Subject: [PATCH 7/9] Remove console log --- intercom/collection_proxy.py | 1 - 1 file changed, 1 deletion(-) diff --git a/intercom/collection_proxy.py b/intercom/collection_proxy.py index 3b675fbc..47d6685e 100644 --- a/intercom/collection_proxy.py +++ b/intercom/collection_proxy.py @@ -109,7 +109,6 @@ def extract_next_link(self, url, response): return '{}?{}'.format(next_parsed.path, next_parsed.query) else: #cursor based pagination - print paging_info return '{}?starting_after={}'.format(six.moves.urllib.parse.urlparse(url).path, paging_info['next']['starting_after']) From ccd57c4c0751012bfc6da60d32a46fb311569c10 Mon Sep 17 00:00:00 2001 From: umarovt Date: Sat, 11 Apr 2020 12:28:25 +0200 Subject: [PATCH 8/9] Remove console log --- intercom/collection_proxy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/intercom/collection_proxy.py b/intercom/collection_proxy.py index 47d6685e..999b6934 100644 --- a/intercom/collection_proxy.py +++ b/intercom/collection_proxy.py @@ -104,7 +104,7 @@ def extract_next_link(self, url, response): paging_info = response["pages"] if 'next' not in paging_info or paging_info['next'] == None: return None - elif paging_info["next"] and (isinstance(paging_info['next'], unicode) or isinstance(paging_info['next'], str)): + elif paging_info["next"] and (isinstance(paging_info['next'], six.text_type) or isinstance(paging_info['next'], str)): next_parsed = six.moves.urllib.parse.urlparse(paging_info["next"]) return '{}?{}'.format(next_parsed.path, next_parsed.query) else: From 9b9875a8a9fbf6ff730e28bcc0837a8dd1184bfb Mon Sep 17 00:00:00 2001 From: umarovt Date: Sat, 11 Apr 2020 12:32:34 +0200 Subject: [PATCH 9/9] Drop python 3.4 because of yaml --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 1ab1f86d..fe0d3b7b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,7 +2,6 @@ sudo: false language: python python: - 2.7 - - 3.4 - 3.5 - 3.6 install: