diff --git a/.gitignore b/.gitignore index 9414a9d..6d422cf 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,5 @@ dist /build MANIFEST +.idea +.DS_Store diff --git a/hapi/base.py b/hapi/base.py index 6298a44..fd8a786 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 +from past.builtins import basestring +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 six -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] @@ -56,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: @@ -64,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') @@ -78,8 +83,7 @@ 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) - headers = opts.get('headers') or {} + url = opts.get('url') or '/%s?%s%s' % (self._get_path(subpath), urlencode(params, doseq), query) headers.update({ 'Accept-Encoding': 'gzip', 'Content-Type': opts.get('content_type') or 'application/json'}) @@ -97,7 +101,7 @@ def _create_request(self, conn, method, url, headers, data): return params def _gunzip_body(self, body): - sio = StringIO.StringIO(body) + sio = six.BytesIO(body) gf = gzip.GzipFile(fileobj=sio, mode="rb") return gf.read() @@ -112,7 +116,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() @@ -137,7 +141,6 @@ def _digest_result(self, data): data = json.loads(data) except ValueError: pass - return data def _call_raw(self, subpath, params=None, method='GET', data=None, doseq=False, query='', retried=False, **options): @@ -167,7 +170,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 +179,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 +191,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 new file mode 100644 index 0000000..11f5fcb --- /dev/null +++ b/hapi/contact_lists.py @@ -0,0 +1,51 @@ +from .base import BaseClient +from . 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): + """ 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 {} + 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 new file mode 100644 index 0000000..512a5b4 --- /dev/null +++ b/hapi/contacts.py @@ -0,0 +1,44 @@ +from .base import BaseClient +from . import logging_helper +from future.moves.urllib.parse import quote + + +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): + """ Creates or Updates a client with the supplied data. """ + data = data or {} + 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('contact/email/{email}/profile'. + format(email=quote(email.encode('utf-8'))), + 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/error.py b/hapi/error.py index efd6859..20924af 100644 --- a/hapi/error.py +++ b/hapi/error.py @@ -1,3 +1,8 @@ +from builtins import object +from builtins import str as unicode +from future.utils import python_2_unicode_compatible + + class EmptyResult(object): ''' Null Object pattern to prevent Null reference errors @@ -9,10 +14,13 @@ def __init__(self): self.msg = '' self.reason = '' - def __nonzero__(self): + def __bool__(self): return False + __nonzero__ = __bool__ + +@python_2_unicode_compatible class HapiError(ValueError): """Any problems get thrown as HapiError exceptions with the relevant info inside""" @@ -42,7 +50,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: @@ -55,10 +62,6 @@ def __init__(self, result, request, err=None): self.err = err def __str__(self): - return self.__unicode__().encode('ascii', 'replace') - - - def __unicode__(self): params = {} request_keys = ('method', 'host', 'url', 'data', 'headers', 'timeout', 'body') result_attrs = ('status', 'reason', 'msg', 'body', 'headers') @@ -73,29 +76,32 @@ 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): + for key, val in list(data.items()): + if isinstance(val, unicode): + unicode_data[key] = val + elif isinstance(val, bytes): unicode_data[key] = unicode(val, 'utf8', 'ignore') else: - unicode_data[key] = val + unicode_data[key] = unicode(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/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..a68fec7 100644 --- a/hapi/mixins/threading.py +++ b/hapi/mixins/threading.py @@ -6,7 +6,10 @@ [here for python 2.5](http://www.lfd.uci.edu/~gohlke/pythonlibs/#pycurl). ''' -import pycurl, cStringIO +from builtins import object +import pycurl +import six + class HapiThreadedError(ValueError): def __init__(self, curl): @@ -74,8 +77,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 = six.BytesIO() + c.response_headers = six.BytesIO() c.setopt(c.URL, c.full_url) c.setopt(c.TIMEOUT, self.options['timeout']) @@ -83,10 +86,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 = six.BytesIO(data) c.setopt(c.READFUNCTION, c.data_out.getvalue) return c @@ -120,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 = cStringIO.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/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..0372d59 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 +import six 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): @@ -25,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 @@ -41,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'}) @@ -61,22 +64,22 @@ 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 result = client._call(*args, **kwargs) - self.assertEquals(2, counter['count']) - self.assertEquals('SUCCESS', result) - - + 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: @@ -91,18 +94,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 = six.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') 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 new file mode 100644 index 0000000..c34c1b1 --- /dev/null +++ b/hapi/test/test_contact_lists.py @@ -0,0 +1,90 @@ +import unittest2 +import random + +from nose.plugins.attrib import attr + +from . import helper +from hapi.contact_lists import ContactListsClient +from hapi.contacts import ContactsClient +from .test_contacts import ContactsClientTestCase + + +class ContactsListsClientTestCase(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_contact_lists(self): + """ Test that the get contact lists endpoint is valid. """ + 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) + + 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']) + diff --git a/hapi/test/test_contacts.py b/hapi/test/test_contacts.py new file mode 100644 index 0000000..3fbe6c5 --- /dev/null +++ b/hapi/test/test_contacts.py @@ -0,0 +1,116 @@ +import unittest2 + +from faker import Faker +from nose.plugins.attrib import attr + +from . 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/hapi/test/test_credentials.json b/hapi/test/test_credentials.json new file mode 100644 index 0000000..dbb73f5 --- /dev/null +++ b/hapi/test/test_credentials.json @@ -0,0 +1,3 @@ +{ + "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/hapi/test/test_error.py b/hapi/test/test_error.py index e206657..571e620 100644 --- a/hapi/test/test_error.py +++ b/hapi/test/test_error.py @@ -1,8 +1,13 @@ +from builtins import str as unicode +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 @@ -15,26 +20,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'] = 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 - 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_(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)) - +class ErrorTest(unittest2.TestCase): + + def test_unicode_error(self): + + result = MockResult() + 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'] = 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 + 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_(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 unicode(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() diff --git a/requirements.pip b/requirements.pip index 065097c..25096ca 100644 --- a/requirements.pip +++ b/requirements.pip @@ -1,5 +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 diff --git a/setup.py b/setup.py index b28e2f0..48ceb46 100755 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ setup( name='hapipy', - version='2.10.4', + version='2.10.5.4+bs2', description="A python wrapper around HubSpot's APIs", long_description=open('README.md').read(), author='HubSpot Dev Team', @@ -13,8 +13,8 @@ license='LICENSE.txt', packages=['hapi', 'hapi.mixins'], install_requires=[ - 'nose==1.1.2', - 'unittest2==0.5.1', - 'simplejson>=2.1.2' + 'simplejson>=2.1.2', + 'six>=1.12.0', + 'future>=0.18.2' ], )