From c083f0d6564defd95e38373b4939bbf239ab7ad1 Mon Sep 17 00:00:00 2001 From: Yitao Xiong Date: Tue, 8 Sep 2015 18:05:46 -0400 Subject: [PATCH 01/25] Added contacts.py file --- hapi/contacts.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 hapi/contacts.py diff --git a/hapi/contacts.py b/hapi/contacts.py new file mode 100644 index 0000000..e69de29 From 90d3c4908cf7e04dbe2e653dfcccafa9edaba4a5 Mon Sep 17 00:00:00 2001 From: Yitao Xiong Date: Tue, 8 Sep 2015 18:07:27 -0400 Subject: [PATCH 02/25] updated the version number --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index b28e2f0..92a6720 100755 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ setup( name='hapipy', - version='2.10.4', + version='2.10.5', description="A python wrapper around HubSpot's APIs", long_description=open('README.md').read(), author='HubSpot Dev Team', From dffaffdebf21d15fe646e5095f18170452c357b4 Mon Sep 17 00:00:00 2001 From: Yitao Xiong Date: Tue, 8 Sep 2015 18:24:54 -0400 Subject: [PATCH 03/25] Added Contacts client and Contact Lists client --- .gitignore | 1 + hapi/contact_lists.py | 27 +++++++++++++++++++++++++++ hapi/contacts.py | 24 ++++++++++++++++++++++++ 3 files changed, 52 insertions(+) create mode 100644 hapi/contact_lists.py diff --git a/.gitignore b/.gitignore index 9414a9d..08b1c2f 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ dist /build MANIFEST +.idea diff --git a/hapi/contact_lists.py b/hapi/contact_lists.py new file mode 100644 index 0000000..07565a9 --- /dev/null +++ b/hapi/contact_lists.py @@ -0,0 +1,27 @@ +from base import BaseClient +import logging_helper + + +CONTACT_LISTS_API_VERSION = '1' + + +class ContactListsClient(BaseClient): + """ + The hapipy Contact Lists client uses the _make_request method to call the API for data. + It returns a python object translated from the json return + """ + + def __init__(self, *args, **kwargs): + super(ContactListsClient, self).__init__(*args, **kwargs) + self.log = logging_helper.get_log('hapi.contact_lists') + + def _get_path(self, subpath): + return 'contacts/v%s/%s' % (self.options.get('version') or CONTACT_LISTS_API_VERSION, subpath) + + def get_contact_lists(self, **options): + return self._call('lists', method='GET', **options) + + def add_contact_to_a_list(self, list_id, data=None, **options): + data = data or {} + return self._call('lists/{list_id}/add'.format(list_id), + data=data, method='POST', **options) \ No newline at end of file diff --git a/hapi/contacts.py b/hapi/contacts.py index e69de29..0ea30ad 100644 --- a/hapi/contacts.py +++ b/hapi/contacts.py @@ -0,0 +1,24 @@ +from base import BaseClient +import logging_helper + + +CONTACTS_API_VERSION = '1' + + +class ContactsClient(BaseClient): + """ + The hapipy Contacts client uses the _make_request method to call the API for data. + It returns a python object translated from the json return + """ + + def __init__(self, *args, **kwargs): + super(ContactsClient, self).__init__(*args, **kwargs) + self.log = logging_helper.get_log('hapi.contacts') + + def _get_path(self, subpath): + return 'contacts/v%s/%s' % (self.options.get('version') or CONTACTS_API_VERSION, subpath) + + def create_or_update_a_contact(self, email, data=None, **options): + data = data or {} + return self._call('contact/createOrUpdate/email/{}'.format(email), + data=data, method='POST', **options) From f291ba185ce024de2547fa1e44e6a820faf91950 Mon Sep 17 00:00:00 2001 From: Yitao Xiong Date: Tue, 8 Sep 2015 20:07:13 -0400 Subject: [PATCH 04/25] Fixed a bug --- hapi/contact_lists.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hapi/contact_lists.py b/hapi/contact_lists.py index 07565a9..c25a9b9 100644 --- a/hapi/contact_lists.py +++ b/hapi/contact_lists.py @@ -23,5 +23,5 @@ def get_contact_lists(self, **options): def add_contact_to_a_list(self, list_id, data=None, **options): data = data or {} - return self._call('lists/{list_id}/add'.format(list_id), - data=data, method='POST', **options) \ No newline at end of file + return self._call('lists/{list_id}/add'.format(list_id=list_id), + data=data, method='POST', **options) From 28579906f9904767ae60d1aa4917583e4c0bf81f Mon Sep 17 00:00:00 2001 From: Yitao Xiong Date: Wed, 9 Sep 2015 10:17:20 -0400 Subject: [PATCH 05/25] Added two new needed methods --- hapi/contacts.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/hapi/contacts.py b/hapi/contacts.py index 0ea30ad..e87b3d1 100644 --- a/hapi/contacts.py +++ b/hapi/contacts.py @@ -20,5 +20,14 @@ def _get_path(self, subpath): def create_or_update_a_contact(self, email, data=None, **options): data = data or {} - return self._call('contact/createOrUpdate/email/{}'.format(email), + return self._call('contact/createOrUpdate/email/{email}'.format(email=email), + data=data, method='POST', **options) + + def get_contact_by_email(self, email, **options): + return self._call('contact/email/{email}/profile'.format(email=email), + method='GET', **options) + + def update_a_contact(self, contact_id, data=None, **options): + data = data or {} + return self._call('contact/vid/{contact_id}/profile'.format(contact_id=contact_id), data=data, method='POST', **options) From 40bcac6a4bd26d11fcd802e8a8dff1bfdd601183 Mon Sep 17 00:00:00 2001 From: Zac Date: Wed, 9 Sep 2015 18:43:28 -0400 Subject: [PATCH 06/25] Tests for contacts & contact lists, added some methods to the API that helped me clean up after the testing on the public api --- hapi/contact_lists.py | 19 +++++- hapi/contacts.py | 7 ++ hapi/test/test_contact_lists.py | 72 ++++++++++++++++++++ hapi/test/test_contacts.py | 117 ++++++++++++++++++++++++++++++++ requirements.pip | 1 + 5 files changed, 214 insertions(+), 2 deletions(-) create mode 100644 hapi/test/test_contact_lists.py create mode 100644 hapi/test/test_contacts.py diff --git a/hapi/contact_lists.py b/hapi/contact_lists.py index c25a9b9..f436292 100644 --- a/hapi/contact_lists.py +++ b/hapi/contact_lists.py @@ -18,10 +18,25 @@ def __init__(self, *args, **kwargs): def _get_path(self, subpath): return 'contacts/v%s/%s' % (self.options.get('version') or CONTACT_LISTS_API_VERSION, subpath) - def get_contact_lists(self, **options): + def get_a_contact_lists(self, **options): + """ Returns all of the contact lists """ return self._call('lists', method='GET', **options) - def add_contact_to_a_list(self, list_id, data=None, **options): + def add_contact_to_a_list(self, list_id, vids, data=None, **options): + """ Adds a list of contact vids to the specified list. """ data = data or {} + data['vids'] = vids return self._call('lists/{list_id}/add'.format(list_id=list_id), data=data, method='POST', **options) + + def create_a_contact_list(self, list_name, portal_id, dynamic=True, data=None, **options): + """ Creates a contact list with given list_name on the given portal_id. """ + data = data or {} + data['name'] = list_name + data['portal_id'] = portal_id + data['dynamic'] = dynamic + return self._call('lists', data=data, method='POST', **options) + + def delete_a_contact_list(self, list_id, **options): + """ Deletes the contact list by list_id. """ + return self._call('lists/{list_id}'.format(list_id=list_id), method='DELETE', **options) diff --git a/hapi/contacts.py b/hapi/contacts.py index e87b3d1..1bd1954 100644 --- a/hapi/contacts.py +++ b/hapi/contacts.py @@ -19,15 +19,22 @@ def _get_path(self, subpath): return 'contacts/v%s/%s' % (self.options.get('version') or CONTACTS_API_VERSION, subpath) def create_or_update_a_contact(self, email, data=None, **options): + """ Creates or Updates a client with the supplied data. """ data = data or {} return self._call('contact/createOrUpdate/email/{email}'.format(email=email), data=data, method='POST', **options) def get_contact_by_email(self, email, **options): + """ Gets contact specified by email address. """ return self._call('contact/email/{email}/profile'.format(email=email), method='GET', **options) def update_a_contact(self, contact_id, data=None, **options): + """ Updates the contact by contact_id with the given data. """ data = data or {} return self._call('contact/vid/{contact_id}/profile'.format(contact_id=contact_id), data=data, method='POST', **options) + + def delete_a_contact(self, contact_id, **options): + """ Deletes a contact by contact_id. """ + return self._call('contact/vid/{contact_id}'.format(contact_id=contact_id), method='DELETE', **options) diff --git a/hapi/test/test_contact_lists.py b/hapi/test/test_contact_lists.py new file mode 100644 index 0000000..6801ab6 --- /dev/null +++ b/hapi/test/test_contact_lists.py @@ -0,0 +1,72 @@ +import unittest2 +import random + +from nose.plugins.attrib import attr + +import helper +from hapi.contact_lists import ContactListsClient +from hapi.contacts import ContactsClient +from test_contacts import ContactsClientTestCase + + +class ConstactListsClientTestCase(unittest2.TestCase): + + """ Unit tests for the HubSpot Contact Lists API Python client. + + This file contains some unittest tests for the Contact Lists API. + + Questions, comments: http://developers.hubspot.com/docs/methods/lists/create_list + """ + + test_portal_id = 62515 + + def setUp(self): + self.client = ContactListsClient(**helper.get_options()) + self.contacts_client = ContactsClient(**helper.get_options()) + self.lists = [] + self.contacts =[] + + def tearDown(self): + """ Clean up all the created objects. """ + if self.contacts: + [self.contacts_client.delete_a_contact(contact) for contact in self.contacts] + if self.lists: + [self.client.delete_a_contact_list(list) for list in self.lists] + + @attr('api') + def test_get_a_contact_lists(self): + """ Test that the get contact lists endpoint is valid. """ + response = self.client.get_a_contact_lists() + self.assertTrue(len(response) > 0) + + @attr('api') + def test_add_contact_to_a_list(self): + """ Test that the add contact to a list endpoint is valid. """ + email = ContactsClientTestCase.test_contact_json['properties'][0]['value'] + contact = self.contacts_client.create_or_update_a_contact(email, data=ContactsClientTestCase.test_contact_json)['vid'] + self.contacts.append(contact) + contact_list = self.client.create_a_contact_list(list_name='test_add_contact_to_a_list' + str(random.randint(1000, 50000)), + portal_id=self.test_portal_id, + dynamic=False) + self.lists.append(contact_list['listId']) + + response = self.client.add_contact_to_a_list(contact_list['listId'], [contact]) + self.assertTrue(len(response) > 0) + + def test_create_a_contact_list(self): + """ Test that the create contact list endpoint is valid. """ + response = self.client.create_a_contact_list(list_name='test_create_a_contact_list' + str(random.randint(1000, 50000)), + portal_id=self.test_portal_id, + dynamic=False) + self.assertTrue(len(response) > 0) + + self.lists.append(response['listId']) + + def test_delete_a_contact_list(self): + """ Test that the delete contact list endpoint is valid. """ + contact_list = self.client.create_a_contact_list(list_name='test_delete_a_contact_list' + str(random.randint(1000, 50000)), + portal_id=self.test_portal_id, + dynamic=False) + + response = self.client.delete_a_contact_list(contact_list['listId']) + \ No newline at end of file diff --git a/hapi/test/test_contacts.py b/hapi/test/test_contacts.py new file mode 100644 index 0000000..bfd4a82 --- /dev/null +++ b/hapi/test/test_contacts.py @@ -0,0 +1,117 @@ +import unittest2 + +from faker import Faker +from nose.plugins.attrib import attr + +import helper +from hapi.contacts import ContactsClient + +fake = Faker() + +class ContactsClientTestCase(unittest2.TestCase): + + """ Unit tests for the HubSpot Contacts API Python client. + + This file contains some unittest tests for the Contacts API. + + Questions, comments: http://developers.hubspot.com/docs/methods/contacts/contacts-overview + """ + + test_contact_json = { + "properties": [ + { + "property": "email", + "value": fake.email() + }, + { + "property": "firstname", + "value": fake.first_name() + }, + { + "property": "lastname", + "value": fake.last_name() + }, + { + "property": "website", + "value": fake.url() + }, + { + "property": "company", + "value": fake.company() + }, + { + "property": "phone", + "value": fake.phone_number() + }, + { + "property": "address", + "value": fake.street_address() + }, + { + "property": "city", + "value": fake.city() + }, + { + "property": "state", + "value": fake.state() + }, + { + "property": "zip", + "value": fake.zipcode() + } + ] + } + + def setUp(self): + self.client = ContactsClient(**helper.get_options()) + self.contacts = [] + + def tearDown(self): + """ Cleans up the created objects. """ + if self.contacts: + [self.client.delete_a_contact(contact) for contact in self.contacts] + + @attr('api') + def test_create_or_update_a_contact(self): + """ Test the create or update a contact endpoint is valid. """ + email = self.test_contact_json['properties'][0]['value'] + + response = self.client.create_or_update_a_contact(email, data=self.test_contact_json) + self.assertTrue(len(response) > 0) + + self.contacts.append(response['vid']) + + @attr('api') + def test_get_contact_by_email(self): + """ Test that the get contact by email address endoint is valid. """ + email = self.test_contact_json['properties'][0]['value'] + contact = self.client.create_or_update_a_contact(email, data=self.test_contact_json)['vid'] + + response = self.client.get_contact_by_email(email) + self.assertTrue(len(response) > 0) + + self.contacts.append(contact) + + @attr('api') + def test_update_a_contact(self): + """ Test that the update contact endpoint is valid and that changes persist. """ + email = self.test_contact_json['properties'][0]['value'] + contact = self.client.create_or_update_a_contact(email, data=self.test_contact_json)['vid'] + new_contact_json = self.test_contact_json.copy() + new_contact_json['properties'][4]['value'] = new_contact_json['properties'][4]['value'] + ' UPDATED' + + response = self.client.update_a_contact(contact, data=self.test_contact_json) + contact_response = self.client.get_contact_by_email(email) + + self.assertEqual(contact_response['properties']['company']['value'], new_contact_json['properties'][4]['value']) + + self.contacts.append(contact) + + @attr('api') + def test_delete_a_contact(self): + """ Test that the delete contact endpoint is valid. """ + email = self.test_contact_json['properties'][0]['value'] + contact = self.client.create_or_update_a_contact(email, data=self.test_contact_json)['vid'] + + response = self.client.delete_a_contact(contact) + self.assertTrue(len(response) > 0) diff --git a/requirements.pip b/requirements.pip index 065097c..490130a 100644 --- a/requirements.pip +++ b/requirements.pip @@ -3,3 +3,4 @@ nose==1.1.2 unittest2==0.5.1 simplejson==2.2.1 +fake-factory==0.5.2 From 0d281b4510e961a0b150b4fa77b6b88a7b8c60a5 Mon Sep 17 00:00:00 2001 From: Zac Date: Fri, 11 Sep 2015 15:45:27 -0400 Subject: [PATCH 07/25] change nosetest requirement --- requirements.pip | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.pip b/requirements.pip index 490130a..25096ca 100644 --- a/requirements.pip +++ b/requirements.pip @@ -1,6 +1,6 @@ # tested on python 2.6 nose==1.1.2 -unittest2==0.5.1 +unittest2==1.1.0 simplejson==2.2.1 fake-factory==0.5.2 From 346f71d492eca949f56bade97a9ac19641ceeecb Mon Sep 17 00:00:00 2001 From: Yitao Xiong Date: Fri, 11 Sep 2015 16:33:22 -0400 Subject: [PATCH 08/25] change "get_a_contact_lists" to "get_contact_lists" --- hapi/contact_lists.py | 2 +- hapi/test/test_contact_lists.py | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/hapi/contact_lists.py b/hapi/contact_lists.py index f436292..4f9486f 100644 --- a/hapi/contact_lists.py +++ b/hapi/contact_lists.py @@ -18,7 +18,7 @@ def __init__(self, *args, **kwargs): def _get_path(self, subpath): return 'contacts/v%s/%s' % (self.options.get('version') or CONTACT_LISTS_API_VERSION, subpath) - def get_a_contact_lists(self, **options): + def get_contact_lists(self, **options): """ Returns all of the contact lists """ return self._call('lists', method='GET', **options) diff --git a/hapi/test/test_contact_lists.py b/hapi/test/test_contact_lists.py index 6801ab6..e77cf3e 100644 --- a/hapi/test/test_contact_lists.py +++ b/hapi/test/test_contact_lists.py @@ -10,7 +10,7 @@ class ConstactListsClientTestCase(unittest2.TestCase): - + """ Unit tests for the HubSpot Contact Lists API Python client. This file contains some unittest tests for the Contact Lists API. @@ -34,10 +34,10 @@ def tearDown(self): [self.client.delete_a_contact_list(list) for list in self.lists] @attr('api') - def test_get_a_contact_lists(self): - """ Test that the get contact lists endpoint is valid. """ - response = self.client.get_a_contact_lists() - self.assertTrue(len(response) > 0) + def test_get_contact_lists(self): + """ Test that the get contact lists endpoint is valid. """ + response = self.client.get_contact_lists() + self.assertTrue(len(response) > 0) @attr('api') def test_add_contact_to_a_list(self): @@ -69,4 +69,4 @@ def test_delete_a_contact_list(self): dynamic=False) response = self.client.delete_a_contact_list(contact_list['listId']) - \ No newline at end of file + From 3d7791c1c2cc9bcd2695f83bf11c80545fa2f546 Mon Sep 17 00:00:00 2001 From: Caroline Gallagher Date: Fri, 14 Oct 2016 11:25:43 -0400 Subject: [PATCH 09/25] Removed nose and unittest from package deps --- setup.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/setup.py b/setup.py index 92a6720..18b1f52 100755 --- a/setup.py +++ b/setup.py @@ -13,8 +13,6 @@ license='LICENSE.txt', packages=['hapi', 'hapi.mixins'], install_requires=[ - 'nose==1.1.2', - 'unittest2==0.5.1', 'simplejson>=2.1.2' ], ) From dd87909bfedc30062acb629d432d36a4a1c1f7d4 Mon Sep 17 00:00:00 2001 From: Caroline Gallagher Date: Wed, 19 Oct 2016 18:03:47 -0400 Subject: [PATCH 10/25] Update version to 2.10.5.1 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 18b1f52..c92e838 100755 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ setup( name='hapipy', - version='2.10.5', + version='2.10.5.1', description="A python wrapper around HubSpot's APIs", long_description=open('README.md').read(), author='HubSpot Dev Team', From 67d1925cadbfed95f9b67ad7f1be67713c24e949 Mon Sep 17 00:00:00 2001 From: philip Date: Wed, 1 Nov 2017 15:12:59 -0400 Subject: [PATCH 11/25] FIx vairaous issues with unicode email addresses --- hapi/contacts.py | 6 +++--- setup.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/hapi/contacts.py b/hapi/contacts.py index 1bd1954..9bc54f0 100644 --- a/hapi/contacts.py +++ b/hapi/contacts.py @@ -16,17 +16,17 @@ def __init__(self, *args, **kwargs): self.log = logging_helper.get_log('hapi.contacts') def _get_path(self, subpath): - return 'contacts/v%s/%s' % (self.options.get('version') or CONTACTS_API_VERSION, subpath) + return u'contacts/v%s/%s' % (self.options.get('version') or CONTACTS_API_VERSION, subpath) def create_or_update_a_contact(self, email, data=None, **options): """ Creates or Updates a client with the supplied data. """ data = data or {} - return self._call('contact/createOrUpdate/email/{email}'.format(email=email), + return self._call(u'contact/createOrUpdate/email/{email}'.format(email=email), data=data, method='POST', **options) def get_contact_by_email(self, email, **options): """ Gets contact specified by email address. """ - return self._call('contact/email/{email}/profile'.format(email=email), + return self._call(u'contact/email/{email}/profile'.format(email=email), method='GET', **options) def update_a_contact(self, contact_id, data=None, **options): diff --git a/setup.py b/setup.py index c92e838..4368f5f 100755 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ setup( name='hapipy', - version='2.10.5.1', + version='2.10.5.2', description="A python wrapper around HubSpot's APIs", long_description=open('README.md').read(), author='HubSpot Dev Team', From e7a6d311de8ba39832698e74318323d78e7c6fc7 Mon Sep 17 00:00:00 2001 From: philip Date: Wed, 15 Nov 2017 10:55:14 -0500 Subject: [PATCH 12/25] Really fix the unicode problem --- hapi/contacts.py | 12 ++++++++---- setup.py | 2 +- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/hapi/contacts.py b/hapi/contacts.py index 9bc54f0..0157230 100644 --- a/hapi/contacts.py +++ b/hapi/contacts.py @@ -1,5 +1,6 @@ from base import BaseClient import logging_helper +from urllib import quote CONTACTS_API_VERSION = '1' @@ -16,17 +17,19 @@ def __init__(self, *args, **kwargs): self.log = logging_helper.get_log('hapi.contacts') def _get_path(self, subpath): - return u'contacts/v%s/%s' % (self.options.get('version') or CONTACTS_API_VERSION, subpath) + return 'contacts/v%s/%s' % (self.options.get('version') or CONTACTS_API_VERSION, subpath) def create_or_update_a_contact(self, email, data=None, **options): """ Creates or Updates a client with the supplied data. """ data = data or {} - return self._call(u'contact/createOrUpdate/email/{email}'.format(email=email), + return self._call('contact/createOrUpdate/email/{email}'. + format(email=quote(email.encode('utf-8'))), data=data, method='POST', **options) def get_contact_by_email(self, email, **options): """ Gets contact specified by email address. """ - return self._call(u'contact/email/{email}/profile'.format(email=email), + return self._call('contact/email/{email}/profile'. + format(email=quote(email.encode('utf-8'))), method='GET', **options) def update_a_contact(self, contact_id, data=None, **options): @@ -37,4 +40,5 @@ def update_a_contact(self, contact_id, data=None, **options): def delete_a_contact(self, contact_id, **options): """ Deletes a contact by contact_id. """ - return self._call('contact/vid/{contact_id}'.format(contact_id=contact_id), method='DELETE', **options) + return self._call('contact/vid/{contact_id}'. + format(contact_id=contact_id), method='DELETE', **options) diff --git a/setup.py b/setup.py index 4368f5f..2c0d1e9 100755 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ setup( name='hapipy', - version='2.10.5.2', + version='2.10.5.3', description="A python wrapper around HubSpot's APIs", long_description=open('README.md').read(), author='HubSpot Dev Team', From 570d52b8e239c49228cbb7eca4654c8a7b767b1a Mon Sep 17 00:00:00 2001 From: Yitao Xiong Date: Fri, 16 Mar 2018 13:54:24 -0400 Subject: [PATCH 13/25] Added a get_contacts_by_list_id method in the client --- hapi/contact_lists.py | 9 ++++ hapi/test/test_contact_lists.py | 82 ++++++++++++++++++++------------- 2 files changed, 59 insertions(+), 32 deletions(-) diff --git a/hapi/contact_lists.py b/hapi/contact_lists.py index 4f9486f..6ce2ea4 100644 --- a/hapi/contact_lists.py +++ b/hapi/contact_lists.py @@ -22,6 +22,15 @@ def get_contact_lists(self, **options): """ Returns all of the contact lists """ return self._call('lists', method='GET', **options) + def get_contacts_by_list_id(self, list_id, query='', **options): + """ Get all contacts in the specified list """ + return self._call( + 'lists/{list_id}/contacts/all'.format(list_id=list_id), + method='GET', + query=query, + **options + ) + def add_contact_to_a_list(self, list_id, vids, data=None, **options): """ Adds a list of contact vids to the specified list. """ data = data or {} diff --git a/hapi/test/test_contact_lists.py b/hapi/test/test_contact_lists.py index e77cf3e..fa3cce6 100644 --- a/hapi/test/test_contact_lists.py +++ b/hapi/test/test_contact_lists.py @@ -9,7 +9,7 @@ from test_contacts import ContactsClientTestCase -class ConstactListsClientTestCase(unittest2.TestCase): +class ContactsListsClientTestCase(unittest2.TestCase): """ Unit tests for the HubSpot Contact Lists API Python client. @@ -21,17 +21,17 @@ class ConstactListsClientTestCase(unittest2.TestCase): test_portal_id = 62515 def setUp(self): - self.client = ContactListsClient(**helper.get_options()) - self.contacts_client = ContactsClient(**helper.get_options()) - self.lists = [] - self.contacts =[] + self.client = ContactListsClient(**helper.get_options()) + self.contacts_client = ContactsClient(**helper.get_options()) + self.lists = [] + self.contacts =[] def tearDown(self): - """ Clean up all the created objects. """ - if self.contacts: - [self.contacts_client.delete_a_contact(contact) for contact in self.contacts] - if self.lists: - [self.client.delete_a_contact_list(list) for list in self.lists] + """ Clean up all the created objects. """ + if self.contacts: + [self.contacts_client.delete_a_contact(contact) for contact in self.contacts] + if self.lists: + [self.client.delete_a_contact_list(list) for list in self.lists] @attr('api') def test_get_contact_lists(self): @@ -39,34 +39,52 @@ def test_get_contact_lists(self): response = self.client.get_contact_lists() self.assertTrue(len(response) > 0) + def test_get_contacts_by_list_id(self): + """ Test that get contacts in a list is returning the right contacts """ + email = ContactsClientTestCase.test_contact_json['properties'][0]['value'] + contact = self.contacts_client.create_or_update_a_contact( + email, + data=ContactsClientTestCase.test_contact_json + )['vid'] + self.contacts.append(contact) + contact_list = self.client.create_a_contact_list( + list_name='test_add_contact_to_a_list' + str(random.randint(1000, 50000)), + portal_id=self.test_portal_id, + dynamic=False) + self.lists.append(contact_list['listId']) + self.client.add_contact_to_a_list(contact_list['listId'], [contact]) + response = self.client.get_contacts_by_list_id(contact_list['listId']) + self.assertEqual(len(response['contacts']), 1) + self.assertEqual(response['contacts'][0]['vid'], contact) + @attr('api') def test_add_contact_to_a_list(self): - """ Test that the add contact to a list endpoint is valid. """ - email = ContactsClientTestCase.test_contact_json['properties'][0]['value'] - contact = self.contacts_client.create_or_update_a_contact(email, data=ContactsClientTestCase.test_contact_json)['vid'] - self.contacts.append(contact) - contact_list = self.client.create_a_contact_list(list_name='test_add_contact_to_a_list' + str(random.randint(1000, 50000)), - portal_id=self.test_portal_id, - dynamic=False) - self.lists.append(contact_list['listId']) - - response = self.client.add_contact_to_a_list(contact_list['listId'], [contact]) - self.assertTrue(len(response) > 0) + """ Test that the add contact to a list endpoint is valid. """ + email = ContactsClientTestCase.test_contact_json['properties'][0]['value'] + contact = self.contacts_client.create_or_update_a_contact(email, data=ContactsClientTestCase.test_contact_json)['vid'] + self.contacts.append(contact) + contact_list = self.client.create_a_contact_list(list_name='test_add_contact_to_a_list' + str(random.randint(1000, 50000)), + portal_id=self.test_portal_id, + dynamic=False) + self.lists.append(contact_list['listId']) + + response = self.client.add_contact_to_a_list(contact_list['listId'], [contact]) + self.assertTrue(len(response) > 0) def test_create_a_contact_list(self): - """ Test that the create contact list endpoint is valid. """ - response = self.client.create_a_contact_list(list_name='test_create_a_contact_list' + str(random.randint(1000, 50000)), - portal_id=self.test_portal_id, - dynamic=False) - self.assertTrue(len(response) > 0) + """ Test that the create contact list endpoint is valid. """ + response = self.client.create_a_contact_list(list_name='test_create_a_contact_list' + str(random.randint(1000, 50000)), + portal_id=self.test_portal_id, + dynamic=False) + self.assertTrue(len(response) > 0) - self.lists.append(response['listId']) + self.lists.append(response['listId']) def test_delete_a_contact_list(self): - """ Test that the delete contact list endpoint is valid. """ - contact_list = self.client.create_a_contact_list(list_name='test_delete_a_contact_list' + str(random.randint(1000, 50000)), - portal_id=self.test_portal_id, - dynamic=False) + """ Test that the delete contact list endpoint is valid. """ + contact_list = self.client.create_a_contact_list(list_name='test_delete_a_contact_list' + str(random.randint(1000, 50000)), + portal_id=self.test_portal_id, + dynamic=False) - response = self.client.delete_a_contact_list(contact_list['listId']) + response = self.client.delete_a_contact_list(contact_list['listId']) From e9ec9e80c748c19281d8080a9494494d551c59d4 Mon Sep 17 00:00:00 2001 From: Yitao Xiong Date: Fri, 16 Mar 2018 13:57:53 -0400 Subject: [PATCH 14/25] Bump the version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 2c0d1e9..d339576 100755 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ setup( name='hapipy', - version='2.10.5.3', + version='2.10.5.4', description="A python wrapper around HubSpot's APIs", long_description=open('README.md').read(), author='HubSpot Dev Team', From 0df6cb501a66eea823e7a0a711c21b6113425018 Mon Sep 17 00:00:00 2001 From: Ruichen_Wang Date: Wed, 4 Dec 2019 15:30:56 -0500 Subject: [PATCH 15/25] Delete currently unused legacy clients, make the active clients to be python2/3 compatible --- .gitignore | 1 + hapi/base.py | 38 ++++--- hapi/blog.py | 81 -------------- hapi/broadcast.py | 186 -------------------------------- hapi/contact_lists.py | 4 +- hapi/contacts.py | 6 +- hapi/error.py | 20 ++-- hapi/forms.py | 26 ----- hapi/keywords.py | 35 ------ hapi/leads.py | 139 ------------------------ hapi/mixins/threading.py | 13 +-- hapi/prospects.py | 73 ------------- hapi/settings.py | 45 -------- hapi/test/.gitignore | 1 - hapi/test/helper.py | 14 +-- hapi/test/test_base.py | 22 ++-- hapi/test/test_broadcast.py | 78 -------------- hapi/test/test_contact_lists.py | 5 +- hapi/test/test_contacts.py | 149 +++++++++++++------------ hapi/test/test_credentials.json | 3 + hapi/test/test_error.py | 14 +-- hapi/test/test_keywords.py | 185 ------------------------------- hapi/test/test_leads.py | 29 ----- hapi/test/test_prospects.py | 135 ----------------------- hapi/test/test_settings.py | 62 ----------- hapi/utils.py | 8 +- 26 files changed, 161 insertions(+), 1211 deletions(-) delete mode 100644 hapi/blog.py delete mode 100644 hapi/broadcast.py delete mode 100644 hapi/forms.py delete mode 100644 hapi/keywords.py delete mode 100644 hapi/leads.py delete mode 100644 hapi/prospects.py delete mode 100644 hapi/settings.py delete mode 100644 hapi/test/test_broadcast.py create mode 100644 hapi/test/test_credentials.json delete mode 100644 hapi/test/test_keywords.py delete mode 100644 hapi/test/test_leads.py delete mode 100644 hapi/test/test_prospects.py delete mode 100644 hapi/test/test_settings.py diff --git a/.gitignore b/.gitignore index 08b1c2f..6d422cf 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ dist /build MANIFEST .idea +.DS_Store diff --git a/hapi/base.py b/hapi/base.py index 6298a44..ba002da 100644 --- a/hapi/base.py +++ b/hapi/base.py @@ -1,15 +1,18 @@ -import urllib -import httplib +from __future__ import print_function +from builtins import object +import six +from future.moves.urllib.parse import urlencode +import http.client import simplejson as json -import utils +from . import utils import logging import sys import time import traceback import gzip -import StringIO +import io -from error import HapiError, HapiBadRequest, HapiNotFound, HapiTimeout, HapiServerError, HapiUnauthorized +from .error import HapiError, HapiBadRequest, HapiNotFound, HapiTimeout, HapiServerError, HapiUnauthorized _PYTHON25 = sys.version_info < (2, 6) @@ -45,7 +48,7 @@ def __init__(self, api_key=None, timeout=10, mixins=[], access_token=None, refre self._prepare_connection_type() def _prepare_connection_type(self): - connection_types = {'http': httplib.HTTPConnection, 'https': httplib.HTTPSConnection} + connection_types = {'http': http.client.HTTPConnection, 'https': http.client.HTTPSConnection} parts = self.options['api_base'].split('://') protocol = (parts[0:-1]+['https'])[0] self.options['connection_type'] = connection_types[protocol] @@ -78,13 +81,13 @@ def _prepare_request(self, subpath, params, data, opts, doseq=False, query=''): query = query[1:] if query and not query.startswith('&'): query = '&' + query - url = opts.get('url') or '/%s?%s%s' % (self._get_path(subpath), urllib.urlencode(params, doseq), query) + url = opts.get('url') or '/%s?%s%s' % (self._get_path(subpath), urlencode(params, doseq), query) headers = opts.get('headers') or {} headers.update({ 'Accept-Encoding': 'gzip', 'Content-Type': opts.get('content_type') or 'application/json'}) - if data and not isinstance(data, basestring) and headers['Content-Type']=='application/json': + if data and not isinstance(data, six.string_types) and headers['Content-Type']=='application/json': data = json.dumps(data) return url, headers, data @@ -97,7 +100,10 @@ def _create_request(self, conn, method, url, headers, data): return params def _gunzip_body(self, body): - sio = StringIO.StringIO(body) + if body and isinstance(body, bytes): + sio = io.BytesIO(body) + else: + sio = io.StringIO(body) gf = gzip.GzipFile(fileobj=sio, mode="rb") return gf.read() @@ -132,12 +138,16 @@ def _execute_request(self, conn, request): return result.body def _digest_result(self, data): - if data and isinstance(data, basestring): + if data and isinstance(data, six.string_types): try: data = json.loads(data) except ValueError: pass - + else: + try: + data = json.loads(gzip.GzipFile(fileobj=io.BytesIO(data)).read()) + except ValueError: + pass return data def _call_raw(self, subpath, params=None, method='GET', data=None, doseq=False, query='', retried=False, **options): @@ -167,7 +177,7 @@ def _call_raw(self, subpath, params=None, method='GET', data=None, doseq=False, request_info = self._create_request(connection, method, url, headers, data) result = self._execute_request_raw(connection, request_info) break - except HapiUnauthorized, e: + except HapiUnauthorized as e: self.log.warning("401 Unauthorized response to API request.") if self.access_token and self.refresh_token and self.client_id and not retried: self.log.info("Refreshing access token") @@ -176,7 +186,7 @@ def _call_raw(self, subpath, params=None, method='GET', data=None, doseq=False, decoded = json.loads(token_response) self.access_token = decoded['access_token'] self.log.info('Retrying with new token %' % (self.access_token)) - except Exception, e: + except Exception as e: self.log.error("Unable to refresh access_token: %s" % (e)) raise return self._call_raw(subpath, params=params, method=method, data=data, doseq=doseq, query=query, retried=True, **options) @@ -188,7 +198,7 @@ def _call_raw(self, subpath, params=None, method='GET', data=None, doseq=False, elif self.access_token and not self.client_id: self.log.error("In order to enable automated refreshing of your access token, please provide a client_id in addition to a refresh token.") raise - except HapiError, e: + except HapiError as e: if try_count > num_retries: logging.warning("Too many retries for %s", url) raise diff --git a/hapi/blog.py b/hapi/blog.py deleted file mode 100644 index dda29d1..0000000 --- a/hapi/blog.py +++ /dev/null @@ -1,81 +0,0 @@ -from base import BaseClient -import simplejson as json - -BLOG_API_VERSION = '1' - -class BlogClient(BaseClient): - - def _get_path(self, subpath): - return 'blog/v%s/%s' % (BLOG_API_VERSION, subpath) - - def get_blogs(self, **options): - return self._call('list.json', **options) - - def get_blog_info(self, blog_guid, **options): - return self._call(blog_guid, **options) - - def get_posts(self, blog_guid, **options): - return self._call('%s/posts.json' % blog_guid, **options) - - def get_draft_posts(self, blog_guid, **options): - return self._call('%s/posts.json' % blog_guid, params={'draft': 'true'}, **options) - - def get_published_posts(self, blog_guid, **options): - params = dict(draft='false') - params.update(options) - return self._call('%s/posts.json' % blog_guid, params=params) - - # Spelled wrong but left for compat - def get_pulished_posts(self, blog_guid, **options): - return self._call('%s/posts.json' % blog_guid, params={'draft': 'false'}, **options) - - def get_blog_comments(self, blog_guid, **options): - return self._call('%s/comments.json' % blog_guid, **options) - - def get_post(self, post_guid, **options): - return self._call('posts/%s.json' % post_guid, **options) - - def get_post_comments(self, post_guid, **options): - return self._call('posts/%s/comments.json' % post_guid, **options) - - def get_comment(self, comment_guid, **options): - return self._call('comments/%s.json' % comment_guid, **options) - - def create_post(self, blog_guid, author_name, author_email, title, summary, content, tags, meta_desc, meta_keyword, **options): - post = json.dumps(dict( - title = title, - authorDisplayName = author_name, - authorEmail = author_email, - summary = summary, - body = content, - tags = tags, - metaDescription = meta_desc, - metaKeywords = meta_keyword)) - raw_response = self._call('%s/posts.json' % blog_guid, data=post, method='POST', content_type='application/json', raw_output=True, **options) - return raw_response - - def update_post(self, post_guid, title=None, summary=None, content=None, meta_desc=None, meta_keyword=None, tags=None, **options): - tags = tags or [] - update_param_translation = dict(title='title', summary='summary', content='body', meta_desc='metaDescription', meta_keyword='metaKeywords', tags='tags') - post_dict = dict([(k,locals()[p]) for p,k in update_param_translation.iteritems() if locals().get(p)]) - post = json.dumps(post_dict) - raw_response = self._call('posts/%s.json' % post_guid, data=post, method='PUT', content_type='application/json', raw_output=True, **options) - return raw_response - - def publish_post(self, post_guid, should_notify, publish_time = None, is_draft = 'false', **options): - post = json.dumps(dict( - published = publish_time, - draft = is_draft, - sendNotifications = should_notify)) - raw_response = self._call('posts/%s.json' % post_guid, data=post, method='PUT', content_type='application/json', raw_output=True, **options) - return raw_response - - def create_comment(self, post_guid, author_name, author_email, author_uri, content, **options): - post = json.dumps(dict( - anonyName = author_name, - anonyEmail = author_email, - anonyUrl = author_uri, - comment = content)) - raw_response = self._call('posts/%s/comments.json' % post_guid, data=post, method='POST', content_type='application/json', raw_output=True, **options) - return raw_response - diff --git a/hapi/broadcast.py b/hapi/broadcast.py deleted file mode 100644 index 00fb6ea..0000000 --- a/hapi/broadcast.py +++ /dev/null @@ -1,186 +0,0 @@ -from base import BaseClient - -HUBSPOT_BROADCAST_API_VERSION = '1' - - -class BaseSocialObject(object): - def _camel_case_to_underscores(self, text): - result = [] - pos = 0 - while pos < len(text): - if text[pos].isupper(): - if pos - 1 > 0 and text[pos - 1].islower() or pos - 1 > 0 and pos + 1 < len(text) and text[pos + 1].islower(): - result.append("_%s" % text[pos].lower()) - else: - result.append(text[pos].lower()) - else: - result.append(text[pos]) - pos += 1 - return "".join(result) - - def _underscores_to_camel_case(self, text): - result = [] - pos = 0 - while pos < len(text): - if text[pos] == "_" and pos + 1 < len(text): - result.append("%s" % text[pos + 1].upper()) - pos += 1 - else: - result.append(text[pos]) - pos += 1 - return "".join(result) - - def to_dict(self): - dict_self = {} - for key in vars(self): - dict_self[self._underscores_to_camel_case(key)] = getattr(self, key) - return dict_self - - def from_dict(self, data): - accepted_fields = self.accepted_fields() - for key in data: - if key in accepted_fields: - setattr(self, self._camel_case_to_underscores(key), data[key]) - - -class Broadcast(BaseSocialObject): - '''Defines a social media broadcast message for the broadcast api''' - - # Constants for remote content type - COS_LP = "coslp" - COS_BLOG = "cosblog" - LEGACY_LP = "cmslp" - LEGACY_BLOG = "cmsblog" - - def __init__(self, broadcast_data): - self.data_parse(broadcast_data) - - def accepted_fields(self): - return [ - 'broadcastGuid', - 'campaignGuid', - 'channel', - 'channelGuid', - 'clicks', - 'clientTag', - 'content', - 'createdAt', - 'createdBy', - 'finishedAt', - 'groupGuid', - 'interactions', - 'interactionCounts', - 'linkGuid', - 'message', - 'messageUrl', - 'portalId', - 'remoteContentId', - 'remoteContentType', - 'status', - 'triggerAt', - 'updatedBy' - ] - - def data_parse(self, broadcast_data): - self.from_dict(broadcast_data) - - -class Channel(BaseSocialObject): - '''Defines the social media channel for the broadcast api''' - - def __init__(self, channel_data): - self.data_parse(channel_data) - - def accepted_fields(self): - return ['channelGuid', 'accountGuid', 'account', - 'type', 'name', 'dataMap', 'createdAt', 'settings'] - - def data_parse(self, channel_data): - self.from_dict(channel_data) - - -class BroadcastClient(BaseClient): - '''Broadcast API to manage messages published to social networks''' - - def _get_path(self, method): - return 'broadcast/v%s/%s' % (HUBSPOT_BROADCAST_API_VERSION, method) - - def get_broadcast(self, broadcast_guid, **kwargs): - ''' - Get a specific broadcast by guid - ''' - params = kwargs - broadcast = self._call('broadcasts/%s' % broadcast_guid, - params=params, content_type='application/json') - return Broadcast(broadcast) - - def get_broadcasts(self, type="", page=None, - remote_content_id=None, limit=None, **kwargs): - ''' - Get all broadcasts, with optional paging and limits. - Type filter can be 'scheduled', 'published' or 'failed' - ''' - if remote_content_id: - return self.get_broadcasts_by_remote(remote_content_id) - - params = {'type': type} - if page: - params['page'] = page - - params.update(kwargs) - - result = self._call('broadcasts', params=params, - content_type='application/json') - broadcasts = [Broadcast(b) for b in result] - - if limit: - return broadcasts[:limit] - return broadcasts - - def create_broadcast(self, broadcast): - if not isinstance(broadcast, dict): - return self._call('broadcasts', data=broadcast.to_dict(), - method='POST', content_type='application/json') - else: - return self._call('broadcasts', data=broadcast, - method='POST', content_type='application/json') - - def cancel_broadcast(self, broadcast_guid): - ''' - Cancel a broadcast specified by guid - ''' - subpath = 'broadcasts/%s/update' % broadcast_guid - broadcast = {'status': 'CANCELED'} - bcast_dict = self._call(subpath, method='POST', data=broadcast, - content_type='application/json') - return bcast_dict - - def get_channel(self, channel_guid): - channel = self._call('channels/%s' % channel_guid, - content_type='application/json') - return Channel(channel) - - def get_channels(self, current=True, publish_only=False, settings=False): - """ - if "current" is false it will return all channels that a user - has published to in the past. - - if publish_only is set to true, then return only the channels - that are publishable. - - if settings is true, the API will make extra queries to return - the settings for each channel. - """ - if publish_only: - if current: - endpoint = 'channels/setting/publish/current' - else: - endpoint = 'channels/setting/publish' - else: - if current: - endpoint = 'channels/current' - else: - endpoint = 'channels' - - result = self._call(endpoint, content_type='application/json', params=dict(settings=settings)) - return [Channel(c) for c in result] diff --git a/hapi/contact_lists.py b/hapi/contact_lists.py index 6ce2ea4..11f5fcb 100644 --- a/hapi/contact_lists.py +++ b/hapi/contact_lists.py @@ -1,5 +1,5 @@ -from base import BaseClient -import logging_helper +from .base import BaseClient +from . import logging_helper CONTACT_LISTS_API_VERSION = '1' diff --git a/hapi/contacts.py b/hapi/contacts.py index 0157230..3bc89ec 100644 --- a/hapi/contacts.py +++ b/hapi/contacts.py @@ -1,6 +1,6 @@ -from base import BaseClient -import logging_helper -from urllib import quote +from .base import BaseClient +from . import logging_helper +from six.moves.urllib.parse import quote CONTACTS_API_VERSION = '1' diff --git a/hapi/error.py b/hapi/error.py index efd6859..967af22 100644 --- a/hapi/error.py +++ b/hapi/error.py @@ -1,3 +1,9 @@ +from builtins import str +from builtins import object +import six +from builtins import str as str + + class EmptyResult(object): ''' Null Object pattern to prevent Null reference errors @@ -9,7 +15,7 @@ def __init__(self): self.msg = '' self.reason = '' - def __nonzero__(self): + def __bool__(self): return False @@ -55,7 +61,7 @@ def __init__(self, result, request, err=None): self.err = err def __str__(self): - return self.__unicode__().encode('ascii', 'replace') + return self.__unicode__() def __unicode__(self): @@ -73,11 +79,11 @@ def __unicode__(self): def _dict_vals_to_unicode(self, data): unicode_data = {} - for key, val in data.items(): - if not isinstance(val, basestring): - unicode_data[key] = unicode(val) - elif not isinstance(val, unicode): - unicode_data[key] = unicode(val, 'utf8', 'ignore') + for key, val in list(data.items()): + if not isinstance(val, six.string_types): + unicode_data[key] = str(val) + elif not isinstance(val, six.string_types): + unicode_data[key] = str(val, 'utf8', 'ignore') else: unicode_data[key] = val return unicode_data diff --git a/hapi/forms.py b/hapi/forms.py deleted file mode 100644 index 570ea90..0000000 --- a/hapi/forms.py +++ /dev/null @@ -1,26 +0,0 @@ -import logging -logger = logging.getLogger(__name__) - -from base import BaseClient - - -class FormSubmissionClient(BaseClient): - - def __init__(self, *args, **kwargs): - super(FormSubmissionClient, self).__init__(*args, **kwargs) - self.options['api_base'] = 'forms.hubspot.com' - - def _get_path(self, subpath): - return '/uploads/form/v2/%s' % subpath - - def submit_form(self, portal_id, form_guid, data, **options): - subpath = '%s/%s' % (portal_id, form_guid) - opts = {'content_type': 'application/x-www-form-urlencoded'} - options.update(opts) - return self._call( - subpath=None, - url=self._get_path(subpath), - method='POST', - data=data, - **options - ) diff --git a/hapi/keywords.py b/hapi/keywords.py deleted file mode 100644 index fdd0811..0000000 --- a/hapi/keywords.py +++ /dev/null @@ -1,35 +0,0 @@ -from base import BaseClient - -KEYWORDS_API_VERSION = 'v1' - -class KeywordsClient(BaseClient): - - def _get_path(self, subpath): - return 'keywords/%s/%s' % (KEYWORDS_API_VERSION, subpath) - - # Contains both list of keywords and metadata - def get_keywords_info(self, **options): - return self._call('keywords', **options) - - # *Only* returns the list of keywords, does not include additional metadata - def get_keywords(self, **options): - return self._call('keywords', **options)['keywords'] - - def get_keyword(self, keyword_guid, **options): - return self._call('keywords/%s' % keyword_guid, **options) - - def add_keyword(self, keyword, **options): - return self._call('keywords', data=dict(keyword=str(keyword)), method='PUT', **options) - - def add_keywords(self, keywords, **options): - data = [] - for keyword in keywords: - if keyword != '': - if type(keyword) is dict: - data.append(keyword) - elif type(keyword) is str: - data.append(dict(keyword=str(keyword))) - return self._call('keywords', data=data, method='PUT', **options)['keywords'] - - def delete_keyword(self, keyword_guid, **options): - return self._call('keywords/%s' % keyword_guid, method='DELETE', **options) diff --git a/hapi/leads.py b/hapi/leads.py deleted file mode 100644 index bd4fa81..0000000 --- a/hapi/leads.py +++ /dev/null @@ -1,139 +0,0 @@ -import time -from base import BaseClient -import logging_helper -#from pprint import pprint - - - -LEADS_API_VERSION = '1' - -def list_to_dict_with_python_case_keys(list_): - d = {} - for item in list_: - d[item] = item - if item.lower() != item: - python_variant = item[0].lower() + ''.join([c if c.lower()==c else '_%s'%c.lower() for c in item[1:]]) - d[python_variant] = item - return d - -SORT_OPTIONS = [ - 'firstName', - 'lastName', - 'email', - 'address', - 'phone', - 'insertedAt', - 'fce.convertDate', - 'lce.convertDate', - 'lastModifiedAt', - 'closedAt'] -SORT_OPTIONS_DICT = list_to_dict_with_python_case_keys(SORT_OPTIONS) -TIME_PIVOT_OPTIONS = [ - 'insertedAt', - 'firstConvertedAt', - 'lastConvertedAt', - 'lastModifiedAt', - 'closedAt'] -TIME_PIVOT_OPTIONS_DICT = list_to_dict_with_python_case_keys(TIME_PIVOT_OPTIONS) -SEARCH_OPTIONS = [ - 'search', - 'sort', - 'dir', - 'max', - 'offset', - 'startTime', - 'stopTime', - 'timePivot', - 'excludeConversionEvents', - 'emailOptOut', - 'eligibleForEmail', - 'bounced', - 'isNotImported'] -SEARCH_OPTIONS_DICT = list_to_dict_with_python_case_keys(SEARCH_OPTIONS) -BOOLEAN_SEARCH_OPTIONS = set([ - 'excludeConversionEvents', - 'emailOptOut', - 'eligibleForEmail', - 'bounced', - 'isNotImported']) - -MAX_BATCH=100 - -class LeadsClient(BaseClient): - """ - The hapipy Leads client uses the _make_request method to call the API for data. It returns a python object translated from the json return - """ - - def __init__(self, *args, **kwargs): - super(LeadsClient, self).__init__(*args, **kwargs) - self.log = logging_helper.get_log('hapi.leads') - - def camelcase_search_options(self, options): - """change all underscored variants back to what the API is expecting""" - new_options = {} - for key in options: - value = options[key] - new_key = SEARCH_OPTIONS_DICT.get(key, key) - if new_key == 'sort': - value = SORT_OPTIONS_DICT.get(value, value) - elif new_key == 'timePivot': - value = TIME_PIVOT_OPTIONS_DICT.get(value, value) - elif new_key in BOOLEAN_SEARCH_OPTIONS: - value = str(value).lower() - new_options[new_key] = value - return new_options - - def _get_path(self, subpath): - return 'leads/v%s/%s' % (self.options.get('version') or LEADS_API_VERSION, subpath) - - def get_lead(self, guid, **options): - return self.get_leads(guid, **options)[0] - - def get_leads(self, *guids, **options): - """Supports all the search parameters in the API as well as python underscored variants""" - original_options = options - options = self.camelcase_search_options(options.copy()) - params = {} - for i in xrange(len(guids)): - params['guids[%s]'%i] = guids[i] - for k in options.keys(): - if k in SEARCH_OPTIONS: - params[k] = options[k] - del options[k] - leads = self._call('list/', params, **options) - self.log.info("retrieved %s leads through API ( %soptions=%s )" % - (len(leads), guids and 'guids=%s, '%guids or '', original_options)) - return leads - - def retrieve_lead(self, *guid, **options): - cur_guid = guid or '' - params = {} - for key in options: - params[key] = options[key] - """ Set guid to -1 as default for not finding a user """ - lead = {'guid' : '-1'} - """ wrap lead call so that it doesn't error out when not finding a lead """ - try: - lead = self._call('lead/%s' % cur_guid, params, **options) - except: - """ no lead here """ - return lead - - - def update_lead(self, guid, update_data=None, **options): - update_data = update_data or {} - update_data['guid'] = guid - return self._call('lead/%s/' % guid, data=update_data, method='PUT', **options) - - def get_webhook(self, **options): #WTF are these 2 methods for? - return self._call('callback-url', **options) - - def register_webhook(self, url, **options): - return self._call('callback-url', params={'url': url}, data={'url': url}, method='POST', **options) - - def close_lead(self, guid, close_time=None, **options): - return self.update_lead(guid, {'closedAt': close_time or int(time.time()*1000)}, **options) - - def open_lead(self, guid, **options): - self.update_lead(guid, {'closedAt': ''}, **options) - diff --git a/hapi/mixins/threading.py b/hapi/mixins/threading.py index 8765185..858cd94 100644 --- a/hapi/mixins/threading.py +++ b/hapi/mixins/threading.py @@ -6,7 +6,8 @@ [here for python 2.5](http://www.lfd.uci.edu/~gohlke/pythonlibs/#pycurl). ''' -import pycurl, cStringIO +from builtins import object +import pycurl, io class HapiThreadedError(ValueError): def __init__(self, curl): @@ -74,8 +75,8 @@ def _create_curl(self, url, headers, data): c.data = data c.status = -1 - c.body = cStringIO.StringIO() - c.response_headers = cStringIO.StringIO() + c.body = io.StringIO() + c.response_headers = io.StringIO() c.setopt(c.URL, c.full_url) c.setopt(c.TIMEOUT, self.options['timeout']) @@ -83,10 +84,10 @@ def _create_curl(self, url, headers, data): c.setopt(c.HEADERFUNCTION, c.response_headers.write) if headers: - c.setopt(c.HTTPHEADER, [ "%s: %s" % (x, y) for x, y in headers.items() ]) + c.setopt(c.HTTPHEADER, [ "%s: %s" % (x, y) for x, y in list(headers.items()) ]) if data: - c.data_out = cStringIO.StringIO(data) + c.data_out = io.StringIO(data) c.setopt(c.READFUNCTION, c.data_out.getvalue) return c @@ -120,7 +121,7 @@ def process_queue(self): for c in m.handles: c.status = c.getinfo(c.HTTP_CODE) if 'Content-Encoding: gzip' in c.response_headers.getvalue(): - c.body = cStringIO.StringIO(self._gunzip_body(c.body.getvalue())) + c.body = io.StringIO(self._gunzip_body(c.body.getvalue())) result = { "data" : self._digest_result(c.body.getvalue()), "code": c.status } if not c.status or c.status >= 400: # Don't throw the exception because some might have succeeded diff --git a/hapi/prospects.py b/hapi/prospects.py deleted file mode 100644 index 48ef23e..0000000 --- a/hapi/prospects.py +++ /dev/null @@ -1,73 +0,0 @@ -from base import BaseClient - -PROSPECTS_API_VERSION = 'v1' - -class ProspectsClient(BaseClient): - """ Python client for the HubSpot Prospects API. - - This client provides convenience methods for the HubSpot Prospects API. - It is a work in progress, and contributions are welcome. - - Questions, comments, etc: http://docs.hubapi.com/wiki/Discussion_Group. - - """ - - def _get_path(self, method): - return 'prospects/%s/%s' % (PROSPECTS_API_VERSION, method) - - def get_prospects(self, offset=None, orgoffset=None, limit=None): - """ Return the prospects for the current API key. - - Optionally start the result list at the given offset. - - Each member of the return list is a prospect element containing - organizational information such as name and location. - - """ - params = {} - if limit: - params['count'] = limit - - if offset: - params['timeOffset'] = offset - params['orgOffset'] = orgoffset - - return self._call('timeline', params) - - def get_company(self, company_slug): - # Return the specific named organization for the given API key, if we find a match. - return self._call('timeline/%s' % company_slug) - - def get_options_for_query(self, query): - # This method allows for discovery of prospects with partial names. - return self._call('typeahead/', {'q': query}) - - def search_prospects(self, search_type, query, offset=None, orgoffset=None): - """ Supports doing a search for prospects by city, reion, or country. - - search_type should be one of 'city' 'region' 'country'. - - This method is intended to be called with one of the outputs from the - get_options_for_query method above. - - """ - - params = {'q': query} - if offset and orgoffset: - params['orgOffset'] = orgoffset - params['timeOffset'] = offset - - return self._call('search/%s' % search_type, params) - - def get_hidden_prospects(self): - # Return the list of prospects hidden by the customer (or this API), if any. - return self._call('filters') - - def hide_prospect(self, company_name): - # Hides the given prospect from the user interface. - return self._call('filters', data=('organization=%s' % company_name), method="POST", content_type="application/x-www-form-urlencoded") - - def unhide_prospect(self, company_name): - # Un-hides, i.e. displays, the given prospect in the user interface. - return self._call('filters', data={'organization': company_name}, method="DELETE") - diff --git a/hapi/settings.py b/hapi/settings.py deleted file mode 100644 index 392bb98..0000000 --- a/hapi/settings.py +++ /dev/null @@ -1,45 +0,0 @@ -from base import BaseClient - -SETTINGS_API_VERSION = 'v1' - -class SettingsClient(BaseClient): - """ Basic Python client for the HubSpot Settings API. - - Use this to read settings for a given API key, as well as update a setting. - - Reference docs: http://docs.hubapi.com/wiki/Settings_API - - Comments, questions, etc: http://docs.hubapi.com/wiki/Discussion_Group - """ - - def _get_path(self, subpath): - return 'settings/%s/%s' % (SETTINGS_API_VERSION, subpath) - - def get_settings(self, **options): - # Returns the settings we know about for this API key. - return self._call('settings', **options) - - def get_setting(self, name, **options): - # Returns the specific requested setting name, if found. - params = { 'name' : name } - return self._call('settings', params=params, **options) - - def update_setting(self, data, **options): - # Updates a specific setting for this API key. - params = {} - if data['name']: - params['name'] = data['name'] - if data['value']: - params['value'] = data['value'] - - return self._call('settings', params=params, data=data, method='POST', **options) - - def delete_setting(self, name, **options): - # "Deletes" a specific setting by emptying out its value. - params = {} - if name: - params['name'] = name - else: - raise HapiError('Setting name required.') - return self._call('settings', params=params, method='DELETE', **options) - diff --git a/hapi/test/.gitignore b/hapi/test/.gitignore index 1179873..0ade2cf 100644 --- a/hapi/test/.gitignore +++ b/hapi/test/.gitignore @@ -1,2 +1 @@ -test_credentials.json test_run.log diff --git a/hapi/test/helper.py b/hapi/test/helper.py index 50c7d2c..7b88e61 100644 --- a/hapi/test/helper.py +++ b/hapi/test/helper.py @@ -1,6 +1,6 @@ import os import json -import logger +from . import logger def get_options(): @@ -12,23 +12,23 @@ def get_options(): try: raw_text = open(path).read() except IOError: - raise Exception, """ + raise Exception(""" Unable to open '%s' for integration tests.\n If this file exists, then you are indicating you want to override the standard 'demo' creds with your own.\n - However, it is currently inaccessible so that is a problem.""" % filename + However, it is currently inaccessible so that is a problem.""" % filename) try: options = json.loads(raw_text) except ValueError: - raise Exception, """ + raise Exception(""" '%s' doesn't appear to be valid json!\n If this file exists, then you are indicating you want to override the standard 'demo' creds with your own.\n - However, if I can't understand the json inside of it, then that is a problem.""" % filename + However, if I can't understand the json inside of it, then that is a problem.""" % filename) if not options.get('api_key') and not options.get('hapikey'): - raise Exception, """ + raise Exception(""" '%s' seems to have no 'api_key' or 'access_token' specified!\n If this file exists, then you are indicating you want to override the standard 'demo' creds with your own.\n - However, I'll need at least an API key to work with, or it definitely won't work.""" % filename + However, I'll need at least an API key to work with, or it definitely won't work.""" % filename) options['api_key'] = options.get('api_key') or options.get('hapikey') return options diff --git a/hapi/test/test_base.py b/hapi/test/test_base.py index fa353cf..bcd532f 100644 --- a/hapi/test/test_base.py +++ b/hapi/test/test_base.py @@ -1,7 +1,9 @@ +from __future__ import print_function +from builtins import object from collections import defaultdict import unittest2 import simplejson as json -from StringIO import StringIO +from io import StringIO, BytesIO from gzip import GzipFile from hapi.base import BaseClient @@ -15,7 +17,7 @@ def _get_path(self, subpath): class TestResult(object): def __init__(self, *args, **kwargs): - for k, v in kwargs.items(): + for k, v in list(kwargs.items()): setattr(self, k, v) def getheaders(self): @@ -46,7 +48,7 @@ def test_prepare_request(self): # so duplicate=key&duplicate=value doseq = True url, headers, data = self.client._prepare_request(subpath, params, data, opts, doseq) - print url + print(url) self.assertTrue('duplicate=key&duplicate=' in url) def test_call(self): @@ -68,8 +70,8 @@ def execute_request_with_retries(a, b): # This should fail once, and then succeed result = client._call(*args, **kwargs) - self.assertEquals(2, counter['count']) - self.assertEquals('SUCCESS', result) + self.assertEqual(2, counter['count']) + self.assertEqual('SUCCESS', result) @@ -91,18 +93,18 @@ def test_digest_result(self): """ plain_text = "Hello Plain Text" data = self.client._process_body(plain_text, False) - self.assertEquals(plain_text, data) + self.assertEqual(plain_text, data) raw_json = '{"hello": "json"}' data = json.loads(self.client._process_body(raw_json, False)) # Should parse as json into dict - self.assertEquals(data.get('hello'), 'json') + self.assertEqual(data.get('hello'), 'json') # Write our data into a gzipped stream - sio = StringIO() + sio = BytesIO() gzf = GzipFile(fileobj=sio, mode='wb') - gzf.write('{"hello": "gzipped"}') + gzf.write(b'{"hello": "gzipped"}') gzf.close() data = json.loads(self.client._process_body(sio.getvalue(), True)) - self.assertEquals(data.get('hello'), 'gzipped') \ No newline at end of file + self.assertEqual(data.get('hello'), 'gzipped') \ No newline at end of file diff --git a/hapi/test/test_broadcast.py b/hapi/test/test_broadcast.py deleted file mode 100644 index f2d2787..0000000 --- a/hapi/test/test_broadcast.py +++ /dev/null @@ -1,78 +0,0 @@ -import unittest2 -import time - -from nose.plugins.attrib import attr - -import helper -from hapi.broadcast import Broadcast, BroadcastClient - - -class BroadcastClientTest(unittest2.TestCase): - """ Unit tests for the HubSpot Broadcast API Python client. - - This file contains some unittest tests for the Broadcast API. - - Questions, comments: http://docs.hubapi.com/wiki/Discussion_Group - """ - - def setUp(self): - self.client = BroadcastClient(**helper.get_options()) - self.broadcast_guids = None - - def tearDown(self): - # Cancel any broadcasts created as part of the tests - if self.broadcast_guids: - map(self.client.cancel_broadcast, self.broadcast_guids) - - @attr('api') - def test_get_broadcasts(self): - # Should fetch at least 1 broadcast on the test portal 62515 - broadcasts = self.client.get_broadcasts(limit=1) - self.assertTrue(len(broadcasts) > 0) - - broadcast = broadcasts[0].to_dict() - self.assertIsNotNone(broadcast['channelGuid']) - print "\n\nFetched some broadcasts" - - broadcast_guid = broadcast['broadcastGuid'] - # Re-fetch the broadcast using different call - bcast = self.client.get_broadcast(broadcast_guid) - # Should have expected fields - self.assertIsNotNone(bcast.broadcast_guid) - self.assertIsNotNone(bcast.channel_guid) - self.assertIsNotNone(bcast.status) - - @attr('api') - def test_get_channels(self): - # Fetch older channels ensured to exist - channels = self.client.get_channels(current=False) - self.assertTrue(len(channels) > 0) - - @attr('api') - def test_create_broadcast(self): - content = dict(body="Test hapipy unit tests http://www.hubspot.com") - channels = self.client.get_channels(current=True, publish_only=True) - if len(channels) == 0: - self.fail("Failed to find a publishable channel") - - channel = channels[0] - - # Get a trigger in the future - trigger_at = int(time.time() + 6000) * 1000 - bcast = Broadcast({"content": content, "triggerAt": - trigger_at, "channelGuid": channel.channel_guid}) - - try: - resp = self.client.create_broadcast(bcast) - broadcast = Broadcast(resp) - self.assertIsNotNone(broadcast.broadcast_guid) - self.assertEqual(channel.channel_guid, broadcast.channel_guid) - # Ensure it is canceled - self.broadcast_guids = [] - self.broadcast_guids.append(broadcast.broadcast_guid) - except Exception as e: - self.fail("Should not have raised exception: %s" % e) - - -if __name__ == "__main__": - unittest2.main() diff --git a/hapi/test/test_contact_lists.py b/hapi/test/test_contact_lists.py index fa3cce6..a56b3be 100644 --- a/hapi/test/test_contact_lists.py +++ b/hapi/test/test_contact_lists.py @@ -1,12 +1,13 @@ +from builtins import str import unittest2 import random from nose.plugins.attrib import attr -import helper +from . import helper from hapi.contact_lists import ContactListsClient from hapi.contacts import ContactsClient -from test_contacts import ContactsClientTestCase +from .test_contacts import ContactsClientTestCase class ContactsListsClientTestCase(unittest2.TestCase): diff --git a/hapi/test/test_contacts.py b/hapi/test/test_contacts.py index bfd4a82..3fbe6c5 100644 --- a/hapi/test/test_contacts.py +++ b/hapi/test/test_contacts.py @@ -3,7 +3,7 @@ from faker import Faker from nose.plugins.attrib import attr -import helper +from . import helper from hapi.contacts import ContactsClient fake = Faker() @@ -18,100 +18,99 @@ class ContactsClientTestCase(unittest2.TestCase): """ test_contact_json = { - "properties": [ - { - "property": "email", - "value": fake.email() - }, - { - "property": "firstname", - "value": fake.first_name() - }, - { - "property": "lastname", - "value": fake.last_name() - }, - { - "property": "website", - "value": fake.url() - }, - { - "property": "company", - "value": fake.company() - }, - { - "property": "phone", - "value": fake.phone_number() - }, - { - "property": "address", - "value": fake.street_address() - }, - { - "property": "city", - "value": fake.city() - }, - { - "property": "state", - "value": fake.state() - }, - { - "property": "zip", - "value": fake.zipcode() - } - ] - } + "properties": [ + { + "property": "email", + "value": fake.email() + }, + { + "property": "firstname", + "value": fake.first_name() + }, + { + "property": "lastname", + "value": fake.last_name() + }, + { + "property": "website", + "value": fake.url() + }, + { + "property": "company", + "value": fake.company() + }, + { + "property": "phone", + "value": fake.phone_number() + }, + { + "property": "address", + "value": fake.street_address() + }, + { + "property": "city", + "value": fake.city() + }, + { + "property": "state", + "value": fake.state() + }, + { + "property": "zip", + "value": fake.zipcode() + } + ] + } def setUp(self): - self.client = ContactsClient(**helper.get_options()) - self.contacts = [] + self.client = ContactsClient(**helper.get_options()) + self.contacts = [] def tearDown(self): - """ Cleans up the created objects. """ - if self.contacts: - [self.client.delete_a_contact(contact) for contact in self.contacts] + """ Cleans up the created objects. """ + if self.contacts: + [self.client.delete_a_contact(contact) for contact in self.contacts] @attr('api') def test_create_or_update_a_contact(self): - """ Test the create or update a contact endpoint is valid. """ - email = self.test_contact_json['properties'][0]['value'] + """ Test the create or update a contact endpoint is valid. """ + email = self.test_contact_json['properties'][0]['value'] - response = self.client.create_or_update_a_contact(email, data=self.test_contact_json) - self.assertTrue(len(response) > 0) - - self.contacts.append(response['vid']) + response = self.client.create_or_update_a_contact(email, data=self.test_contact_json) + self.assertTrue(len(response) > 0) + self.contacts.append(response['vid']) @attr('api') def test_get_contact_by_email(self): - """ Test that the get contact by email address endoint is valid. """ - email = self.test_contact_json['properties'][0]['value'] - contact = self.client.create_or_update_a_contact(email, data=self.test_contact_json)['vid'] + """ Test that the get contact by email address endoint is valid. """ + email = self.test_contact_json['properties'][0]['value'] + contact = self.client.create_or_update_a_contact(email, data=self.test_contact_json)['vid'] - response = self.client.get_contact_by_email(email) - self.assertTrue(len(response) > 0) + response = self.client.get_contact_by_email(email) + self.assertTrue(len(response) > 0) - self.contacts.append(contact) + self.contacts.append(contact) @attr('api') def test_update_a_contact(self): - """ Test that the update contact endpoint is valid and that changes persist. """ - email = self.test_contact_json['properties'][0]['value'] - contact = self.client.create_or_update_a_contact(email, data=self.test_contact_json)['vid'] - new_contact_json = self.test_contact_json.copy() - new_contact_json['properties'][4]['value'] = new_contact_json['properties'][4]['value'] + ' UPDATED' + """ Test that the update contact endpoint is valid and that changes persist. """ + email = self.test_contact_json['properties'][0]['value'] + contact = self.client.create_or_update_a_contact(email, data=self.test_contact_json)['vid'] + new_contact_json = self.test_contact_json.copy() + new_contact_json['properties'][4]['value'] = new_contact_json['properties'][4]['value'] + ' UPDATED' - response = self.client.update_a_contact(contact, data=self.test_contact_json) - contact_response = self.client.get_contact_by_email(email) + response = self.client.update_a_contact(contact, data=self.test_contact_json) + contact_response = self.client.get_contact_by_email(email) - self.assertEqual(contact_response['properties']['company']['value'], new_contact_json['properties'][4]['value']) + self.assertEqual(contact_response['properties']['company']['value'], new_contact_json['properties'][4]['value']) - self.contacts.append(contact) + self.contacts.append(contact) @attr('api') def test_delete_a_contact(self): - """ Test that the delete contact endpoint is valid. """ - email = self.test_contact_json['properties'][0]['value'] - contact = self.client.create_or_update_a_contact(email, data=self.test_contact_json)['vid'] + """ Test that the delete contact endpoint is valid. """ + email = self.test_contact_json['properties'][0]['value'] + contact = self.client.create_or_update_a_contact(email, data=self.test_contact_json)['vid'] - response = self.client.delete_a_contact(contact) - self.assertTrue(len(response) > 0) + response = self.client.delete_a_contact(contact) + self.assertTrue(len(response) > 0) diff --git a/hapi/test/test_credentials.json b/hapi/test/test_credentials.json new file mode 100644 index 0000000..5c9a902 --- /dev/null +++ b/hapi/test/test_credentials.json @@ -0,0 +1,3 @@ +{ + "api_key": "9b1a7bfc-4457-4c9b-b2a2-0cc056352a1c" +} \ No newline at end of file diff --git a/hapi/test/test_error.py b/hapi/test/test_error.py index e206657..3af7713 100644 --- a/hapi/test/test_error.py +++ b/hapi/test/test_error.py @@ -1,4 +1,6 @@ +from builtins import str +from builtins import object from hapi.error import HapiError, EmptyResult from nose.tools import ok_ @@ -18,23 +20,23 @@ def __init__(self): def test_unicode_error(): result = MockResult() - result.body = 'A HapiException with unicode \u8131 \xe2\x80\xa2\t' + result.body = 'A HapiException with unicode \\u8131 \xe2\x80\xa2\t' result.reason = 'Why must everything have a reason?' request = {} for key in ('method', 'host', 'url', 'timeout', 'data', 'headers'): request[key] = '' - request['url'] = u'http://adomain/with-unicode-\u8131' + request['url'] = 'http://adomain/with-unicode-\\u8131' # Note the following line is missing the 'u' modifier on the string, # this is intentional to simulate poorly formatted input that should # still be handled without an exception - request['data'] = "A HapiException with unicode \u8131 \xe2\x80\xa2" - request['headers'] = {'Cookie': "with unicode \u8131 \xe2\x80\xa2"} + request['data'] = "A HapiException with unicode \\u8131 \xe2\x80\xa2" + request['headers'] = {'Cookie': "with unicode \\u8131 \xe2\x80\xa2"} exc = HapiError(result, request) - ok_(request['url'] in unicode(exc)) + ok_(request['url'] in str(exc)) ok_(result.reason in str(exc)) def test_error_with_no_result_or_request(): exc = HapiError(None, None, 'a silly error') - ok_('a silly error' in unicode(exc)) + ok_('a silly error' in str(exc)) diff --git a/hapi/test/test_keywords.py b/hapi/test/test_keywords.py deleted file mode 100644 index 2edded3..0000000 --- a/hapi/test/test_keywords.py +++ /dev/null @@ -1,185 +0,0 @@ -# coding: utf-8 -import random -import unittest2 -import uuid - -import simplejson as json -from nose.plugins.attrib import attr - -import helper -from hapi.keywords import KeywordsClient - -class KeywordsClientTest(unittest2.TestCase): - """ Unit tests for the HubSpot Keyword API Python client. - - This file contains some unittest tests for the Keyword API. - - Questions, comments: http://docs.hubapi.com/wiki/Discussion_Group - """ - - def setUp(self): - self.client = KeywordsClient(**helper.get_options()) - self.keyword_guids = None - - def tearDown(self): - if (self.keyword_guids): - map( - lambda keyword_guid: self.client.delete_keyword(keyword_guid), - self.keyword_guids - ) - - @attr('api') - def test_get_keywords(self): - keywords = self.client.get_keywords() - self.assertTrue(len(keywords)) - - print "\n\nGot some keywords: %s" % json.dumps(keywords) - - @attr('api') - def test_get_keyword(self): - keywords = self.client.get_keywords() - if len(keywords) < 1: - self.fail("No keywords available for test.") - - keyword = keywords[0] - print "\n\nGoing to get a specific keyword: %s" % keyword - - result = self.client.get_keyword(keyword['keyword_guid']) - self.assertEquals(keyword, result) - - print "\n\nGot a single matching keyword: %s" % keyword['keyword_guid'] - -# TODO This test does not currently work because there is no traffic on the demo portal -# Becuase there is no traffic, there are no visits or leads for this to look at -# @attr('api') -# def test_get_keyword_with_visit_lead(self): -# # Change the test keyword if you are running on not the demo portal -# test_keyword = "app" -# keywords = self.client.get_keywords() -# if len(keywords) < 1: -# self.fail("No keywords available for test.") -# for keyword in keywords: -# if keyword['keyword'] == test_keyword: -# self.assertTrue(keyword.has_key('visits')) -# self.assertTrue(keyword.has_key('leads')) - - @attr('api') - def test_add_keyword(self): - keyword = [] - # Add a single keyword to this self, it is a string with a uuid added because a string with a - # random number appended to it has too high of a collision rate - keyword.append('hapipy_test_keyword%s' % str(uuid.uuid4())) - - # copy the keyword into 'result' after the client adds it - result = self.client.add_keyword(keyword) - - # make sure 'result' has one keyword in it - self.assertEqual(len(result['keywords']), 1) - - print "\n\nAdded keyword: %s" % json.dumps(result) - - # holds the guid of the keyword being added - self.keyword_guid = [] - - # get the keyword's guid - self.keyword_guid.append(result['keywords'][0]['keyword_guid']) - - # now check if the keyword is in the client - - # get what is in the client - check = self.client.get_keywords() - - # filter 'check' if it is in this self - check = filter(lambda p: p['keyword_guid'] in self.keyword_guid, check) - - # check if it was filtered. If it was, it is in the client - self.assertEqual(len(check), 1) - - print "\n\nSaved keyword %s" % json.dumps(check) - - @attr('api') - def test_add_keywords(self): - # Add multiple Keywords in one API call. - keywords = [] - for i in range(10): - # A string with a random number between 0 and 1000 as a test keyword has too high of a collision rate. - # switched test string to a uuid to decrease collision chance. - keywords.append('hapipy_test_keyword%s' % str(uuid.uuid4())) - - # copy the keywords into 'result' after the client adds them - result = self.client.add_keywords(keywords) - - # Now check if all of the keywords have been put in 'results' - self.assertEqual(len(result), 10) - - # make and fill a list of 'keyword's guid's - self.keyword_guids = [] - for keyword in result: - self.keyword_guids.append(keyword['keyword_guid']) - - # This next section removes keywords from 'keywords' that are already in self by - # checking the guid's. If none of the keywords in 'keywords' are already there, it is done. Otherwise, fails at the assert. - - # Make sure they're in the list now - keywords = self.client.get_keywords() - - keywords = filter(lambda x: x['keyword_guid'] in self.keyword_guids, keywords) - self.assertEqual(len(keywords), 10) - - print "\n\nAdded multiple keywords: %s" % keywords - - @attr('api') - def test_delete_keyword(self): - # Delete multiple keywords in one API call. - keyword = 'hapipy_test_keyword%s' % str(uuid.uuid4()) - result = self.client.add_keyword(keyword) - keywords = result['keywords'] - first_keyword = keywords[0] - print "\n\nAbout to delete a keyword, result= %s" % json.dumps(result) - - self.client.delete_keyword(first_keyword['keyword_guid']) - - # Make sure it's not in the list now - keywords = self.client.get_keywords() - - keywords = filter(lambda x: x['keyword_guid'] == first_keyword['keyword_guid'], keywords) - self.assertTrue(len(keywords) == 0) - - print "\n\nDeleted keyword %s" % json.dumps(first_keyword) - - @attr('api') - def test_utf8_keywords(self): - # Start with base utf8 characters - # TODO: Fails when adding simplified chinese char: 广 or cyrillic: л - utf8_keyword_bases = ['é', 'ü'] - - keyword_guids = [] - for utf8_keyword_base in utf8_keyword_bases: - original_keyword = '%s - %s' % (utf8_keyword_base, str(uuid.uuid4())) - result = self.client.add_keyword(original_keyword) - print "\n\nAdded keyword: %s" % json.dumps(result) - print result - - keywords_results = result.get('keywords') - keyword_result = keywords_results[0] - - self.assertTrue(keyword_result['keyword_guid']) - keyword_guids.append(keyword_result['keyword_guid']) - - actual_keyword = keyword_result['keyword'] - - # Convert to utf-8 to compare strings. Returned string is \x-escaped - if isinstance(original_keyword, unicode): - original_unicode_keyword = original_keyword - else: - original_unicode_keyword = original_keyword.decode('utf-8') - - if isinstance(actual_keyword, unicode): - actual_unicode_keyword = actual_keyword - else: - actual_unicode_keyword = actual_keyword.decode('utf-8') - - self.assertEqual(actual_unicode_keyword, original_unicode_keyword) - -if __name__ == "__main__": - unittest2.main() \ No newline at end of file diff --git a/hapi/test/test_leads.py b/hapi/test/test_leads.py deleted file mode 100644 index a1c95d3..0000000 --- a/hapi/test/test_leads.py +++ /dev/null @@ -1,29 +0,0 @@ -import unittest2 -import helper -from hapi.leads import LeadsClient -import logger -import time - -class LeadsClientTest(unittest2.TestCase): - def setUp(self): - self.client = LeadsClient(**helper.get_options()) - - def tearDown(self): - pass - - def test_camelcased_params(self): - in_options = { - 'sort': 'fce.convert_date', - 'search': 'BlahBlah', - 'time_pivot': 'last_modified_at', - 'is_not_imported': True } - out_options = { - 'sort': 'fce.convertDate', - 'search': 'BlahBlah', - 'timePivot': 'lastModifiedAt', - 'isNotImported': 'true' } - self.assertEquals(out_options, self.client.camelcase_search_options(in_options)) - - -if __name__ == "__main__": - unittest2.main() diff --git a/hapi/test/test_prospects.py b/hapi/test/test_prospects.py deleted file mode 100644 index 7ab6c8a..0000000 --- a/hapi/test/test_prospects.py +++ /dev/null @@ -1,135 +0,0 @@ -import unittest2 -import helper -import simplejson as json -from nose.plugins.attrib import attr -from hapi.prospects import ProspectsClient -from hapi.error import HapiError - -class ProspectsClientTest(unittest2.TestCase): - """ Unit tests for the HubSpot Prospects API Python client. - - This file contains some unittest tests for the Prospects API. - - It is not intended to be exhaustive, just simple exercises and - illustrations, at this point. - - Additional tests and other improvements welcome. - - Questions, comments, etc: http://docs.hubapi.com/wiki/Discussion_Group - """ - - def setUp(self): - self.client = ProspectsClient(**helper.get_options()) - - def tearDown(self): - pass - - @attr('api') - def test_get_prospects(self): - # List the prospects for our Hub ID. - - prospects = self.client.get_prospects() - self.assertTrue(len(prospects)) - print "Got some prospects: %s" % json.dumps(prospects) - - @attr('api') - def test_get_limited_prospects(self): - # Get a specific number of prospects - limit = 10 - - # Loop through and make sure that we always get the number of prospects - # we request. To eliminate flukes, let's loop through and make sure there - # is always bread in the oven. - while limit > 1: - prospects = self.client.get_prospects(limit=limit) - - if prospects['has-more'] is True: - self.assertTrue(len(prospects['prospects']) == limit) - - limit -= 1 - - print "Always got the right number of prospects: %s" % json.dumps(prospects) - - @attr('api') - def test_get_company(self): - # Looks up a specific company. - company = self.client.get_company('') - - # The company may or may not be a prospect. - # self.assertTrue(len(company)) - print "Got the company timeline: %s" % json.dumps(company) - - @attr('api') - def test_search_country(self): - # Looks up prospects from a specific country. - search_results = self.client.search_prospects('country', 'Czech') - - # There may or may not be recent prospects from this country. - # self.assertTrue(len(search_results)) - print "Got some country search results: %s" % json.dumps(search_results) - - @attr('api') - def test_search_region(self): - # Looks up prospects from a specific region, e.g. US state. - search_results = self.client.search_prospects('region', 'Massachusetts') - - # There may or may not be recent prospects from this region. - # self.assertTrue(len(search_results)) - print "Got some region search results: %s" % json.dumps(search_results) - - @attr('api') - def test_search_city(self): - # Looks up prospects from a given city, e.g. Boston. - search_results = self.client.search_prospects('city', 'Boston') - - # There may or may not be recent prospects from this city. - # self.assertTrue(len(search_results)) - print "Got some city search results: %s" % json.dumps(search_results) - - @attr('api') - def test_get_hidden_prospects(self): - # Lists the prospects that have been "hidden" in this account, if any. - hidden_prospects = self.client.get_hidden_prospects() - - # This account may or may not have hidden prospects. - # self.assertTrue(len(hidden_prospects)) - print "Got %d hidden prospects: %s" % (len(hidden_prospects), json.dumps(hidden_prospects)) - - @attr('api') - def test_hide_prospect(self): - # gets one prospect - result = self.client.get_prospects(None, None, 1) - - # checks if it got one - self.assertTrue(len(result['prospects']) == 1) - - # gets just the prospect. result is a list of one prospect - prospect = result['prospects'][0] - - # gets that prospect's slug. This is because hide_prospect wants what organization to hide - # Doesn't use 'organization' because there could be spaces and other junk in there - prospect_slug = prospect['slug'] - - # Tries to hide the prospect. - self.client.hide_prospect(prospect_slug) - - @attr('api') - def test_unhide_prospect(self): - # Tries to un-hide a hidden prospect. - # This test is unusual, and kind of weak, unfortunately, because we can't - # predict what prospects will be hidden already on this shared demo account. - # This API returns a 40x response if no matching hidden prospect is found, - # so we need to catch that error, and we can't assert against it. ;( - data = None - try: - data = self.client.unhide_prospect('') - except HapiError: - print "No matching prospect found to un-hide. This is alright." - - # If there's no matching hidden prospect, can't un-hide it. - # self.assertTrue(len(response)) - if data: - print "Tried to un-hide a prospect: %s" % json.dumps(data) - -if __name__ == "__main__": - unittest2.main() diff --git a/hapi/test/test_settings.py b/hapi/test/test_settings.py deleted file mode 100644 index c12d18a..0000000 --- a/hapi/test/test_settings.py +++ /dev/null @@ -1,62 +0,0 @@ -import random -import unittest2 - -import simplejson as json -from nose.plugins.attrib import attr - -import helper -from hapi.settings import SettingsClient - -class SettingsClientTest(unittest2.TestCase): - """ Unit tests for the HubSpot Settings API Python client. - - This file contains some unittest tests for the Settings API. - - Docs: http://docs.hubapi.com/wiki/Settings_API - - Questions, comments: http://docs.hubapi.com/wiki/Discussion_Group - """ - - def setUp(self): - self.client = SettingsClient(**helper.get_options()) - - def tearDown(self): - pass - - @attr('api') - def test_get_settings(self): - # Get all settings, a lengthy list typically. - settings = self.client.get_settings() - self.assertTrue(len(settings)) - - print "\n\nGot some settings: %s" % json.dumps(settings) - - @attr('api') - def test_get_setting(self): - # Get a specific named setting. - name = 'test_name' - settings = self.client.get_setting(name) - self.assertTrue(len(settings)) - - print "\n\nGot a specific setting: %s, giving %s" % (name, json.dumps(settings)); - - @attr('api') - def test_add_setting(self): - # Add or update a specific setting. - data = { 'name': 'test_name', 'value': 'test_value' } - result = self.client.update_setting(data) - # This is just a 201 response (or 500), no contents. - - print "\n\nUpdated setting: %s." % data['name'] - - @attr('api') - def test_delete_setting(self): - # Deletes a specific setting, emptying out its value. - name = 'test_name' - settings = self.client.delete_setting(name) - # This is just a 201 response (or 500), no contents. - - print "\n\nDeleted setting: %s." % name - -if __name__ == "__main__": - unittest2.main() diff --git a/hapi/utils.py b/hapi/utils.py index 8d99823..cd7a635 100644 --- a/hapi/utils.py +++ b/hapi/utils.py @@ -1,6 +1,6 @@ -import httplib +import http.client import logging -from error import HapiError +from .error import HapiError class NullHandler(logging.Handler): @@ -16,7 +16,7 @@ def get_log(name): def auth_checker(access_token): # Do a simple api request using the access token - connection = httplib.HTTPSConnection('api.hubapi.com') + connection = http.client.HTTPSConnection('api.hubapi.com') connection.request('GET', '/contacts/v1/lists/all/contacts/all?count=1&offset=0&access_token=%s' % access_token) result = connection.getresponse() return result.status @@ -25,7 +25,7 @@ def auth_checker(access_token): def refresh_access_token(refresh_token, client_id): # Refreshes an OAuth access token payload = 'refresh_token=%s&client_id=%s&grant_type=refresh_token' % (refresh_token, client_id) - connection = httplib.HTTPSConnection('api.hubapi.com') + connection = http.client.HTTPSConnection('api.hubapi.com') connection.request('POST', '/auth/v1/refresh', payload) result = connection.getresponse() return result.read() From 3c51f189684309b6ddd121b32666806c83b4f0ad Mon Sep 17 00:00:00 2001 From: Ruichen_Wang Date: Wed, 4 Dec 2019 15:34:17 -0500 Subject: [PATCH 16/25] Bump the version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index d339576..77a8c34 100755 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ setup( name='hapipy', - version='2.10.5.4', + version='2.10.5.5', description="A python wrapper around HubSpot's APIs", long_description=open('README.md').read(), author='HubSpot Dev Team', From f2b9ef0e1dd707ef7cf7630a5fe5d01b1a30826c Mon Sep 17 00:00:00 2001 From: Ruichen_Wang Date: Wed, 4 Dec 2019 18:20:33 -0500 Subject: [PATCH 17/25] Reformat codes, add dependencies for python2/3 compatibility in setup.py --- hapi/base.py | 2 ++ hapi/contacts.py | 2 +- hapi/error.py | 34 ++++++++++++++++++++++------------ setup.py | 4 +++- 4 files changed, 28 insertions(+), 14 deletions(-) diff --git a/hapi/base.py b/hapi/base.py index ba002da..9b3ce8b 100644 --- a/hapi/base.py +++ b/hapi/base.py @@ -139,11 +139,13 @@ def _execute_request(self, conn, request): def _digest_result(self, data): if data and isinstance(data, six.string_types): + # This is for python 2 version, data is str try: data = json.loads(data) except ValueError: pass else: + # This is for python 3 version, data is bytes string and get Gzipped try: data = json.loads(gzip.GzipFile(fileobj=io.BytesIO(data)).read()) except ValueError: diff --git a/hapi/contacts.py b/hapi/contacts.py index 3bc89ec..512a5b4 100644 --- a/hapi/contacts.py +++ b/hapi/contacts.py @@ -1,6 +1,6 @@ from .base import BaseClient from . import logging_helper -from six.moves.urllib.parse import quote +from future.moves.urllib.parse import quote CONTACTS_API_VERSION = '1' diff --git a/hapi/error.py b/hapi/error.py index 967af22..ab42952 100644 --- a/hapi/error.py +++ b/hapi/error.py @@ -1,7 +1,5 @@ -from builtins import str from builtins import object -import six -from builtins import str as str +from future.utils import python_2_unicode_compatible class EmptyResult(object): @@ -48,7 +46,6 @@ class HapiError(ValueError): {error} ''' - def __init__(self, result, request, err=None): super(HapiError,self).__init__(result and result.reason or "Unknown Reason") if result == None: @@ -60,10 +57,10 @@ def __init__(self, result, request, err=None): self.request = request self.err = err + @python_2_unicode_compatible def __str__(self): return self.__unicode__() - def __unicode__(self): params = {} request_keys = ('method', 'host', 'url', 'data', 'headers', 'timeout', 'body') @@ -80,28 +77,41 @@ def __unicode__(self): def _dict_vals_to_unicode(self, data): unicode_data = {} for key, val in list(data.items()): - if not isinstance(val, six.string_types): - unicode_data[key] = str(val) - elif not isinstance(val, six.string_types): - unicode_data[key] = str(val, 'utf8', 'ignore') - else: - unicode_data[key] = val - return unicode_data + try: + # For python2 + basestring + if not isinstance(val, basestring): + unicode_data[key] = unicode(val) + elif not isinstance(val, unicode): + unicode_data[key] = unicode(val, 'utf8', 'ignore') + else: + unicode_data[key] = val + except NameError: + # For python3 + if not isinstance(val, str): + unicode_data[key] = str(val) + else: + unicode_data[key] = val + return unicode_data # Create more specific error cases, to make filtering errors easier class HapiBadRequest(HapiError): '''Error wrapper for most 40X results and 501 results''' + class HapiNotFound(HapiError): '''Error wrapper for 404 and 410 results''' + class HapiTimeout(HapiError): '''Wrapper for socket timeouts, sslerror, and 504''' + class HapiUnauthorized(HapiError): '''Wrapper for 401 Unauthorized errors''' + class HapiServerError(HapiError): '''Wrapper for most 500 errors''' diff --git a/setup.py b/setup.py index 77a8c34..04535e6 100755 --- a/setup.py +++ b/setup.py @@ -13,6 +13,8 @@ license='LICENSE.txt', packages=['hapi', 'hapi.mixins'], install_requires=[ - 'simplejson>=2.1.2' + 'simplejson>=2.1.2', + 'six>=1.12.0', + 'future>=0.18.2' ], ) From 120093d140644f11da774a993fa45b253059177a Mon Sep 17 00:00:00 2001 From: Ruichen_Wang Date: Thu, 5 Dec 2019 11:32:01 -0500 Subject: [PATCH 18/25] Reformat logic of convert arbitrary contents to unicode, make tests for error.py run again. --- hapi/error.py | 24 ++++++------------- hapi/test/test_error.py | 51 ++++++++++++++++++++++------------------- 2 files changed, 35 insertions(+), 40 deletions(-) diff --git a/hapi/error.py b/hapi/error.py index ab42952..a1f4d77 100644 --- a/hapi/error.py +++ b/hapi/error.py @@ -1,4 +1,4 @@ -from builtins import object +from builtins import object, str from future.utils import python_2_unicode_compatible @@ -77,22 +77,12 @@ def __unicode__(self): def _dict_vals_to_unicode(self, data): unicode_data = {} for key, val in list(data.items()): - try: - # For python2 - basestring - if not isinstance(val, basestring): - unicode_data[key] = unicode(val) - elif not isinstance(val, unicode): - unicode_data[key] = unicode(val, 'utf8', 'ignore') - else: - unicode_data[key] = val - except NameError: - # For python3 - if not isinstance(val, str): - unicode_data[key] = str(val) - else: - unicode_data[key] = val - + if isinstance(val, str): + unicode_data[key] = val + elif isinstance(val, bytes): + unicode_data[key] = val.decode('utf-8', 'ignore') + else: + unicode_data[key] = str(val) return unicode_data diff --git a/hapi/test/test_error.py b/hapi/test/test_error.py index 3af7713..79aeb04 100644 --- a/hapi/test/test_error.py +++ b/hapi/test/test_error.py @@ -1,10 +1,14 @@ from builtins import str from builtins import object + +import unittest2 + from hapi.error import HapiError, EmptyResult from nose.tools import ok_ + class MockResult(object): ''' Null Object pattern to prevent Null reference errors @@ -17,26 +21,27 @@ def __init__(self): self.reason = '' -def test_unicode_error(): - - result = MockResult() - result.body = 'A HapiException with unicode \\u8131 \xe2\x80\xa2\t' - result.reason = 'Why must everything have a reason?' - request = {} - for key in ('method', 'host', 'url', 'timeout', 'data', 'headers'): - request[key] = '' - request['url'] = 'http://adomain/with-unicode-\\u8131' - # Note the following line is missing the 'u' modifier on the string, - # this is intentional to simulate poorly formatted input that should - # still be handled without an exception - request['data'] = "A HapiException with unicode \\u8131 \xe2\x80\xa2" - request['headers'] = {'Cookie': "with unicode \\u8131 \xe2\x80\xa2"} - - exc = HapiError(result, request) - ok_(request['url'] in str(exc)) - ok_(result.reason in str(exc)) - -def test_error_with_no_result_or_request(): - exc = HapiError(None, None, 'a silly error') - ok_('a silly error' in str(exc)) - +class ErrorTest(unittest2.TestCase): + + def test_unicode_error(self): + + result = MockResult() + result.body = 'A HapiException with unicode \\u8131 \xe2\x80\xa2\t' + result.reason = 'Why must everything have a reason?' + request = {} + for key in ('method', 'host', 'url', 'timeout', 'data', 'headers'): + request[key] = '' + request['url'] = 'http://adomain/with-unicode-\\u8131' + # Note the following line is missing the 'u' modifier on the string, + # this is intentional to simulate poorly formatted input that should + # still be handled without an exception + request['data'] = "A HapiException with unicode \\u8131 \xe2\x80\xa2" + request['headers'] = {'Cookie': "with unicode \\u8131 \xe2\x80\xa2"} + + exc = HapiError(result, request) + ok_(request['url'] in str(exc)) + ok_(result.reason in str(exc)) + + def test_error_with_no_result_or_request(self): + exc = HapiError(None, None, 'a silly error') + ok_('a silly error' in str(exc)) From d5d0a2a95047c8661ab8a84cdf72ac317c9005d3 Mon Sep 17 00:00:00 2001 From: Ruichen_Wang Date: Fri, 6 Dec 2019 11:10:42 -0500 Subject: [PATCH 19/25] Modify some comments, bump internal version --- hapi/test/test_credentials.json | 2 +- hapi/test/test_credentials.json.sample | 6 ++++-- setup.py | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/hapi/test/test_credentials.json b/hapi/test/test_credentials.json index 5c9a902..dbb73f5 100644 --- a/hapi/test/test_credentials.json +++ b/hapi/test/test_credentials.json @@ -1,3 +1,3 @@ { - "api_key": "9b1a7bfc-4457-4c9b-b2a2-0cc056352a1c" + "api_key": "your_api_key_here" } \ No newline at end of file diff --git a/hapi/test/test_credentials.json.sample b/hapi/test/test_credentials.json.sample index 1a29dfe..01b9ea7 100644 --- a/hapi/test/test_credentials.json.sample +++ b/hapi/test/test_credentials.json.sample @@ -1,5 +1,7 @@ -# you may choose to override the test suite default demo key with your own, -# and you can do so by simply specifying your own test_credentials.json +# https://developers.hubspot.com/changelog/upcoming-changes-to-the-public-api-demo-account +# According to the above link, if you want to run all tests in this library, +# you need to create a HubSpot test account, get your own API test key +# and put it in test_credentials.json file. # just create a json hash with the keys/values you want passed in to the # HubSpotClient subclass constructors. Here is a sample (and remember valid diff --git a/setup.py b/setup.py index 04535e6..173de23 100755 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ setup( name='hapipy', - version='2.10.5.5', + version='2.10.5.4-bs1', description="A python wrapper around HubSpot's APIs", long_description=open('README.md').read(), author='HubSpot Dev Team', From b6d9d25f705bd72429cc33783f4d2ce610b50b70 Mon Sep 17 00:00:00 2001 From: Ruichen_Wang Date: Mon, 9 Dec 2019 15:38:40 -0500 Subject: [PATCH 20/25] Override nonzero function, use six.BytesIO for python 2/3 compatibility --- hapi/base.py | 5 +---- hapi/error.py | 2 ++ 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/hapi/base.py b/hapi/base.py index 9b3ce8b..783a0e8 100644 --- a/hapi/base.py +++ b/hapi/base.py @@ -100,10 +100,7 @@ def _create_request(self, conn, method, url, headers, data): return params def _gunzip_body(self, body): - if body and isinstance(body, bytes): - sio = io.BytesIO(body) - else: - sio = io.StringIO(body) + sio = six.BytesIO(body) gf = gzip.GzipFile(fileobj=sio, mode="rb") return gf.read() diff --git a/hapi/error.py b/hapi/error.py index a1f4d77..6fd9562 100644 --- a/hapi/error.py +++ b/hapi/error.py @@ -16,6 +16,8 @@ def __init__(self): def __bool__(self): return False + __nonzero__ = __bool__ + class HapiError(ValueError): """Any problems get thrown as HapiError exceptions with the relevant info inside""" From 6957d41a078d7147abcc264ac53e48f4312ba1b0 Mon Sep 17 00:00:00 2001 From: Ruichen_Wang Date: Tue, 10 Dec 2019 12:08:53 -0500 Subject: [PATCH 21/25] Distinguish between str and unicode, unzip gzipped body in one place, handle __str__ in HapiError class smoothly --- hapi/base.py | 20 ++++++++------------ hapi/error.py | 16 +++++++++------- hapi/test/test_contact_lists.py | 1 - hapi/test/test_error.py | 11 +++++------ setup.py | 1 - 5 files changed, 22 insertions(+), 27 deletions(-) diff --git a/hapi/base.py b/hapi/base.py index 783a0e8..e32a3e6 100644 --- a/hapi/base.py +++ b/hapi/base.py @@ -1,6 +1,6 @@ from __future__ import print_function from builtins import object -import six +from past.builtins import basestring from future.moves.urllib.parse import urlencode import http.client import simplejson as json @@ -87,7 +87,7 @@ def _prepare_request(self, subpath, params, data, opts, doseq=False, query=''): 'Accept-Encoding': 'gzip', 'Content-Type': opts.get('content_type') or 'application/json'}) - if data and not isinstance(data, six.string_types) and headers['Content-Type']=='application/json': + if data and not isinstance(data, basestring) and headers['Content-Type']=='application/json': data = json.dumps(data) return url, headers, data @@ -100,7 +100,10 @@ def _create_request(self, conn, method, url, headers, data): return params def _gunzip_body(self, body): - sio = six.BytesIO(body) + if isinstance(body, bytes): + sio = io.BytesIO(body) + else: + sio = io.StringIO(body) gf = gzip.GzipFile(fileobj=sio, mode="rb") return gf.read() @@ -115,7 +118,7 @@ def _execute_request_raw(self, conn, request): except: raise HapiTimeout(None, request, traceback.format_exc()) - encoding = [i[1] for i in result.getheaders() if i[0] == 'content-encoding'] + encoding = [i[1] for i in result.getheaders() if i[0].lower() == 'content-encoding'] result.body = self._process_body(result.read(), len(encoding) and encoding[0] == 'gzip') conn.close() @@ -135,18 +138,11 @@ def _execute_request(self, conn, request): return result.body def _digest_result(self, data): - if data and isinstance(data, six.string_types): - # This is for python 2 version, data is str + if data and isinstance(data, basestring): try: data = json.loads(data) except ValueError: pass - else: - # This is for python 3 version, data is bytes string and get Gzipped - try: - data = json.loads(gzip.GzipFile(fileobj=io.BytesIO(data)).read()) - except ValueError: - pass return data def _call_raw(self, subpath, params=None, method='GET', data=None, doseq=False, query='', retried=False, **options): diff --git a/hapi/error.py b/hapi/error.py index 6fd9562..1aa02ca 100644 --- a/hapi/error.py +++ b/hapi/error.py @@ -1,4 +1,5 @@ -from builtins import object, str +from builtins import object +from builtins import str as unicode from future.utils import python_2_unicode_compatible @@ -19,6 +20,7 @@ def __bool__(self): __nonzero__ = __bool__ +@python_2_unicode_compatible class HapiError(ValueError): """Any problems get thrown as HapiError exceptions with the relevant info inside""" @@ -59,11 +61,11 @@ def __init__(self, result, request, err=None): self.request = request self.err = err - @python_2_unicode_compatible + def __str__(self): - return self.__unicode__() + return self.__unicode_string__() - def __unicode__(self): + def __unicode_string__(self): params = {} request_keys = ('method', 'host', 'url', 'data', 'headers', 'timeout', 'body') result_attrs = ('status', 'reason', 'msg', 'body', 'headers') @@ -79,12 +81,12 @@ def __unicode__(self): def _dict_vals_to_unicode(self, data): unicode_data = {} for key, val in list(data.items()): - if isinstance(val, str): + if isinstance(val, unicode): unicode_data[key] = val elif isinstance(val, bytes): - unicode_data[key] = val.decode('utf-8', 'ignore') + unicode_data[key] = unicode(val, 'utf8', 'ignore') else: - unicode_data[key] = str(val) + unicode_data[key] = unicode(val) return unicode_data diff --git a/hapi/test/test_contact_lists.py b/hapi/test/test_contact_lists.py index a56b3be..c34c1b1 100644 --- a/hapi/test/test_contact_lists.py +++ b/hapi/test/test_contact_lists.py @@ -1,4 +1,3 @@ -from builtins import str import unittest2 import random diff --git a/hapi/test/test_error.py b/hapi/test/test_error.py index 79aeb04..571e620 100644 --- a/hapi/test/test_error.py +++ b/hapi/test/test_error.py @@ -1,5 +1,4 @@ - -from builtins import str +from builtins import str as unicode from builtins import object import unittest2 @@ -26,12 +25,12 @@ class ErrorTest(unittest2.TestCase): def test_unicode_error(self): result = MockResult() - result.body = 'A HapiException with unicode \\u8131 \xe2\x80\xa2\t' + result.body = u'A HapiException with unicode \u8131 \xe2\x80\xa2\t' result.reason = 'Why must everything have a reason?' request = {} for key in ('method', 'host', 'url', 'timeout', 'data', 'headers'): request[key] = '' - request['url'] = 'http://adomain/with-unicode-\\u8131' + request['url'] = u'http://adomain/with-unicode-\u8131' # Note the following line is missing the 'u' modifier on the string, # this is intentional to simulate poorly formatted input that should # still be handled without an exception @@ -39,9 +38,9 @@ def test_unicode_error(self): request['headers'] = {'Cookie': "with unicode \\u8131 \xe2\x80\xa2"} exc = HapiError(result, request) - ok_(request['url'] in str(exc)) + ok_(request['url'] in unicode(exc)) ok_(result.reason in str(exc)) def test_error_with_no_result_or_request(self): exc = HapiError(None, None, 'a silly error') - ok_('a silly error' in str(exc)) + ok_('a silly error' in unicode(exc)) diff --git a/setup.py b/setup.py index 173de23..0c29391 100755 --- a/setup.py +++ b/setup.py @@ -14,7 +14,6 @@ packages=['hapi', 'hapi.mixins'], install_requires=[ 'simplejson>=2.1.2', - 'six>=1.12.0', 'future>=0.18.2' ], ) From e3fe350453aa65cae27a4de6ccb670dd855ec7e8 Mon Sep 17 00:00:00 2001 From: Ruichen_Wang Date: Tue, 10 Dec 2019 13:26:05 -0500 Subject: [PATCH 22/25] Get rid of __unicode__ function for python2/3 compatibility --- hapi/error.py | 4 ---- hapi/test/test_base.py | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/hapi/error.py b/hapi/error.py index 1aa02ca..20924af 100644 --- a/hapi/error.py +++ b/hapi/error.py @@ -61,11 +61,7 @@ def __init__(self, result, request, err=None): self.request = request self.err = err - def __str__(self): - return self.__unicode_string__() - - def __unicode_string__(self): params = {} request_keys = ('method', 'host', 'url', 'data', 'headers', 'timeout', 'body') result_attrs = ('status', 'reason', 'msg', 'body', 'headers') diff --git a/hapi/test/test_base.py b/hapi/test/test_base.py index bcd532f..757bdc2 100644 --- a/hapi/test/test_base.py +++ b/hapi/test/test_base.py @@ -107,4 +107,4 @@ def test_digest_result(self): gzf.close() data = json.loads(self.client._process_body(sio.getvalue(), True)) - self.assertEqual(data.get('hello'), 'gzipped') \ No newline at end of file + self.assertEqual(data.get('hello'), 'gzipped') From da07db6a8a41cdb5b8bf24fc0565337bdcbdd667 Mon Sep 17 00:00:00 2001 From: Ruichen_Wang Date: Wed, 11 Dec 2019 11:24:46 -0500 Subject: [PATCH 23/25] Replace StringIO with six.BytesIO for python2/3 compatibility --- hapi/base.py | 7 ++----- hapi/mixins/threading.py | 12 +++++++----- hapi/test/test_base.py | 4 ++-- setup.py | 1 + 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/hapi/base.py b/hapi/base.py index e32a3e6..e2dd289 100644 --- a/hapi/base.py +++ b/hapi/base.py @@ -10,7 +10,7 @@ import time import traceback import gzip -import io +import six from .error import HapiError, HapiBadRequest, HapiNotFound, HapiTimeout, HapiServerError, HapiUnauthorized @@ -100,10 +100,7 @@ def _create_request(self, conn, method, url, headers, data): return params def _gunzip_body(self, body): - if isinstance(body, bytes): - sio = io.BytesIO(body) - else: - sio = io.StringIO(body) + sio = six.BytesIO(body) gf = gzip.GzipFile(fileobj=sio, mode="rb") return gf.read() diff --git a/hapi/mixins/threading.py b/hapi/mixins/threading.py index 858cd94..a68fec7 100644 --- a/hapi/mixins/threading.py +++ b/hapi/mixins/threading.py @@ -7,7 +7,9 @@ ''' from builtins import object -import pycurl, io +import pycurl +import six + class HapiThreadedError(ValueError): def __init__(self, curl): @@ -75,8 +77,8 @@ def _create_curl(self, url, headers, data): c.data = data c.status = -1 - c.body = io.StringIO() - c.response_headers = io.StringIO() + c.body = six.BytesIO() + c.response_headers = six.BytesIO() c.setopt(c.URL, c.full_url) c.setopt(c.TIMEOUT, self.options['timeout']) @@ -87,7 +89,7 @@ def _create_curl(self, url, headers, data): c.setopt(c.HTTPHEADER, [ "%s: %s" % (x, y) for x, y in list(headers.items()) ]) if data: - c.data_out = io.StringIO(data) + c.data_out = six.BytesIO(data) c.setopt(c.READFUNCTION, c.data_out.getvalue) return c @@ -121,7 +123,7 @@ def process_queue(self): for c in m.handles: c.status = c.getinfo(c.HTTP_CODE) if 'Content-Encoding: gzip' in c.response_headers.getvalue(): - c.body = io.StringIO(self._gunzip_body(c.body.getvalue())) + c.body = six.BytesIO(self._gunzip_body(c.body.getvalue())) result = { "data" : self._digest_result(c.body.getvalue()), "code": c.status } if not c.status or c.status >= 400: # Don't throw the exception because some might have succeeded diff --git a/hapi/test/test_base.py b/hapi/test/test_base.py index 757bdc2..7274cd3 100644 --- a/hapi/test/test_base.py +++ b/hapi/test/test_base.py @@ -3,7 +3,7 @@ from collections import defaultdict import unittest2 import simplejson as json -from io import StringIO, BytesIO +import six from gzip import GzipFile from hapi.base import BaseClient @@ -101,7 +101,7 @@ def test_digest_result(self): self.assertEqual(data.get('hello'), 'json') # Write our data into a gzipped stream - sio = BytesIO() + sio = six.BytesIO() gzf = GzipFile(fileobj=sio, mode='wb') gzf.write(b'{"hello": "gzipped"}') gzf.close() diff --git a/setup.py b/setup.py index 0c29391..173de23 100755 --- a/setup.py +++ b/setup.py @@ -14,6 +14,7 @@ packages=['hapi', 'hapi.mixins'], install_requires=[ 'simplejson>=2.1.2', + 'six>=1.12.0', 'future>=0.18.2' ], ) From d4b1a589b7ae8ad8974f5774e5b7d779424df116 Mon Sep 17 00:00:00 2001 From: Leonid Belyi Date: Tue, 17 Dec 2019 11:48:08 -0500 Subject: [PATCH 24/25] Change version name to comply with pep 0440 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 173de23..5d0dbb7 100755 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ setup( name='hapipy', - version='2.10.5.4-bs1', + version='2.10.5.4+bs1', description="A python wrapper around HubSpot's APIs", long_description=open('README.md').read(), author='HubSpot Dev Team', From 8302062c1804d37479970e6465621bdbd5c73acb Mon Sep 17 00:00:00 2001 From: Rita Soldi Date: Tue, 14 Feb 2023 10:40:40 +0000 Subject: [PATCH 25/25] Update hubspot api to use access token in header --- hapi/base.py | 7 ++++--- hapi/test/test_base.py | 19 ++++++++++--------- setup.py | 2 +- 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/hapi/base.py b/hapi/base.py index e2dd289..fd8a786 100644 --- a/hapi/base.py +++ b/hapi/base.py @@ -59,6 +59,7 @@ def _get_path(self, subpath): raise Exception("Unimplemented get_path for BaseClient subclass!") def _prepare_request_auth(self, subpath, params, data, opts): + headers = opts.get('headers', {}) if opts else {} if self.api_key: params['hapikey'] = params.get('hapikey') or self.api_key else: @@ -67,11 +68,12 @@ def _prepare_request_auth(self, subpath, params, data, opts): # but one was provided as part of the method invocation, we persist it if params.get('access_token') and not self.access_token: self.access_token = params.get('access_token') - params['access_token'] = self.access_token + headers.update({"Authorization": "Bearer %s" % self.access_token}) + return headers def _prepare_request(self, subpath, params, data, opts, doseq=False, query=''): params = params or {} - self._prepare_request_auth(subpath, params, data, opts) + headers = self._prepare_request_auth(subpath, params, data, opts) if opts.get('hub_id') or opts.get('portal_id'): params['portalId'] = opts.get('hub_id') or opts.get('portal_id') @@ -82,7 +84,6 @@ def _prepare_request(self, subpath, params, data, opts, doseq=False, query=''): if query and not query.startswith('&'): query = '&' + query url = opts.get('url') or '/%s?%s%s' % (self._get_path(subpath), urlencode(params, doseq), query) - headers = opts.get('headers') or {} headers.update({ 'Accept-Encoding': 'gzip', 'Content-Type': opts.get('content_type') or 'application/json'}) diff --git a/hapi/test/test_base.py b/hapi/test/test_base.py index 7274cd3..0372d59 100644 --- a/hapi/test/test_base.py +++ b/hapi/test/test_base.py @@ -27,7 +27,7 @@ def getheaders(self): class BaseTest(unittest2.TestCase): def setUp(self): - self.client = TestBaseClient('unit_api_key') + self.client = TestBaseClient(access_token='client_access_token') def tearDown(self): pass @@ -43,18 +43,19 @@ def test_prepare_request(self): # so duplicate=[key,value] url, headers, data = self.client._prepare_request(subpath, params, data, opts, doseq) self.assertTrue('duplicate=%5B%27key%27%2C+%27value%27%5D' in url) + self.assertEqual(headers['Authorization'], 'Bearer client_access_token') # with doseq=True the values will be split and assigned their own key # so duplicate=key&duplicate=value doseq = True url, headers, data = self.client._prepare_request(subpath, params, data, opts, doseq) - print(url) self.assertTrue('duplicate=key&duplicate=' in url) - + self.assertEqual(headers['Authorization'], 'Bearer client_access_token') + def test_call(self): client = TestBaseClient('key', api_base='base', env='hudson') client.sleep_multiplier = .02 - client._create_request = lambda *args:None + client._create_request = lambda *args: None counter = dict(count=0) args = ('/my-api-path', {'bebop': 'rocksteady'}) @@ -63,9 +64,10 @@ def test_call(self): def execute_request_with_retries(a, b): counter['count'] += 1 if counter['count'] < 2: - raise HapiError(defaultdict(str), defaultdict(str)) + raise HapiError(defaultdict(str), defaultdict(str)) else: return TestResult(body='SUCCESS') + client._execute_request_raw = execute_request_with_retries # This should fail once, and then succeed @@ -73,12 +75,11 @@ def execute_request_with_retries(a, b): self.assertEqual(2, counter['count']) self.assertEqual('SUCCESS', result) - - def execute_request_failed(a, b): - raise HapiError(defaultdict(str), defaultdict(str)) + raise HapiError(defaultdict(str), defaultdict(str)) + + # This should fail and retry and still fail - # This should fail and retry and still fail client._execute_request_raw = execute_request_failed raised_error = False try: diff --git a/setup.py b/setup.py index 5d0dbb7..48ceb46 100755 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ setup( name='hapipy', - version='2.10.5.4+bs1', + version='2.10.5.4+bs2', description="A python wrapper around HubSpot's APIs", long_description=open('README.md').read(), author='HubSpot Dev Team',