diff --git a/aweber_api/__init__.py b/aweber_api/__init__.py index 4c38221..2799007 100644 --- a/aweber_api/__init__.py +++ b/aweber_api/__init__.py @@ -1,7 +1,13 @@ from urlparse import parse_qs -from aweber_api.base import (AWeberBase, API_BASE, ACCESS_TOKEN_URL, - REQUEST_TOKEN_URL, AUTHORIZE_URL, APIException) +from aweber_api.base import ( + ACCESS_TOKEN_URL, + APIException, + API_BASE, + AUTHORIZE_URL, + AWeberBase, + REQUEST_TOKEN_URL, +) from aweber_api.collection import AWeberCollection from aweber_api.entry import AWeberEntry from aweber_api.oauth import OAuthAdapter @@ -9,9 +15,13 @@ 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 - can be provided tokens and used to access that user's resources. """ + """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 can be provided tokens and used to + access that user's resources. + + """ def __init__(self, consumer_key, consumer_secret): self.adapter = OAuthAdapter(consumer_key, consumer_secret, API_BASE) @@ -19,25 +29,19 @@ def __init__(self, consumer_key, consumer_secret): @classmethod def parse_authorization_code(cls, authorization_code): - """ - Class method to exchange an authorization code for new api keys. - Returns a tuple containing the new consumer key/secret and access - token key/secret. + """Exchange an authorization code for new api keys. + + Returns a tuple containing the new consumer key/secret and + access token key/secret. + """ # parse and validate authorization code - keys = authorization_code.split('|') - if len(keys) < 5: - raise APIException('Invalid Authorization Code') - - # create an instance of AWeberAPI for getting the access token + keys = cls._parse_and_validate_authorization_code(authorization_code) consumer_key = keys[0] consumer_secret = keys[1] - instance = cls(consumer_key, consumer_secret) - # set request token and verifier code - instance.user.request_token = keys[2] - instance.user.token_secret = keys[3] - instance.user.verifier = keys[4] + # create an instance of AWeberAPI for getting the access token + instance = cls._create_new_instance(keys) # exchange request token for an access token access_key, access_secret = instance.get_access_token() @@ -45,84 +49,125 @@ def parse_authorization_code(cls, authorization_code): # return consumer key/secret and access token key/secret return consumer_key, consumer_secret, access_key, access_secret + @classmethod + def _parse_and_validate_authorization_code(cls, authorization_code): + """parse and validate authorization code.""" + keys = authorization_code.split('|') + if len(keys) < 5: + raise APIException('Invalid Authorization Code') + + return keys + + @classmethod + def _create_new_instance(cls, keys): + """Create an instance of AWeberAPI for getting the access token.""" + instance = cls(keys[0], keys[1]) + instance.user.request_token = keys[2] + instance.user.token_secret = keys[3] + instance.user.verifier = keys[4] + + return instance + @property def authorize_url(self): - """ - Returns the authorize url, potentially containing the request token - parameter + """Return the authorize url. + + Potentially containing the request token parameter. + """ if self.user.request_token: - return "{0}?oauth_token={1}".format(AUTHORIZE_URL, - self.user.request_token) + return "{0}?oauth_token={1}".format( + AUTHORIZE_URL, self.user.request_token) + return AUTHORIZE_URL def get_request_token(self, callback_url): + """Get a new request token / token secret for the callback url. + + Returns request token / secret, and sets properties on the + AWeberUser object (self.user). + """ - Gets a new request token / token secret for the given callback URL - and the current consumer. Returns token / secret, and sets properties - on the AWeberUser object (self.user) - """ - data = { 'oauth_callback' : callback_url } - response = self.adapter.request('POST', - REQUEST_TOKEN_URL, - data) - self.user.request_token, self.user.token_secret = self.\ - _parse_token_response(response) + data = {'oauth_callback': callback_url} + response = self.adapter.request( + 'POST', REQUEST_TOKEN_URL, data) + self.user.request_token, self.user.token_secret = ( + self._parse_token_response(response)) + return (self.user.request_token, self.user.token_secret) def get_access_token(self): + """Exchange request tokens for Access tokens. + + Gets an access token for the combination of + * request token + * token secret + * verifier + in the AWeberUser object at self.user. + + Updates the user object and returns the tokens. + """ - Gets an access token for the given request token / token secret / - verifier combination in the AWeberUser object at self.user - Updates the user object and returns the tokens - """ + data = {'oauth_verifier': self.user.verifier} + response = self.adapter.request( + 'POST', ACCESS_TOKEN_URL, data) + self.user.access_token, self.user.token_secret = ( + self._parse_token_response(response)) - data = { 'oauth_verifier' : self.user.verifier } - response = self.adapter.request('POST', - ACCESS_TOKEN_URL, - data) - self.user.access_token, self.user.token_secret = self.\ - _parse_token_response(response) return (self.user.access_token, self.user.token_secret) def _parse_token_response(self, response): - if not type(response) == str: + """Parses token response. + + Return the token key and the token secret + + """ + if not isinstance(response, str): raise TypeError('Expected response to be a string') data = parse_qs(response) - if not 'oauth_token' in data and not 'oauth_token_secret' in data: + if (data.get('oauth_token') is None) or ( + data.get('oauth_token_secret') is None): raise ValueError('OAuth parameters not returned') + return (data['oauth_token'][0], data['oauth_token_secret'][0]) def get_account(self, access_token=False, token_secret=False): - """ - Returns the AWeberEntry object for the account specified by the - access_token and token_secret currently in the self.user object. - Optionally, access_token and token_secret can be provided to replace - the properties in self.user.access_token and self.user.token_secret, - respectively. + """Returns the AWeberEntry object for the account. + + Specified by the access_token and token_secret currently + in the self.user object. + + Optionally, access_token and token_secret can be provided to + replace the properties in self.user.access_token and + self.user.token_secret, respectively. + """ if access_token: self.user.access_token = access_token if token_secret: self.user.token_secret = token_secret + url = '/accounts' response = self.adapter.request('GET', url) accounts = self._read_response(url, response) + return accounts[0] class AWeberUser(object): - """ - Simple data storage object representing the user in the OAuth model. Has - properties for request_token, token_secret, access_token, and verifier. - """ + """Data storage object representing the user in the OAuth model. + + Has properties for request_token, token_secret, access_token, and + verifier. + """ request_token = None token_secret = None access_token = None verifier = None def get_highest_priority_token(self): + """Return either the access token or the request token.""" return self.access_token or self.request_token diff --git a/aweber_api/base.py b/aweber_api/base.py index 2bb2c84..799f1ba 100644 --- a/aweber_api/base.py +++ b/aweber_api/base.py @@ -1,8 +1,7 @@ -API_BASE = 'https://api.aweber.com/1.0' - ACCESS_TOKEN_URL = 'https://auth.aweber.com/1.0/oauth/access_token' -REQUEST_TOKEN_URL = 'https://auth.aweber.com/1.0/oauth/request_token' +API_BASE = 'https://api.aweber.com/1.0' AUTHORIZE_URL = 'https://auth.aweber.com/1.0/oauth/authorize' +REQUEST_TOKEN_URL = 'https://auth.aweber.com/1.0/oauth/request_token' class APIException(Exception): @@ -10,25 +9,28 @@ class APIException(Exception): class AWeberBase(object): - """ - Provides functionality shared accross all AWeber objects - """ + """Provides functionality shared accross all AWeber objects""" collections_map = { - 'account' : ['lists', 'integrations'], - 'broadcast_campaign' : ['links', 'messages', 'stats'], + 'account': ['lists', 'integrations'], + 'broadcast_campaign': ['links', 'messages', 'stats'], 'component': [], 'custom_field': [], - 'followup_campaign' : ['links', 'messages', 'stats'], + 'followup_campaign': ['links', 'messages', 'stats'], 'integration': [], - 'link' : ['clicks'], - 'list' : ['campaigns', 'custom_fields', 'subscribers', - 'web_forms', 'web_form_split_tests'], - 'message' : ['opens', 'tracked_events'], + 'link': ['clicks'], + 'list': [ + 'campaigns', + 'custom_fields', + 'subscribers', + 'web_forms', + 'web_form_split_tests', + ], + 'message': ['opens', 'tracked_events'], 'service-root': 'accounts', - 'subscriber' : [], + 'subscriber': [], 'tracked_events': [], 'web_form': [], - 'web_form_split_test' : ['components'] + 'web_form_split_test': ['components'], } @property @@ -36,9 +38,7 @@ def user(self): return self.adapter.user def load_from_url(self, url): - """ - Gets an AWeberCollection or AWeberEntry from the given URL. - """ + """Gets an AWeberCollection or AWeberEntry from a given URL.""" response = self.adapter.request('GET', url) return self._read_response(url, response) @@ -50,15 +50,39 @@ def _read_response(self, url, response): if 'entries' in response: from aweber_api.collection import AWeberCollection return AWeberCollection(url, response, self.adapter) + if 'resource_type_link' in response: from aweber_api.entry import AWeberEntry return AWeberEntry(url, response, self.adapter) + raise TypeError('Unknown value returned') def _parseNamedOperation(self, data): from aweber_api.entry import AWeberEntry entries = [] for item in data: - entries.append(AWeberEntry(item['self_link'].replace(API_BASE, ''), - item, self.adapter)) + entries.append( + AWeberEntry( + item['self_link'].replace(API_BASE, ''), + item, + self.adapter, + ) + ) return entries + + def _partition_url(self): + try: + url_parts = self.url.split('/') + #If top of tree - no parent entry + if len(url_parts) <= 3: + return None + + except AttributeError: + return None + + return url_parts + + def _construct_parent_url(self, url_parts, child_position): + """Remove collection id and slash from end of url.""" + url = '/'.join(url_parts[:-child_position]) + return url diff --git a/aweber_api/collection.py b/aweber_api/collection.py index 2e23a73..b176e69 100644 --- a/aweber_api/collection.py +++ b/aweber_api/collection.py @@ -2,41 +2,47 @@ from urlparse import parse_qs from urllib import urlencode -import aweber_api +from aweber_api.base import API_BASE +from aweber_api.entry import AWeberEntry from aweber_api.response import AWeberResponse class AWeberCollection(AWeberResponse): - """ - Represents a collection of similar objects. Encapsulates data that is - found at the base URI's for a given object type, ie: + """Represents a collection of similar objects. + + Encapsulates data that is found at the base URI's for a given object + type, ie: /accounts /accounts/XXX/lists + Parses the data from the response and provides basic sequence like - operations, such as iteration and indexing to access the entries that - are contained in this collection. + operations, such as iteration and indexing to access the entries + that are contained in this collection. + """ + page_size = 100 def __init__(self, url, data, adapter): self._entry_data = {} self._current = 0 - AWeberResponse.__init__(self, url, data, adapter) + super(AWeberCollection, self).__init__(url, data, adapter) self._key_entries(self._data) def get_by_id(self, id): - """ - Returns an entry from this collection, as found by its actual - AWeber id, not its offset. Will actually request the data from - the API. + """Returns an entry from this collection. + + The Entry as found by its actual AWeber id, not its offset. + Will actually request the data from the API. + """ return self.load_from_url("{0}/{1}".format(self.url, id)) def _key_entries(self, response): count = 0 for entry in response['entries']: - self._entry_data[count+response['start']] = entry + self._entry_data[count + response['start']] = entry count += 1 def _load_page_for_offset(self, offset): @@ -45,6 +51,7 @@ def _load_page_for_offset(self, offset): self._key_entries(response) def _get_page_params(self, offset): + """Return the start and size of the paginated response.""" next_link = self._data.get('next_collection_link', None) if next_link is None: """no more parameters in page!""" @@ -55,20 +62,22 @@ def _get_page_params(self, offset): self.page_size = int(query_parts['ws.size'][0]) page_number = int(floor(offset / self.page_size)) start = page_number * self.page_size - return { 'ws.start' : start, 'ws.size' : self.page_size } + return {'ws.start': start, 'ws.size': self.page_size} def create(self, **kwargs): + """Method to create an item.""" params = {'ws.op': 'create'} params.update(kwargs) - response = self.adapter.request('POST', self.url, params, - response='headers') + response = self.adapter.request( + 'POST', self.url, params, response='headers') resource_url = response['location'] data = self.adapter.request('GET', resource_url) - return aweber_api.AWeberEntry(resource_url, data, self.adapter) + return AWeberEntry(resource_url, data, self.adapter) def find(self, **kwargs): + """Method to request a collection.""" params = {'ws.op': 'find'} params.update(kwargs) query_string = urlencode(params) @@ -86,28 +95,26 @@ def _get_total_size(self, uri, **kwargs): def get_parent_entry(self): """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: + url_parts = self._partition_url() + if url_parts is None: return None - size = len(url_parts) - #Remove collection id and slash from end of url - url = self.url[:-len(url_parts[size-1])-1] + url = self._construct_parent_url(url_parts, 1) + data = self.adapter.request('GET', url) try: - entry = aweber_api.AWeberEntry(url, data, self.adapter) + entry = AWeberEntry(url, data, self.adapter) + except TypeError: return None return entry def _create_entry(self, offset): + """Add an entry to the collection""" data = self._entry_data[offset] - url = data['self_link'].replace(aweber_api.API_BASE, '') - self._entries[offset] = aweber_api.AWeberEntry(url, data, - self.adapter) + url = data['self_link'].replace(API_BASE, '') + self._entries[offset] = AWeberEntry(url, data, self.adapter) def __len__(self): return self.total_size @@ -116,9 +123,10 @@ def __iter__(self): return self def next(self): + """Get the next entry in the collection.""" if self._current < self.total_size: self._current += 1 - return self[self._current-1] + return self[self._current - 1] self._current = 0 raise StopIteration @@ -131,4 +139,3 @@ def __getitem__(self, offset): self._load_page_for_offset(offset) self._create_entry(offset) return self._entries[offset] - diff --git a/aweber_api/data_dict.py b/aweber_api/data_dict.py index 1de7de7..024cfb3 100644 --- a/aweber_api/data_dict.py +++ b/aweber_api/data_dict.py @@ -1,10 +1,12 @@ -""" -This class is used to propagate changes to a parent item. This is -used for when an AWeberEntry has a dict item as on of the attributes -in _data. When changes are made to an item in this data dict, __setattr__ -gets called on the parent with the new state of the dict. -""" class DataDict(object): + """This class is used to propagate changes to a parent item. + + This is used for when an AWeberEntry has a dict item as one of the + attributes in _data. When changes are made to an item in this data + dict, __setattr__ gets called on the parent with the new state of + the dict. + + """ def __init__(self, data, name, parent): self.parent = parent diff --git a/aweber_api/entry.py b/aweber_api/entry.py index 6d9f542..f716338 100644 --- a/aweber_api/entry.py +++ b/aweber_api/entry.py @@ -1,28 +1,35 @@ +from urllib import urlencode + import aweber_api -from aweber_api.response import AWeberResponse from aweber_api.data_dict import DataDict - -from urllib import urlencode +from aweber_api.response import AWeberResponse class AWeberEntry(AWeberResponse): - """ - Represents a single entry in the AWeber API data heirarchy, such as one - specific account, list, web form, etc. Built on data that is returned - from an id-ed URI, such as: - /accounts/XXXX - /accounts/XXXX/lists/XXXX - Can also be generated from the data in the entries array of a collection - object, which is identical to the data return from the URI for that - specific entry. - - Provides direct access to properties in the response, such as self.id + """Represents a single entry in the AWeber API data heirarchy. + + For example, single entries can be a: + * specific account + * list + * web form + + Built on data that is returned from an id-ed URI, such as: + * /accounts/XXXX + * /accounts/XXXX/lists/XXXX + + Can also be generated from the data in the entries array of a + collection object, which is identical to the data return from the + URI for that specific entry. + + Provides direct access to properties in the response, such as + self.id + """ def __init__(self, url, data, adapter): self._data = {} self._diff = {} - AWeberResponse.__init__(self, url, data, adapter) + super(AWeberEntry, self).__init__(url, data, adapter) self._child_collections = {} def __setattr__(self, key, value): @@ -30,34 +37,37 @@ def __setattr__(self, key, value): self._data[key] = value self._diff[key] = value return value - return AWeberResponse.__setattr__(self, key, value) + return super(AWeberEntry, self).__setattr__(key, value) def delete(self): """Invoke the API method to DELETE* this entry resource. - * Note: Not all entry resources are eligible to be DELETED, please - refer to the AWeber API Reference Documentation at - https://labs.aweber.com/docs/reference/1.0 for more - details on which entry resources may be deleted. + * Note: + Not all entry resources are eligible to be DELETED, please + refer to the AWeber API Reference Documentation at + https://labs.aweber.com/docs/reference/1.0 for more + details on which entry resources may be deleted. + """ + self.adapter.request('DELETE', self.url, response='status') return True def move(self, list_, **kwargs): - """Invoke the API method to MOVE an entry resource to a - different List. - - Note: Not all entry resources are eligible to be moved, please - refer to the AWeber API Reference Documentation at - https://labs.aweber.com/docs/reference/1.0 for more - details on which entry resources may be moved and if there - are any requirements for moving that resource. + """Invoke the API method to Move an entry resource to a List. + + * Note: + Not all entry resources are eligible to be moved, please + refer to the AWeber API Reference Documentation at + https://labs.aweber.com/docs/reference/1.0 for more + details on which entry resources may be moved and if there + are any requirements for moving that resource. + """ - params = {'ws.op': 'move', - 'list_link': list_.self_link} + params = {'ws.op': 'move', 'list_link': list_.self_link} params.update(kwargs) - response = self.adapter.request('POST', self.url, params, - response='headers') + response = self.adapter.request( + 'POST', self.url, params, response='headers') new_resource = response['location'] self._diff = {} @@ -72,10 +82,12 @@ def save(self): def get_activity(self): """Invoke the API method to return all Subscriber activity. - * Note: This method only works on Subscriber Entry resources. - refer to the AWeber API Reference Documentation at - https://labs.aweber.com/docs/reference/1.0#subscriber - for more details on how to call this method. + * Note: + This method only works on Subscriber Entry resources. + refer to the AWeber API Reference Documentation at + https://labs.aweber.com/docs/reference/1.0#subscriber + for more details on how to call this method. + """ self._method_for('subscriber') params = {'ws.op': 'getActivity'} @@ -88,13 +100,15 @@ def get_activity(self): return collection def findSubscribers(self, **kwargs): - """Invoke the API method to find all subscribers on all Lists + """Invoke the API method to find all subscribers on all Lists. + + * Note: + This method only works on Account Entry resources and + requires access to subscriber information. please + refer to the AWeber API Reference Documentation at + https://labs.aweber.com/docs/reference/1.0#account + for more details on how to call this method. - * Note: This method only works on Account Entry resources and - requires access to subscriber information. please - refer to the AWeber API Reference Documentation at - https://labs.aweber.com/docs/reference/1.0#account - for more details on how to call this method. """ self._method_for('account') params = {'ws.op': 'findSubscribers'} @@ -113,37 +127,36 @@ def _get_total_size(self, uri, **kwargs): return int(self.adapter.request('GET', total_size_uri)) def get_parent_entry(self): - """Return the parent entry of this entry or None if no parent exists. + """Return the parent entry of this entry + + returns None if no parent exists. Example: - calling get_parent_entry on a SubscriberEntry will return the List - Entry that SubscriberEntry belongs to. For more information on - the AWeber API and how resources are arranged, refer to: - https://labs.aweber.com/docs/reference/1.0 - """ - url_parts = self.url.split('/') - size = len(url_parts) - url = self.url[:-len(url_parts[size-1])-1] - url = url[:-len(url_parts[size-2])-1] + calling get_parent_entry on a SubscriberEntry will return the + List Entry that SubscriberEntry belongs to. For more + information on the AWeber API and how resources are arranged, + refer to: https://labs.aweber.com/docs/reference/1.0 - if url == '': + """ + url_parts = self._partition_url() + if url_parts is None: return None + url = self._construct_parent_url(url_parts, 2) + data = self.adapter.request('GET', url) return AWeberEntry(url, data, self.adapter) def get_web_forms(self): self._method_for('account') - data = self.adapter.request('GET', - "{0}?ws.op=getWebForms".format(self.url)) + data = self.adapter.request( + 'GET', "{0}?ws.op=getWebForms".format(self.url)) return self._parseNamedOperation(data) def get_web_form_split_tests(self): self._method_for('account') data = self.adapter.request( - 'GET', - "{0}?ws.op=getWebFormSplitTests".format(self.url), - ) + 'GET', "{0}?ws.op=getWebFormSplitTests".format(self.url)) return self._parseNamedOperation(data) def _child_collection(self, attr): diff --git a/aweber_api/oauth.py b/aweber_api/oauth.py index 74bc572..1fe3003 100644 --- a/aweber_api/oauth.py +++ b/aweber_api/oauth.py @@ -1,9 +1,10 @@ +from urllib import urlencode +import json import os + import oauth2 as oauth -import json -from urllib import urlencode -import aweber_api +from aweber_api.base import APIException class OAuthAdapter(object): @@ -29,21 +30,20 @@ def request(self, method, url, data={}, response='body'): url = self._expand_url(url) body = self._prepare_request_body(method, url, data) - # need a test for the next 4 lines below content_type = 'application/json' if method == 'GET' and body is not None and body is not '': - # todo: need a better way to do this! if '?' in url: url = '{0}&{1}'.format(url, body) else: url = '{0}?{1}'.format(url, body) + if method == 'POST': content_type = 'application/x-www-form-urlencoded' - headers = {'Content-Type' : content_type} + headers = {'Content-Type': content_type} + resp, content = client.request( + url, method, body=body, headers=headers) - resp, content = client.request(url, method, body=body, - headers=headers) if int(resp['status']) >= 400: """ API Service Errors: @@ -58,7 +58,7 @@ def request(self, method, url, data={}, response='body'): error = content.get('error', {}) error_type = error.get('type') error_msg = error.get('message') - raise aweber_api.base.APIException( + raise APIException( '{0}: {1}'.format(error_type, error_msg)) if response == 'body' and isinstance(content, str): @@ -71,7 +71,7 @@ def request(self, method, url, data={}, response='body'): def _expand_url(self, url): if not url[:4] == 'http': - return '%s%s' % (self.api_base, url) + return '{0}{1}'.format(self.api_base, url) return url def _get_client(self): @@ -81,11 +81,11 @@ def _get_client(self): client = oauth.Client(self.consumer, token=token) else: client = oauth.Client(self.consumer) + client.ca_certs = os.path.join(os.path.dirname(__file__), 'cacert.crt') return client def _prepare_request_body(self, method, url, data): - # might need a test for the changes to this method if method not in ['POST', 'GET', 'PATCH'] or len(data.keys()) == 0: return '' if method in ['POST', 'GET']: diff --git a/aweber_api/response.py b/aweber_api/response.py index 8d574cc..1d44e43 100644 --- a/aweber_api/response.py +++ b/aweber_api/response.py @@ -27,4 +27,3 @@ def __getattr__(self, attr): return self._data[attr] else: raise AttributeError(attr) -