diff --git a/CHANGELOG.rst b/CHANGELOG.rst new file mode 100644 index 0000000..e69de29 diff --git a/README.rst b/README.rst index d33715c..9dfc24d 100644 --- a/README.rst +++ b/README.rst @@ -16,14 +16,12 @@ Or can be installed using easy_install:: $ easy_install aweber_api - - Usage ===== To connect the AWeber API Python Libray, you simply include the main class, AWeberAPI in your application, then create an instace of it with your -application's consumer key and secret:: +application's consumer key and secret.:: from aweber_api import AWeberAPI aweber = AWeberAPI(consumer_key, consumer_secret) @@ -32,6 +30,38 @@ application's consumer key and secret:: for list in account.lists: print list.name +Handling Errors ++++++++++++++++ + +Sometimes errors happen and your application should handle them appropriately. +Whenever an API error occurs an AWeberAPIException will be raised with a +detailed error message and documentation link to explain whats wrong. + +You should wrap any calls to the API in a try/except block. + +Common Errors: + * Resource not found (404 error) + * Your application has been rate limited (403 error) + * Bad request (400 error) + * API Temporarily unavailable (503 error) + +Refer to https://labs.aweber.com/docs/troubleshooting for the complete list:: + + from aweber_api import AWeberAPI, APIException + aweber = AWeberAPI(consumer_key, consumer_secret) + account = aweber.get_account(access_token, token_secret) + + + try: + invalid_resource = account.load_from_url('/idontexist') + except APIException, exc: + print '404! {0}'.format(exc) + + try: + print len(account.lists) + except APIException, exc: + print 'hmm, something unexpected happened!: {0}'.format(exc) + Getting request tokens / access tokens ++++++++++++++++++++++++++++++++++++++ diff --git a/aweber_api/__init__.py b/aweber_api/__init__.py index 3d0717f..c709046 100644 --- a/aweber_api/__init__.py +++ b/aweber_api/__init__.py @@ -1,12 +1,13 @@ from urlparse import parse_qs from aweber_api.base import (AWeberBase, API_BASE, ACCESS_TOKEN_URL, - REQUEST_TOKEN_URL, AUTHORIZE_URL) + REQUEST_TOKEN_URL, AUTHORIZE_URL, APIException) from aweber_api.collection import AWeberCollection from aweber_api.entry import AWeberEntry from aweber_api.oauth import OAuthAdapter from aweber_api.response import AWeberResponse + class AWeberAPI(AWeberBase): """ Base class for connecting to the AWeberAPI. Created with a consumer key and secret, then used to either generate tokens for authorizing a user, or @@ -83,6 +84,7 @@ def get_account(self, access_token=False, token_secret=False): accounts = self._read_response(url, response) return accounts[0] + class AWeberUser(object): """ Simple data storage object representing the user in the OAuth model. Has @@ -96,4 +98,3 @@ class AWeberUser(object): def get_highest_priority_token(self): return self.access_token or self.request_token - diff --git a/aweber_api/base.py b/aweber_api/base.py index 8f90ad3..7225dfa 100644 --- a/aweber_api/base.py +++ b/aweber_api/base.py @@ -4,6 +4,11 @@ REQUEST_TOKEN_URL = 'https://auth.aweber.com/1.0/oauth/request_token' AUTHORIZE_URL = 'https://auth.aweber.com/1.0/oauth/authorize' + +class APIException(Exception): + """APIExceptions.""" + + class AWeberBase(object): """ Provides functionality shared accross all AWeber objects diff --git a/aweber_api/collection.py b/aweber_api/collection.py index 5073a68..7be881f 100644 --- a/aweber_api/collection.py +++ b/aweber_api/collection.py @@ -1,9 +1,9 @@ from math import floor from urlparse import parse_qs from urllib import urlencode -from aweber_api.response import AWeberResponse -from aweber_api.base import API_BASE + import aweber_api +from aweber_api.response import AWeberResponse class AWeberCollection(AWeberResponse): @@ -45,7 +45,11 @@ def _load_page_for_offset(self, offset): self._key_entries(response) def _get_page_params(self, offset): - next_link = self._data.get('next_collection_link', '') + next_link = self._data.get('next_collection_link', None) + if next_link is None: + """no more parameters in page!""" + raise StopIteration + url, query = next_link.split('?') query_parts = parse_qs(query) self.page_size = int(query_parts['ws.size'][0]) @@ -62,7 +66,7 @@ def create(self, **kwargs): resource_url = response['location'] data = self.adapter.request('GET', resource_url) - return aweber_api.entry.AWeberEntry(resource_url, data, self.adapter) + return aweber_api.AWeberEntry(resource_url, data, self.adapter) def find(self, **kwargs): params = {'ws.op': 'find'} @@ -70,12 +74,8 @@ def find(self, **kwargs): query_string = urlencode(params) url = '{0.url}?{1}'.format(self, query_string) data = self.adapter.request('GET', url) - try: - collection = AWeberCollection(url, data, self.adapter) - except TypeError: - return False - # collections return total_size_link + collection = AWeberCollection(url, data, self.adapter) collection._data['total_size'] = self._get_total_size(url) return collection @@ -84,29 +84,30 @@ def _get_total_size(self, uri, **kwargs): total_size_uri = '{0}&ws.show=total_size'.format(uri) return self.adapter.request('GET', total_size_uri) - # This method gets a collection's parent entry - # Or returns None if no parent entry def get_parent_entry(self): - from aweber_api.entry import AWeberEntry + """Return a collection's parent entry or None.""" url_parts = self.url.split('/') + #If top of tree - no parent entry if len(url_parts) <= 3: return None size = len(url_parts) + #Remove collection id and slash from end of url url = self.url[:-len(url_parts[size-1])-1] data = self.adapter.request('GET', url) try: - entry = AWeberEntry(url, data, self.adapter) + entry = aweber_api.AWeberEntry(url, data, self.adapter) except TypeError: - return False + return None + return entry def _create_entry(self, offset): - from aweber_api.entry import AWeberEntry data = self._entry_data[offset] - url = data['self_link'].replace(API_BASE, '') - self._entries[offset] = AWeberEntry(url, data, self.adapter) + url = data['self_link'].replace(aweber_api.API_BASE, '') + self._entries[offset] = aweber_api.AWeberEntry(url, data, + self.adapter) def __len__(self): return self.total_size diff --git a/aweber_api/entry.py b/aweber_api/entry.py index 02fc288..bd43dcf 100644 --- a/aweber_api/entry.py +++ b/aweber_api/entry.py @@ -1,6 +1,7 @@ +import aweber_api from aweber_api.response import AWeberResponse from aweber_api.data_dict import DataDict -from aweber_api import AWeberCollection + from urllib import urlencode @@ -39,10 +40,8 @@ def delete(self): https://labs.aweber.com/docs/reference/1.0 for more details on which entry resources may be deleted. """ - status = self.adapter.request('DELETE', self.url, response='status') - if str(status)[:2] == '20': - return True - return False + self.adapter.request('DELETE', self.url, response='status') + return True def move(self, list_): """Invoke the API method to MOVE an entry resource to a @@ -58,20 +57,16 @@ def move(self, list_): 'list_link': list_.self_link} response = self.adapter.request('POST', self.url, params, response='headers') - if response['status'] != '201': - return False + new_resource = response['location'] self._diff = {} self._data = self.adapter.request('GET', new_resource) return True def save(self): - response = self.adapter.request('PATCH', self.url, self._diff, - response='status') + self.adapter.request('PATCH', self.url, self._diff, response='status') self._diff = {} - if str(response)[:2] == '20': - return True - return False + return True def get_activity(self): """Invoke the API method to return all Subscriber activity. @@ -86,12 +81,8 @@ def get_activity(self): query_string = urlencode(params) url = '{0.url}?{1}'.format(self, query_string) data = self.adapter.request('GET', url) - try: - collection = AWeberCollection(url, data, self.adapter) - except TypeError: - return False - # collections return total_size_link + collection = aweber_api.AWeberCollection(url, data, self.adapter) collection._data['total_size'] = self._get_total_size(url) return collection @@ -109,13 +100,9 @@ def findSubscribers(self, **kwargs): params.update(kwargs) query_string = urlencode(params) url = '{0.url}?{1}'.format(self, query_string) - data = self.adapter.request('GET', url) - try: - collection = AWeberCollection(url, data, self.adapter) - except TypeError: - return False - # collections return total_size_link + data = self.adapter.request('GET', url) + collection = aweber_api.AWeberCollection(url, data, self.adapter) collection._data['total_size'] = self._get_total_size(url) return collection diff --git a/aweber_api/oauth.py b/aweber_api/oauth.py index 9ff4454..c34835f 100644 --- a/aweber_api/oauth.py +++ b/aweber_api/oauth.py @@ -2,6 +2,9 @@ import json from urllib import urlencode +import aweber_api + + class OAuthAdapter(object): def __init__(self, key, secret, base): @@ -53,7 +56,8 @@ def request(self, method, url, data={}, response='body'): error = content.get('error', {}) error_type = error.get('type') error_msg = error.get('message') - raise Exception('{0}: {1}'.format(error_type, error_msg)) + raise aweber_api.base.APIException( + '{0}: {1}'.format(error_type, error_msg)) if response == 'body' and isinstance(content, str): return self._parse(content) diff --git a/aweber_api/response.py b/aweber_api/response.py index fcc6121..8d574cc 100644 --- a/aweber_api/response.py +++ b/aweber_api/response.py @@ -1,5 +1,6 @@ from aweber_api import AWeberBase + class AWeberResponse(AWeberBase): def __init__(self, url, data, adapter): diff --git a/setup.py b/setup.py index 8a07b29..a51d528 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ setup( name='aweber_api', - version='1.0.4', + version='1.1.0', packages=find_packages(exclude=['tests']), url='https://github.com/aweber/AWeber-API-Python-Library', install_requires = ['oauth2 >= 1.2'], diff --git a/tests/mock_adapter.py b/tests/mock_adapter.py index 111e1cf..81c0cd2 100644 --- a/tests/mock_adapter.py +++ b/tests/mock_adapter.py @@ -58,7 +58,7 @@ 'location': '/accounts/1/lists/505454/subscribers/3'}, None), }, 'PATCH' : { - '/accounts/1/lists/303449/subscribers/1': ({}, None), + '/accounts/1/lists/303449/subscribers/1': ({'status': '209'}, None), '/accounts/1/lists/303449/subscribers/2': ({'status': '400'}, 'error'), }, 'DELETE' : { diff --git a/tests/test_aweber_collection.py b/tests/test_aweber_collection.py index b20f5fa..2fdd767 100644 --- a/tests/test_aweber_collection.py +++ b/tests/test_aweber_collection.py @@ -1,6 +1,6 @@ from unittest import TestCase from aweber_api import AWeberAPI, AWeberCollection, AWeberEntry -from aweber_api.base import API_BASE +from aweber_api.base import API_BASE, APIException from mock_adapter import MockAdapter @@ -56,7 +56,7 @@ def test_should_support_find_method(self): def test_find_should_handle_errors(self): base_url = '/accounts/1/lists/303449/subscribers' subscriber_collection = self.aweber.load_from_url(base_url) - self.assertRaises(Exception, subscriber_collection.find, name='joe') + self.assertRaises(APIException, subscriber_collection.find, name='joe') def test_should_create_entries_with_correct_url(self): base_url = '/accounts/1' @@ -76,7 +76,7 @@ def setUp(self): self.aweber.adapter.requests = [] def test_should_raise_exception(self): - self.assertRaises(Exception, self.cf.create, name='Duplicate Name') + self.assertRaises(APIException, self.cf.create, name='Duplicate Name') class TestCreatingCustomFields(TestCase): diff --git a/tests/test_aweber_entry.py b/tests/test_aweber_entry.py index 1280161..aab7e50 100644 --- a/tests/test_aweber_entry.py +++ b/tests/test_aweber_entry.py @@ -3,6 +3,7 @@ from urllib import urlencode from aweber_api import AWeberAPI, AWeberCollection, AWeberEntry +from aweber_api.base import APIException from mock_adapter import MockAdapter @@ -29,7 +30,7 @@ def test_should_have_child_collections(self): def test_findSubscribers_should_handle_errors(self): account = self.aweber.load_from_url('/accounts/1') - self.assertRaises(Exception, account.findSubscribers, name='bob') + self.assertRaises(APIException, account.findSubscribers, name='bob') class AccountTestCase(TestCase): @@ -227,7 +228,7 @@ def setUp(self): self.subscriber.custom_fields['New Custom Field'] = 'Cookies' def test_save_failed(self): - self.assertRaises(Exception, self.subscriber.save) + self.assertRaises(APIException, self.subscriber.save) class TestDeletingSubscriberData(SubscriberTestCase): @@ -257,7 +258,7 @@ def setUp(self): self.subscriber = self.aweber.load_from_url(sub_url) def test_should_raise_exception_when_failing(self): - self.assertRaises(Exception, self.subscriber.delete) + self.assertRaises(APIException, self.subscriber.delete) class TestGettingParentEntry(TestCase): @@ -267,8 +268,6 @@ def setUp(self): self.aweber.adapter = MockAdapter() self.list = self.aweber.load_from_url('/accounts/1/lists/303449') self.account = self.aweber.load_from_url('/accounts/1') - #print self.account._data - #1/0 self.custom_field = self.aweber.load_from_url('/accounts/1/lists/303449/custom_fields/1') def test_should_be_able_get_parent_entry(self):