diff --git a/README.rst b/README.rst
new file mode 100644
index 0000000..9231629
--- /dev/null
+++ b/README.rst
@@ -0,0 +1,111 @@
+About
+-----
+
+The AWeber API Python Library allows you to quickly get up and running with
+integrating access to the AWeber API into your Python applications. This
+egg requires the python-oauth2 to handle the authentication.
+
+Installation
+============
+
+This egg can be installed by checking out the source::
+
+ $ sudo python setup.py install
+
+Or from Pypi::
+
+ $ easy_install -U 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::
+
+ from aweber import AWeberAPI
+ aweber = AWeberAPI(consumer_key, consumer_secret)
+ account = aweber.get_account(access_token, token_secret)
+
+ for list in account.lists:
+ print list.name
+
+
+Getting request tokens / access tokens
+++++++++++++++++++++++++++++++++++++++
+
+You can also use the AWeberAPI object to handle retrieving request tokens::
+
+ from aweber import AWeberAPI
+ aweber = AWeberAPI(consumer_key, consumer_secret)
+ request_token, request_token_secret = aweber.get_request_token(callback_url)
+ print aweber.authorize_url
+
+As well as access tokens::
+
+ from aweber import AWeberAPI
+ aweber = AWeberAPI(consumer_key, consumer_secret)
+ aweber.user.verifier = verifier
+ aweber.user.request_token = request_token
+ aweber.user.token_secret = request_token_secret
+ access_token, access_token_secret = aweber.get_access_token()
+
+
+Full Pylons example
++++++++++++++++++++
+
+Here is a simple Pylons example that uses the AWeber API Python Library to get
+a request token, have it authorized, and then print some basic stats about the
+web forms in that user's first list::
+
+ from pylons import session, request, tmpl_context as c
+ from pylons.controllers.util import redirect
+
+ from awebertest.lib.base import BaseController, render
+
+ from aweber_api import AWeberAPI
+
+ url = 'http://localhost:5000'
+ consumer_key = "vjckgsr5y4gfOa3PWnf"
+ consumer_secret = "u3sQ7vGGJBfds4q5dfgsTESi685c5x2wm6gZuIj"
+ class DemoController(BaseController):
+
+ def __before__(self):
+ self.aweber = AWeberAPI(consumer_key, consumer_secret)
+
+ def index(self):
+ token, secret = self.aweber.get_request_token(url+'/demo/get_access')
+ session['request_token_secret'] = secret
+ session.save()
+ redirect(self.aweber.authorize_url)
+
+ def get_access(self):
+ self.aweber.user.request_token = request.params['oauth_token']
+ self.aweber.user.token_secret = session['request_token_secret']
+ self.aweber.user.verifier = request.params['oauth_verifier']
+ session['token'], session['secret'] = self.aweber.get_access_token()
+ session.save()
+ redirect(url+'/demo/show')
+
+ def show(self):
+ c.account = self.aweber.get_account(session['token'], session['secret'])
+ return render('data.mako')
+
+
+In `data.mako`::
+
+
+
+
+ Web Forms
+ % for list in c.account.lists:
+ List Id: ${list.id}, name: ${list.name}
+ Currently has: ${len(list.web_forms)} web forms
+
+ % for form in list.web_forms:
+ - Form Id: ${form.id}, name: ${form.name}
+ % endfor
+
+ % endfor
+
+
diff --git a/aweber_api.egg-info/PKG-INFO b/aweber_api.egg-info/PKG-INFO
new file mode 100644
index 0000000..f53dc95
--- /dev/null
+++ b/aweber_api.egg-info/PKG-INFO
@@ -0,0 +1,10 @@
+Metadata-Version: 1.0
+Name: aweber-api
+Version: 1.0.0
+Summary: UNKNOWN
+Home-page: UNKNOWN
+Author: UNKNOWN
+Author-email: UNKNOWN
+License: UNKNOWN
+Description: UNKNOWN
+Platform: UNKNOWN
diff --git a/aweber_api.egg-info/SOURCES.txt b/aweber_api.egg-info/SOURCES.txt
new file mode 100644
index 0000000..30a7136
--- /dev/null
+++ b/aweber_api.egg-info/SOURCES.txt
@@ -0,0 +1,11 @@
+setup.py
+aweber_api/__init__.py
+aweber_api/base.py
+aweber_api/collection.py
+aweber_api/entry.py
+aweber_api/oauth.py
+aweber_api/response.py
+aweber_api.egg-info/PKG-INFO
+aweber_api.egg-info/SOURCES.txt
+aweber_api.egg-info/dependency_links.txt
+aweber_api.egg-info/top_level.txt
\ No newline at end of file
diff --git a/aweber_api.egg-info/dependency_links.txt b/aweber_api.egg-info/dependency_links.txt
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/aweber_api.egg-info/dependency_links.txt
@@ -0,0 +1 @@
+
diff --git a/aweber_api.egg-info/top_level.txt b/aweber_api.egg-info/top_level.txt
new file mode 100644
index 0000000..5ca1f7a
--- /dev/null
+++ b/aweber_api.egg-info/top_level.txt
@@ -0,0 +1 @@
+aweber_api
diff --git a/aweber_api/.base.py.swp b/aweber_api/.base.py.swp
new file mode 100644
index 0000000..7080f19
Binary files /dev/null and b/aweber_api/.base.py.swp differ
diff --git a/aweber_api/.collection.py.swp b/aweber_api/.collection.py.swp
new file mode 100644
index 0000000..e6edd1a
Binary files /dev/null and b/aweber_api/.collection.py.swp differ
diff --git a/aweber_api/.entry.py.swp b/aweber_api/.entry.py.swp
new file mode 100644
index 0000000..f4cdc55
Binary files /dev/null and b/aweber_api/.entry.py.swp differ
diff --git a/aweber_api/.oauth.py.swp b/aweber_api/.oauth.py.swp
new file mode 100644
index 0000000..61684b5
Binary files /dev/null and b/aweber_api/.oauth.py.swp differ
diff --git a/aweber_api/.response.py.swp b/aweber_api/.response.py.swp
new file mode 100644
index 0000000..8a6e0ad
Binary files /dev/null and b/aweber_api/.response.py.swp differ
diff --git a/aweber_api/__init__.py b/aweber_api/__init__.py
new file mode 100644
index 0000000..3d0717f
--- /dev/null
+++ b/aweber_api/__init__.py
@@ -0,0 +1,99 @@
+from urlparse import parse_qs
+
+from aweber_api.base import (AWeberBase, API_BASE, ACCESS_TOKEN_URL,
+ REQUEST_TOKEN_URL, AUTHORIZE_URL)
+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
+ 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)
+ self.adapter.user = AWeberUser()
+
+ @property
+ def authorize_url(self):
+ """
+ Returns 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 AUTHORIZE_URL
+
+ def get_request_token(self, callback_url):
+ """
+ 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)
+ return (self.user.request_token, self.user.token_secret)
+
+ def get_access_token(self):
+ """
+ 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)
+ return (self.user.access_token, self.user.token_secret)
+
+ def _parse_token_response(self, response):
+ if not type(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:
+ 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.
+ """
+ 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.
+ """
+
+ request_token = None
+ token_secret = None
+ access_token = None
+ verifier = None
+
+ def get_highest_priority_token(self):
+ return self.access_token or self.request_token
+
diff --git a/aweber_api/__init__.pyc b/aweber_api/__init__.pyc
new file mode 100644
index 0000000..3c9acc4
Binary files /dev/null and b/aweber_api/__init__.pyc differ
diff --git a/aweber_api/base.py b/aweber_api/base.py
new file mode 100644
index 0000000..8f9dafc
--- /dev/null
+++ b/aweber_api/base.py
@@ -0,0 +1,54 @@
+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'
+AUTHORIZE_URL = 'https://auth.aweber.com/1.0/oauth/authorize'
+
+class AWeberBase(object):
+ """
+ Provides functionality shared accross all AWeber objects
+ """
+
+ collections_map = {
+ 'account' : ['lists', 'vendor_accounts'],
+ 'broadcast_campaign' : ['links', 'messages'],
+ 'followup_campaign' : ['links', 'messages'],
+ 'link' : ['clicks'],
+ 'list' : ['campaigns', 'subscribers', 'web_forms',
+ 'web_form_split_tests'],
+ 'message' : ['opens', 'tracked_events'],
+ 'subscriber' : [],
+ 'web_form': [],
+ 'web_form_split_test' : ['components']
+ }
+
+ @property
+ def user(self):
+ return self.adapter.user
+
+ def load_from_url(self, url):
+ """
+ Gets an AWeberCollection or AWeberEntry from the given URL.
+ """
+ response = self.adapter.request('GET', url)
+ return self._read_response(url, response)
+
+ def _method_for(self, type):
+ if not self.type == type:
+ raise AttributeError('Method does not exist')
+
+ def _read_response(self, url, response):
+ if 'entries' in response:
+ from aweber_api.collection import AWeberCollection
+ return AWeberCollection(url, response, self.adapter)
+ if 'id' 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))
+ return entries
diff --git a/aweber_api/base.pyc b/aweber_api/base.pyc
new file mode 100644
index 0000000..56fbc9a
Binary files /dev/null and b/aweber_api/base.pyc differ
diff --git a/aweber_api/collection.py b/aweber_api/collection.py
new file mode 100644
index 0000000..a22bc25
--- /dev/null
+++ b/aweber_api/collection.py
@@ -0,0 +1,81 @@
+from math import floor
+from urlparse import parse_qs
+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:
+ /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.
+ """
+ page_size = 100
+
+ def __init__(self, url, data, adapter):
+ self._entry_data = {}
+ self._current = 0
+
+ AWeberResponse.__init__(self, 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.
+ """
+ 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
+ count += 1
+
+ def _load_page_for_offset(self, offset):
+ page = self._get_page_params(offset)
+ response = self.adapter.request('GET', self.url, page)
+ self._key_entries(response)
+
+ def _get_page_params(self, offset):
+ next_link = self._data['next_collection_link']
+ url, query = next_link.split('?')
+ query_parts = parse_qs(query)
+ 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 }
+
+ def _create_entry(self, offset):
+ from aweber_api.entry import AWeberEntry
+ data = self._entry_data[offset]
+
+ url = "{0}/{1}".format(self.url, data['id'])
+ self._entries[offset] = AWeberEntry(url, data, self.adapter)
+
+ def __len__(self):
+ return self.total_size
+
+ def __iter__(self):
+ return self
+
+ def next(self):
+ if self._current < self.total_size:
+ self._current += 1
+ return self[self._current-1]
+ self._current = 0
+ raise StopIteration
+
+ def __getitem__(self, offset):
+ if offset < 0 or offset >= self._data['total_size']:
+ raise ValueError('Offset {0} does not exist'.format(offset))
+
+ if not offset in self._entries:
+ if not offset in self._entry_data:
+ self._load_page_for_offset(offset)
+ self._create_entry(offset)
+ return self._entries[offset]
+
diff --git a/aweber_api/collection.pyc b/aweber_api/collection.pyc
new file mode 100644
index 0000000..70d02e5
Binary files /dev/null and b/aweber_api/collection.pyc differ
diff --git a/aweber_api/entry.py b/aweber_api/entry.py
new file mode 100644
index 0000000..a348716
--- /dev/null
+++ b/aweber_api/entry.py
@@ -0,0 +1,42 @@
+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
+ """
+
+ def __init__(self, url, data, adapter):
+ AWeberResponse.__init__(self, url, data, adapter)
+ self._child_collections = {}
+
+
+ def get_web_forms(self):
+ self._method_for('account')
+ data = self.adapter.request('GET',
+ "{0}?ws.op=getWebForms".format(self.url))
+ return self._parseNamedOperation(data)
+
+ def _child_collection(self, attr):
+ if not attr in self._child_collections:
+ url = "{0}/{1}".format(self.url, attr)
+ self._child_collections[attr] = self.load_from_url(url)
+ return self._child_collections[attr]
+
+ def __getattr__(self, attr):
+ if attr in self._data:
+ return self._data[attr]
+ elif attr in self.collections_map[self.type]:
+ return self._child_collection(attr)
+ else:
+ raise AttributeError(attr)
+
+
diff --git a/aweber_api/entry.pyc b/aweber_api/entry.pyc
new file mode 100644
index 0000000..10c6e32
Binary files /dev/null and b/aweber_api/entry.pyc differ
diff --git a/aweber_api/oauth.py b/aweber_api/oauth.py
new file mode 100644
index 0000000..8524648
--- /dev/null
+++ b/aweber_api/oauth.py
@@ -0,0 +1,45 @@
+import oauth2 as oauth
+import json
+
+class OAuthAdapter(object):
+
+ def __init__(self, key, secret, base):
+ self.key = key
+ self.secret = secret
+ self.consumer = oauth.Consumer(key=self.key, secret=self.secret)
+ self.api_base = base
+
+ def _parse(self, response):
+ try:
+ data = json.loads(response)
+ if not data or data == '':
+ return response
+ return data
+ except:
+ pass
+ return response
+
+ def request(self, method, url, data={}):
+ token = self.user.get_highest_priority_token()
+ if token:
+ token = oauth.Token(token, self.user.token_secret)
+ client = oauth.Client(self.consumer, token=token)
+ else:
+ client = oauth.Client(self.consumer)
+
+ if not url[:4] == 'http':
+ url = '%s%s' % (self.api_base, url)
+
+ if len(data.keys()) == 0:
+ body = None
+ else:
+ body = '&'.join(map(lambda x: "{0}={1}".format(x, data[x]),
+ data.keys()))
+
+ try:
+ resp, content = client.request(url, method, body=body)
+ if type(content) == str:
+ return self._parse(content)
+ except e:
+ pass
+ return None
diff --git a/aweber_api/oauth.pyc b/aweber_api/oauth.pyc
new file mode 100644
index 0000000..2bc30ff
Binary files /dev/null and b/aweber_api/oauth.pyc differ
diff --git a/aweber_api/response.py b/aweber_api/response.py
new file mode 100644
index 0000000..fcc6121
--- /dev/null
+++ b/aweber_api/response.py
@@ -0,0 +1,29 @@
+from aweber_api import AWeberBase
+
+class AWeberResponse(AWeberBase):
+
+ def __init__(self, url, data, adapter):
+ self._type = None
+ self.adapter = adapter
+ self.url = url
+ self._data = data
+ self._entries = {}
+
+ def _generate_type(self):
+ if 'resource_type_link' in self._data:
+ type = self._data['resource_type_link']
+ self._type = type.split('#').pop()
+ return None
+
+ @property
+ def type(self):
+ if not self._type:
+ self._generate_type()
+ return self._type
+
+ def __getattr__(self, attr):
+ if attr in self._data:
+ return self._data[attr]
+ else:
+ raise AttributeError(attr)
+
diff --git a/aweber_api/response.pyc b/aweber_api/response.pyc
new file mode 100644
index 0000000..3231da3
Binary files /dev/null and b/aweber_api/response.pyc differ
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..a816493
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,10 @@
+from setuptools import setup, find_packages
+
+setup(
+ name='aweber_api',
+ version='1.0.0',
+ packages=find_packages(exclude=['tests']),
+ test_suite='nose.collector',
+ include_package_data=True
+)
+
diff --git a/tests/data/account.json b/tests/data/account.json
new file mode 100644
index 0000000..4782e1e
--- /dev/null
+++ b/tests/data/account.json
@@ -0,0 +1 @@
+{"http_etag": "\"5ae3075ce7c9b1869ce83bf2a7d67da376af0031-ca5feee2b7fbb6febfca8af5541541ea960aaedb\"", "lists_collection_link": "https://api.aweber.com/1.0/accounts/326084/lists", "self_link": "https://api.aweber.com/1.0/accounts/326084", "resource_type_link": "https://api.aweber.com/1.0/#account", "id": 326084, "integrations_collection_link": "https://api.aweber.com/1.0/accounts/326084/integrations"}
diff --git a/tests/data/accounts.json b/tests/data/accounts.json
new file mode 100644
index 0000000..0e0edc4
--- /dev/null
+++ b/tests/data/accounts.json
@@ -0,0 +1,9 @@
+{"total_size": 1, "start": 0, "next_collection_link":
+"http://api.apitest.lab:81/0.1/accounts?ws.start=20&ws.size=20", "entries":
+[{"http_etag":
+"\"c70dfb07dc67c9dd6a8bc01130acf68d1fbf035f-ca5feee2b7fbb6febfca8af5541541ea960aaedb\"",
+"vendor_accounts_collection_link":
+"http://api.apitest.lab:81/0.1/accounts/910/vendor_accounts",
+"lists_collection_link": "http://api.apitest.lab:81/0.1/accounts/910/lists",
+"self_link": "http://api.apitest.lab:81/0.1/accounts/910",
+"resource_type_link": "http://api.apitest.lab:81/0.1/#account", "id": 910}], "resource_type_link" : "http://api.apitest.lab:81/0.1/#accounts"}
diff --git a/tests/data/error.json b/tests/data/error.json
new file mode 100644
index 0000000..bb8eb9b
--- /dev/null
+++ b/tests/data/error.json
@@ -0,0 +1 @@
+{"error": {"message": "Missing required parameter 'oauth_consumer_key'.", "type": "MissingRequiredOAuthParameterError"}}
diff --git a/tests/data/lists.json b/tests/data/lists.json
new file mode 100644
index 0000000..fc113ef
--- /dev/null
+++ b/tests/data/lists.json
@@ -0,0 +1 @@
+{"total_size": 24, "start": 0, "next_collection_link": "http://api.apitest.lab:81/0.1/accounts/1/lists?ws.start=20&ws.size=20", "entries": [{"campaigns_collection_link": "http://api.apitest.lab:81/0.1/accounts/1/lists/251847/campaigns", "name": "default251847", "subscribers_collection_link": "http://api.apitest.lab:81/0.1/accounts/1/lists/251847/subscribers", "id": 251847, "http_etag": "\"dac7041022b8d1b9690fbc0a34121a19c0461781-ca5feee2b7fbb6febfca8af5541541ea960aaedb\"", "self_link": "http://api.apitest.lab:81/0.1/accounts/1/lists/251847", "resource_type_link": "http://api.apitest.lab:81/0.1/#list", "web_forms_collection_link": "http://api.apitest.lab:81/0.1/accounts/1/lists/251847/web_forms"}, {"campaigns_collection_link": "http://api.apitest.lab:81/0.1/accounts/1/lists/251846/campaigns", "name": "default251846", "subscribers_collection_link": "http://api.apitest.lab:81/0.1/accounts/1/lists/251846/subscribers", "id": 251846, "http_etag": "\"acefbf9202cfd5d156006591fa0222b7e4f735d2-ca5feee2b7fbb6febfca8af5541541ea960aaedb\"", "self_link": "http://api.apitest.lab:81/0.1/accounts/1/lists/251846", "resource_type_link": "http://api.apitest.lab:81/0.1/#list", "web_forms_collection_link": "http://api.apitest.lab:81/0.1/accounts/1/lists/251846/web_forms"}, {"campaigns_collection_link": "http://api.apitest.lab:81/0.1/accounts/1/lists/251845/campaigns", "name": "default251845", "subscribers_collection_link": "http://api.apitest.lab:81/0.1/accounts/1/lists/251845/subscribers", "id": 251845, "http_etag": "\"a2845b27890ae073977798afe229933c00b6604a-ca5feee2b7fbb6febfca8af5541541ea960aaedb\"", "self_link": "http://api.apitest.lab:81/0.1/accounts/1/lists/251845", "resource_type_link": "http://api.apitest.lab:81/0.1/#list", "web_forms_collection_link": "http://api.apitest.lab:81/0.1/accounts/1/lists/251845/web_forms"}, {"campaigns_collection_link": "http://api.apitest.lab:81/0.1/accounts/1/lists/251843/campaigns", "name": "default251843", "subscribers_collection_link": "http://api.apitest.lab:81/0.1/accounts/1/lists/251843/subscribers", "id": 251843, "http_etag": "\"80ef114161f2e417ce3da2a5ae0f45e1f9e2a17d-ca5feee2b7fbb6febfca8af5541541ea960aaedb\"", "self_link": "http://api.apitest.lab:81/0.1/accounts/1/lists/251843", "resource_type_link": "http://api.apitest.lab:81/0.1/#list", "web_forms_collection_link": "http://api.apitest.lab:81/0.1/accounts/1/lists/251843/web_forms"}, {"campaigns_collection_link": "http://api.apitest.lab:81/0.1/accounts/1/lists/312802/campaigns", "name": "anewtestlist", "subscribers_collection_link": "http://api.apitest.lab:81/0.1/accounts/1/lists/312802/subscribers", "id": 312802, "http_etag": "\"b4408834c0f13761cc2f4b90784e1faee5b8d084-ca5feee2b7fbb6febfca8af5541541ea960aaedb\"", "self_link": "http://api.apitest.lab:81/0.1/accounts/1/lists/312802", "resource_type_link": "http://api.apitest.lab:81/0.1/#list", "web_forms_collection_link": "http://api.apitest.lab:81/0.1/accounts/1/lists/312802/web_forms"}, {"campaigns_collection_link": "http://api.apitest.lab:81/0.1/accounts/1/lists/251848/campaigns", "name": "testmc", "subscribers_collection_link": "http://api.apitest.lab:81/0.1/accounts/1/lists/251848/subscribers", "id": 251848, "http_etag": "\"be0d61f3d6cbf34819f74e994b1ffa2c96ed7bb3-ca5feee2b7fbb6febfca8af5541541ea960aaedb\"", "self_link": "http://api.apitest.lab:81/0.1/accounts/1/lists/251848", "resource_type_link": "http://api.apitest.lab:81/0.1/#list", "web_forms_collection_link": "http://api.apitest.lab:81/0.1/accounts/1/lists/251848/web_forms"}, {"campaigns_collection_link": "http://api.apitest.lab:81/0.1/accounts/1/lists/303449/campaigns", "name": "default303449", "subscribers_collection_link": "http://api.apitest.lab:81/0.1/accounts/1/lists/303449/subscribers", "id": 303449, "http_etag": "\"5ba07c4b65dd1f6d245a7fd0dada5ea65b0f054d-ca5feee2b7fbb6febfca8af5541541ea960aaedb\"", "self_link": "http://api.apitest.lab:81/0.1/accounts/1/lists/303449", "resource_type_link": "http://api.apitest.lab:81/0.1/#list", "web_forms_collection_link": "http://api.apitest.lab:81/0.1/accounts/1/lists/303449/web_forms"}, {"campaigns_collection_link": "http://api.apitest.lab:81/0.1/accounts/1/lists/305162/campaigns", "name": "herewego12", "subscribers_collection_link": "http://api.apitest.lab:81/0.1/accounts/1/lists/305162/subscribers", "id": 305162, "http_etag": "\"03a0ec4e9f52a0a1a99a8d4a83ef6587eed9a6b8-ca5feee2b7fbb6febfca8af5541541ea960aaedb\"", "self_link": "http://api.apitest.lab:81/0.1/accounts/1/lists/305162", "resource_type_link": "http://api.apitest.lab:81/0.1/#list", "web_forms_collection_link": "http://api.apitest.lab:81/0.1/accounts/1/lists/305162/web_forms"}, {"campaigns_collection_link": "http://api.apitest.lab:81/0.1/accounts/1/lists/226186/campaigns", "name": "test-list777", "subscribers_collection_link": "http://api.apitest.lab:81/0.1/accounts/1/lists/226186/subscribers", "id": 226186, "http_etag": "\"e51bc0351083caca2a811a97937225605e80113c-ca5feee2b7fbb6febfca8af5541541ea960aaedb\"", "self_link": "http://api.apitest.lab:81/0.1/accounts/1/lists/226186", "resource_type_link": "http://api.apitest.lab:81/0.1/#list", "web_forms_collection_link": "http://api.apitest.lab:81/0.1/accounts/1/lists/226186/web_forms"}, {"campaigns_collection_link": "http://api.apitest.lab:81/0.1/accounts/1/lists/251844/campaigns", "name": "default251844", "subscribers_collection_link": "http://api.apitest.lab:81/0.1/accounts/1/lists/251844/subscribers", "id": 251844, "http_etag": "\"fd2ed44b31f0b07b1c3e1d8cc094c152e83b2dc5-ca5feee2b7fbb6febfca8af5541541ea960aaedb\"", "self_link": "http://api.apitest.lab:81/0.1/accounts/1/lists/251844", "resource_type_link": "http://api.apitest.lab:81/0.1/#list", "web_forms_collection_link": "http://api.apitest.lab:81/0.1/accounts/1/lists/251844/web_forms"}, {"campaigns_collection_link": "http://api.apitest.lab:81/0.1/accounts/1/lists/1/campaigns", "name": "test-list2fff", "subscribers_collection_link": "http://api.apitest.lab:81/0.1/accounts/1/lists/1/subscribers", "id": 1, "http_etag": "\"a2e27930058f806c597e52b9f069284cff575588-ca5feee2b7fbb6febfca8af5541541ea960aaedb\"", "self_link": "http://api.apitest.lab:81/0.1/accounts/1/lists/1", "resource_type_link": "http://api.apitest.lab:81/0.1/#list", "web_forms_collection_link": "http://api.apitest.lab:81/0.1/accounts/1/lists/1/web_forms"}, {"campaigns_collection_link": "http://api.apitest.lab:81/0.1/accounts/1/lists/301413/campaigns", "name": "test-list1d", "subscribers_collection_link": "http://api.apitest.lab:81/0.1/accounts/1/lists/301413/subscribers", "id": 301413, "http_etag": "\"6a4f25e78b120a99951beae31f3d9491f19d6719-ca5feee2b7fbb6febfca8af5541541ea960aaedb\"", "self_link": "http://api.apitest.lab:81/0.1/accounts/1/lists/301413", "resource_type_link": "http://api.apitest.lab:81/0.1/#list", "web_forms_collection_link": "http://api.apitest.lab:81/0.1/accounts/1/lists/301413/web_forms"}, {"campaigns_collection_link": "http://api.apitest.lab:81/0.1/accounts/1/lists/320972/campaigns", "name": "superbenk", "subscribers_collection_link": "http://api.apitest.lab:81/0.1/accounts/1/lists/320972/subscribers", "id": 320972, "http_etag": "\"cd895b82944e86aefd492949d8b7d923d7d28778-ca5feee2b7fbb6febfca8af5541541ea960aaedb\"", "self_link": "http://api.apitest.lab:81/0.1/accounts/1/lists/320972", "resource_type_link": "http://api.apitest.lab:81/0.1/#list", "web_forms_collection_link": "http://api.apitest.lab:81/0.1/accounts/1/lists/320972/web_forms"}, {"campaigns_collection_link": "http://api.apitest.lab:81/0.1/accounts/1/lists/363010/campaigns", "name": "default363010", "subscribers_collection_link": "http://api.apitest.lab:81/0.1/accounts/1/lists/363010/subscribers", "id": 363010, "http_etag": "\"4b095ec0243d40ab613096d8d3f96a609fdd27d5-ca5feee2b7fbb6febfca8af5541541ea960aaedb\"", "self_link": "http://api.apitest.lab:81/0.1/accounts/1/lists/363010", "resource_type_link": "http://api.apitest.lab:81/0.1/#list", "web_forms_collection_link": "http://api.apitest.lab:81/0.1/accounts/1/lists/363010/web_forms"}, {"campaigns_collection_link": "http://api.apitest.lab:81/0.1/accounts/1/lists/307014/campaigns", "name": "devteamtest_2", "subscribers_collection_link": "http://api.apitest.lab:81/0.1/accounts/1/lists/307014/subscribers", "id": 307014, "http_etag": "\"d78abf7131f250f0929a6558f000b8760e76155b-ca5feee2b7fbb6febfca8af5541541ea960aaedb\"", "self_link": "http://api.apitest.lab:81/0.1/accounts/1/lists/307014", "resource_type_link": "http://api.apitest.lab:81/0.1/#list", "web_forms_collection_link": "http://api.apitest.lab:81/0.1/accounts/1/lists/307014/web_forms"}, {"campaigns_collection_link": "http://api.apitest.lab:81/0.1/accounts/1/lists/305164/campaigns", "name": "homer_simpson", "subscribers_collection_link": "http://api.apitest.lab:81/0.1/accounts/1/lists/305164/subscribers", "id": 305164, "http_etag": "\"568306713e4d297e2f40948e63f240b1798fc3d8-ca5feee2b7fbb6febfca8af5541541ea960aaedb\"", "self_link": "http://api.apitest.lab:81/0.1/accounts/1/lists/305164", "resource_type_link": "http://api.apitest.lab:81/0.1/#list", "web_forms_collection_link": "http://api.apitest.lab:81/0.1/accounts/1/lists/305164/web_forms"}, {"campaigns_collection_link": "http://api.apitest.lab:81/0.1/accounts/1/lists/363013/campaigns", "name": "default363013", "subscribers_collection_link": "http://api.apitest.lab:81/0.1/accounts/1/lists/363013/subscribers", "id": 363013, "http_etag": "\"4eee1ea48164b794be3891422d0ddbede8c26ecc-ca5feee2b7fbb6febfca8af5541541ea960aaedb\"", "self_link": "http://api.apitest.lab:81/0.1/accounts/1/lists/363013", "resource_type_link": "http://api.apitest.lab:81/0.1/#list", "web_forms_collection_link": "http://api.apitest.lab:81/0.1/accounts/1/lists/363013/web_forms"}, {"campaigns_collection_link": "http://api.apitest.lab:81/0.1/accounts/1/lists/363014/campaigns", "name": "default363014", "subscribers_collection_link": "http://api.apitest.lab:81/0.1/accounts/1/lists/363014/subscribers", "id": 363014, "http_etag": "\"511a9e0afca20511da481093d1742ce1d3a791bf-ca5feee2b7fbb6febfca8af5541541ea960aaedb\"", "self_link": "http://api.apitest.lab:81/0.1/accounts/1/lists/363014", "resource_type_link": "http://api.apitest.lab:81/0.1/#list", "web_forms_collection_link": "http://api.apitest.lab:81/0.1/accounts/1/lists/363014/web_forms"}, {"campaigns_collection_link": "http://api.apitest.lab:81/0.1/accounts/1/lists/363009/campaigns", "name": "super", "subscribers_collection_link": "http://api.apitest.lab:81/0.1/accounts/1/lists/363009/subscribers", "id": 363009, "http_etag": "\"570519a927e3bf6a13499bff4c10339ed8572216-ca5feee2b7fbb6febfca8af5541541ea960aaedb\"", "self_link": "http://api.apitest.lab:81/0.1/accounts/1/lists/363009", "resource_type_link": "http://api.apitest.lab:81/0.1/#list", "web_forms_collection_link": "http://api.apitest.lab:81/0.1/accounts/1/lists/363009/web_forms"}, {"campaigns_collection_link": "http://api.apitest.lab:81/0.1/accounts/1/lists/50000003/campaigns", "name": "default50000003", "subscribers_collection_link": "http://api.apitest.lab:81/0.1/accounts/1/lists/50000003/subscribers", "id": 50000003, "http_etag": "\"0f1b51f3c1fa3f61fe29a7783312371cc4f42149-ca5feee2b7fbb6febfca8af5541541ea960aaedb\"", "self_link": "http://api.apitest.lab:81/0.1/accounts/1/lists/50000003", "resource_type_link": "http://api.apitest.lab:81/0.1/#list", "web_forms_collection_link": "http://api.apitest.lab:81/0.1/accounts/1/lists/50000003/web_forms"}], "resource_type_link" : "http://api.apitest.lab:81/0.1/#list-page-resource"}
diff --git a/tests/data/lists/303449.json b/tests/data/lists/303449.json
new file mode 100644
index 0000000..582192b
--- /dev/null
+++ b/tests/data/lists/303449.json
@@ -0,0 +1 @@
+{"campaigns_collection_link": "http://api.apitest.lab:81/0.1/accounts/1/lists/303449/campaigns", "name": "default303449", "subscribers_collection_link": "http://api.apitest.lab:81/0.1/accounts/1/lists/303449/subscribers", "id": 303449, "http_etag": "\"5ba07c4b65dd1f6d245a7fd0dada5ea65b0f054d-ca5feee2b7fbb6febfca8af5541541ea960aaedb\"", "self_link": "http://api.apitest.lab:81/0.1/accounts/1/lists/303449", "resource_type_link": "http://api.apitest.lab:81/0.1/#list", "web_forms_collection_link": "http://api.apitest.lab:81/0.1/accounts/1/lists/303449/web_forms"}
diff --git a/tests/data/lists/303449/campaigns.json b/tests/data/lists/303449/campaigns.json
new file mode 100644
index 0000000..1ff9a96
--- /dev/null
+++ b/tests/data/lists/303449/campaigns.json
@@ -0,0 +1 @@
+{"total_size": 0, "start": null, "entries": [], "resource_type_link" : "http://api.apitest.lab:81/0.1/#campaign-page-resource"}
diff --git a/tests/data/lists_page2.json b/tests/data/lists_page2.json
new file mode 100644
index 0000000..2bf6446
--- /dev/null
+++ b/tests/data/lists_page2.json
@@ -0,0 +1 @@
+{"total_size": 24, "start": 20, "prev_collection_link": "http://api.apitest.lab:81/0.1/accounts/1/lists?ws.start=0&ws.size=20", "entries": [{"campaigns_collection_link": "http://api.apitest.lab:81/0.1/accounts/1/lists/50000002/campaigns", "name": "one_two_test", "subscribers_collection_link": "http://api.apitest.lab:81/0.1/accounts/1/lists/50000002/subscribers", "id": 50000002, "http_etag": "\"daeb57124532aac221374522cb6c07754e71ba21-ca5feee2b7fbb6febfca8af5541541ea960aaedb\"", "self_link": "http://api.apitest.lab:81/0.1/accounts/1/lists/50000002", "resource_type_link": "http://api.apitest.lab:81/0.1/#list", "web_forms_collection_link": "http://api.apitest.lab:81/0.1/accounts/1/lists/50000002/web_forms"}, {"campaigns_collection_link": "http://api.apitest.lab:81/0.1/accounts/1/lists/406860/campaigns", "name": "devteamtest", "subscribers_collection_link": "http://api.apitest.lab:81/0.1/accounts/1/lists/406860/subscribers", "id": 406860, "http_etag": "\"db00e81c0187d524ac56a254636664e14e90184b-ca5feee2b7fbb6febfca8af5541541ea960aaedb\"", "self_link": "http://api.apitest.lab:81/0.1/accounts/1/lists/406860", "resource_type_link": "http://api.apitest.lab:81/0.1/#list", "web_forms_collection_link": "http://api.apitest.lab:81/0.1/accounts/1/lists/406860/web_forms"}, {"campaigns_collection_link": "http://api.apitest.lab:81/0.1/accounts/1/lists/50000054/campaigns", "name": "furedesign_test", "subscribers_collection_link": "http://api.apitest.lab:81/0.1/accounts/1/lists/50000054/subscribers", "id": 50000054, "http_etag": "\"d082409a783f7f4f955cca186e17eb39d4b28045-ca5feee2b7fbb6febfca8af5541541ea960aaedb\"", "self_link": "http://api.apitest.lab:81/0.1/accounts/1/lists/50000054", "resource_type_link": "http://api.apitest.lab:81/0.1/#list", "web_forms_collection_link": "http://api.apitest.lab:81/0.1/accounts/1/lists/50000054/web_forms"}, {"campaigns_collection_link": "http://api.apitest.lab:81/0.1/accounts/1/lists/327293/campaigns", "name": "anewtestlist1", "subscribers_collection_link": "http://api.apitest.lab:81/0.1/accounts/1/lists/327293/subscribers", "id": 327293, "http_etag": "\"99057bd859740bc8cb946dac3bf6d44a3bcc5a72-ca5feee2b7fbb6febfca8af5541541ea960aaedb\"", "self_link": "http://api.apitest.lab:81/0.1/accounts/1/lists/327293", "resource_type_link": "http://api.apitest.lab:81/0.1/#list", "web_forms_collection_link": "http://api.apitest.lab:81/0.1/accounts/1/lists/327293/web_forms"}], "resource_type_link" : "http://api.apitest.lab:81/0.1/#list-page-resource"}
diff --git a/tests/data/web_forms.json b/tests/data/web_forms.json
new file mode 100644
index 0000000..24635f0
--- /dev/null
+++ b/tests/data/web_forms.json
@@ -0,0 +1 @@
+[{"conversion_percentage": 0.0, "unique_conversion_percentage": 0.0, "name": "My Web Form", "total_unique_displays": 0, "type": "styled", "is_active": true, "total_submissions": 0, "self_link": "https://api.aweber.com/1.0/accounts/1/lists/1192556/web_forms/1733464505", "http_etag": "\"81c5932ffaf2418e6547f3db29ff430f83c9ad79-ca5feee2b7fbb6febfca8af5541541ea960aaedb\"", "total_displays": 0, "resource_type_link": "https://api.aweber.com/1.0/#web_form", "id": 1733464505}, {"conversion_percentage": 33.333333333333329, "unique_conversion_percentage": 100.0, "name": "My Web Form", "total_unique_displays": 1, "type": "styled", "is_active": true, "total_submissions": 1, "self_link": "https://api.aweber.com/1.0/accounts/1/lists/1121402/web_forms/1369283626", "http_etag": "\"29ac5759b22c6d92eec850b40f2e270e42b8f6fd-ca5feee2b7fbb6febfca8af5541541ea960aaedb\"", "total_displays": 3, "resource_type_link": "https://api.aweber.com/1.0/#web_form", "id": 1369283626}, {"conversion_percentage": 0.11641443538998836, "unique_conversion_percentage": 0.26515151515151519, "name": "Home page, sidebar", "total_unique_displays": 2640, "type": "styled", "is_active": true, "total_submissions": 7, "self_link": "https://api.aweber.com/1.0/accounts/1/lists/1093432/web_forms/356827526", "http_etag": "\"d131de42f8375408f277ccee852edec27fbf8321-ca5feee2b7fbb6febfca8af5541541ea960aaedb\"", "total_displays": 6013, "resource_type_link": "https://api.aweber.com/1.0/#web_form", "id": 356827526}, {"conversion_percentage": 0.0, "unique_conversion_percentage": 0.0, "name": null, "total_unique_displays": 0, "type": "styled", "is_active": true, "total_submissions": 0, "self_link": "https://api.aweber.com/1.0/accounts/1/lists/771601/web_forms/131500557", "http_etag": "\"1288c4078b1f5a78197f267b9b0e3079320aa015-ca5feee2b7fbb6febfca8af5541541ea960aaedb\"", "total_displays": 0, "resource_type_link": "https://api.aweber.com/1.0/#web_form", "id": 131500557}, {"conversion_percentage": 0.0, "unique_conversion_percentage": 0.0, "name": "Testing", "total_unique_displays": 0, "type": "inline", "is_active": true, "total_submissions": 0, "self_link": "https://api.aweber.com/1.0/accounts/1/lists/771601/web_forms/180597630", "http_etag": "\"9e0dc548e60eecde94661e53f06380f411118e70-ca5feee2b7fbb6febfca8af5541541ea960aaedb\"", "total_displays": 0, "resource_type_link": "https://api.aweber.com/1.0/#web_form", "id": 180597630}, {"conversion_percentage": 33.333333333333329, "unique_conversion_percentage": 50.0, "name": "fdsdfsdfdfs", "total_unique_displays": 2, "type": "styled", "is_active": true, "total_submissions": 1, "self_link": "https://api.aweber.com/1.0/accounts/1/lists/771601/web_forms/270024008", "http_etag": "\"50d20937bcc6ae716543f3da492d71302f7ba5da-ca5feee2b7fbb6febfca8af5541541ea960aaedb\"", "total_displays": 3, "resource_type_link": "https://api.aweber.com/1.0/#web_form", "id": 270024008}, {"conversion_percentage": 0.0, "unique_conversion_percentage": 0.0, "name": "My Web Form", "total_unique_displays": 0, "type": "styled", "is_active": true, "total_submissions": 0, "self_link": "https://api.aweber.com/1.0/accounts/1/lists/771601/web_forms/721555577", "http_etag": "\"82b4eb089fd0b6a0b157bda1373115d146feb59b-ca5feee2b7fbb6febfca8af5541541ea960aaedb\"", "total_displays": 0, "resource_type_link": "https://api.aweber.com/1.0/#web_form", "id": 721555577}, {"conversion_percentage": 0.0, "unique_conversion_percentage": 0.0, "name": null, "total_unique_displays": 0, "type": "styled", "is_active": true, "total_submissions": 0, "self_link": "https://api.aweber.com/1.0/accounts/1/lists/771601/web_forms/1070664099", "http_etag": "\"76e920f4b1d7ee7763b1f41a59a243a8187aba99-ca5feee2b7fbb6febfca8af5541541ea960aaedb\"", "total_displays": 0, "resource_type_link": "https://api.aweber.com/1.0/#web_form", "id": 1070664099}, {"conversion_percentage": 0.0, "unique_conversion_percentage": 0.0, "name": null, "total_unique_displays": 0, "type": "styled", "is_active": true, "total_submissions": 1, "self_link": "https://api.aweber.com/1.0/accounts/1/lists/771601/web_forms/2013603311", "http_etag": "\"b6691478c2334176f9c0a0065e729e5511258aa5-ca5feee2b7fbb6febfca8af5541541ea960aaedb\"", "total_displays": 0, "resource_type_link": "https://api.aweber.com/1.0/#web_form", "id": 2013603311}, {"conversion_percentage": 1.1469142545057347, "unique_conversion_percentage": 3.9585296889726673, "name": "website", "total_unique_displays": 1061, "type": "inline", "is_active": true, "total_submissions": 42, "self_link": "https://api.aweber.com/1.0/accounts/1/lists/771601/web_forms/1077762960", "http_etag": "\"4037f3d1f0c29bb19a3a5d92d13e7f0d96dd3bb1-ca5feee2b7fbb6febfca8af5541541ea960aaedb\"", "total_displays": 3662, "resource_type_link": "https://api.aweber.com/1.0/#web_form", "id": 1077762960}, {"conversion_percentage": 0.0, "unique_conversion_percentage": 0.0, "name": "blarhg", "total_unique_displays": 1, "type": "inline", "is_active": true, "total_submissions": 0, "self_link": "https://api.aweber.com/1.0/accounts/1/lists/798061/web_forms/668327018", "http_etag": "\"36e02decd31624698c6753a7f6f8b246017ca6f2-ca5feee2b7fbb6febfca8af5541541ea960aaedb\"", "total_displays": 5, "resource_type_link": "https://api.aweber.com/1.0/#web_form", "id": 668327018}, {"conversion_percentage": 0.0, "unique_conversion_percentage": 0.0, "name": "New Form 785164876", "total_unique_displays": 0, "type": "styled", "is_active": true, "total_submissions": 0, "self_link": "https://api.aweber.com/1.0/accounts/1/lists/798061/web_forms/785164876", "http_etag": "\"99ad3b309fd7caa39cdc74a57093199166847ddb-ca5feee2b7fbb6febfca8af5541541ea960aaedb\"", "total_displays": 0, "resource_type_link": "https://api.aweber.com/1.0/#web_form", "id": 785164876}, {"conversion_percentage": 0.0, "unique_conversion_percentage": 0.0, "name": "barf", "total_unique_displays": 1, "type": "popover", "is_active": true, "total_submissions": 0, "self_link": "https://api.aweber.com/1.0/accounts/1/lists/798061/web_forms/1437187399", "http_etag": "\"83222600a0690071080e876fdf0d62c948ae0807-ca5feee2b7fbb6febfca8af5541541ea960aaedb\"", "total_displays": 1, "resource_type_link": "https://api.aweber.com/1.0/#web_form", "id": 1437187399}, {"conversion_percentage": 0.0, "unique_conversion_percentage": 0.0, "name": "Old ass legacy stinky form", "total_unique_displays": 0, "type": "lightbox", "is_active": true, "total_submissions": 0, "self_link": "https://api.aweber.com/1.0/accounts/1/lists/853318/web_forms/1126276203", "http_etag": "\"c8b7aad2c71683c23296ca4469e125fa3af67fee-ca5feee2b7fbb6febfca8af5541541ea960aaedb\"", "total_displays": 0, "resource_type_link": "https://api.aweber.com/1.0/#web_form", "id": 1126276203}, {"conversion_percentage": 0.0, "unique_conversion_percentage": 0.0, "name": "My Web Form 2", "total_unique_displays": 0, "type": "styled", "is_active": true, "total_submissions": 0, "self_link": "https://api.aweber.com/1.0/accounts/1/lists/853318/web_forms/1222053368", "http_etag": "\"4372b1ea1703db57e6f588d777244c903815c10b-ca5feee2b7fbb6febfca8af5541541ea960aaedb\"", "total_displays": 0, "resource_type_link": "https://api.aweber.com/1.0/#web_form", "id": 1222053368}, {"conversion_percentage": 400.0, "unique_conversion_percentage": 400.0, "name": "Sample", "total_unique_displays": 1, "type": "styled", "is_active": true, "total_submissions": 4, "self_link": "https://api.aweber.com/1.0/accounts/1/lists/853318/web_forms/1653200290", "http_etag": "\"552639ecbb958d98848a020ab38abce5f805db00-ca5feee2b7fbb6febfca8af5541541ea960aaedb\"", "total_displays": 1, "resource_type_link": "https://api.aweber.com/1.0/#web_form", "id": 1653200290}, {"conversion_percentage": 0.0, "unique_conversion_percentage": 0.0, "name": "New Form 1900114264", "total_unique_displays": 0, "type": "styled", "is_active": true, "total_submissions": 0, "self_link": "https://api.aweber.com/1.0/accounts/1/lists/853318/web_forms/1900114264", "http_etag": "\"ccb02527c3b7060d649ff8b611ccd4531d46cc85-ca5feee2b7fbb6febfca8af5541541ea960aaedb\"", "total_displays": 0, "resource_type_link": "https://api.aweber.com/1.0/#web_form", "id": 1900114264}, {"conversion_percentage": 0.0, "unique_conversion_percentage": 0.0, "name": "Form popup", "total_unique_displays": 0, "type": "inline", "is_active": true, "total_submissions": 0, "self_link": "https://api.aweber.com/1.0/accounts/1/lists/853318/web_forms/1938455405", "http_etag": "\"29e56cc073fdf5e9d5dcdcbef5ef9f4adcd0b98e-ca5feee2b7fbb6febfca8af5541541ea960aaedb\"", "total_displays": 0, "resource_type_link": "https://api.aweber.com/1.0/#web_form", "id": 1938455405}, {"conversion_percentage": 0.0, "unique_conversion_percentage": 0.0, "name": "My Web Form", "total_unique_displays": 0, "type": "styled", "is_active": true, "total_submissions": 0, "self_link": "https://api.aweber.com/1.0/accounts/1/lists/853318/web_forms/2082623301", "http_etag": "\"3023b1ad6b3e0fbc52129deb5a55ce1effd56bef-ca5feee2b7fbb6febfca8af5541541ea960aaedb\"", "total_displays": 0, "resource_type_link": "https://api.aweber.com/1.0/#web_form", "id": 2082623301}, {"conversion_percentage": 0.0, "unique_conversion_percentage": 0.0, "name": "Sign up for free newsletter", "total_unique_displays": 1, "type": "inline", "is_active": true, "total_submissions": 0, "self_link": "https://api.aweber.com/1.0/accounts/1/lists/853404/web_forms/1276584893", "http_etag": "\"a8ab161350d7e0d244913ff848f301c7ba66029e-ca5feee2b7fbb6febfca8af5541541ea960aaedb\"", "total_displays": 1, "resource_type_link": "https://api.aweber.com/1.0/#web_form", "id": 1276584893}, {"conversion_percentage": 0.0, "unique_conversion_percentage": 0.0, "name": "New styled form", "total_unique_displays": 1, "type": "styled", "is_active": true, "total_submissions": 0, "self_link": "https://api.aweber.com/1.0/accounts/1/lists/853399/web_forms/992299989", "http_etag": "\"669b36b1c356151329743774bff0edd1bd5a2877-ca5feee2b7fbb6febfca8af5541541ea960aaedb\"", "total_displays": 3, "resource_type_link": "https://api.aweber.com/1.0/#web_form", "id": 992299989}, {"conversion_percentage": 0.0, "unique_conversion_percentage": 0.0, "name": "Existing Styled form", "total_unique_displays": 0, "type": "styled", "is_active": true, "total_submissions": 0, "self_link": "https://api.aweber.com/1.0/accounts/1/lists/853399/web_forms/1716841568", "http_etag": "\"013c2ffd669a6dff74db540bcaaaebc2494ef682-ca5feee2b7fbb6febfca8af5541541ea960aaedb\"", "total_displays": 0, "resource_type_link": "https://api.aweber.com/1.0/#web_form", "id": 1716841568}, {"conversion_percentage": 0.0, "unique_conversion_percentage": 0.0, "name": "Legacy form", "total_unique_displays": 0, "type": "styled", "is_active": true, "total_submissions": 0, "self_link": "https://api.aweber.com/1.0/accounts/1/lists/853399/web_forms/2120561755", "http_etag": "\"c58a76526d5614ae99fac801207b18851b8f6284-ca5feee2b7fbb6febfca8af5541541ea960aaedb\"", "total_displays": 0, "resource_type_link": "https://api.aweber.com/1.0/#web_form", "id": 2120561755}]
diff --git a/tests/mock_adapter.py b/tests/mock_adapter.py
new file mode 100644
index 0000000..2b938f3
--- /dev/null
+++ b/tests/mock_adapter.py
@@ -0,0 +1,43 @@
+from aweber_api import AWeberUser
+import json
+import os
+
+class MockAdapter(object):
+
+ def __init__(self):
+ self.user = AWeberUser()
+
+ request_paths = {
+ '/accounts' : 'accounts',
+ '/accounts/1' : 'account',
+ '/accounts/1/lists' : 'lists',
+ '/accounts/1?ws.op=getWebForms' : 'web_forms',
+ '/accounts/1/lists?ws.start=20&ws.size=20' : 'lists_page2',
+ '/accounts/1/lists/303449' : 'lists/303449',
+ '/accounts/1/lists/303449/campaigns' : 'lists/303449/campaigns'
+ }
+
+ requests = []
+
+ def request(self, method, url, data={}):
+ if not (len(data.keys()) == 0):
+ first = True
+ for key in data.keys():
+ if first:
+ url +='?'
+ else:
+ url +='&'
+ first = False
+ url += key+'='+str(data[key])
+
+ if not url in self.request_paths:
+ return ''
+
+ path = "{0}{1}.json".format(self.data_dir,
+ self.request_paths[url])
+ return json.load(open(path))
+
+ @property
+ def data_dir(self):
+ s = os.sep
+ return s.join(__file__.split(s)[:-1]+['data',''])
diff --git a/tests/mock_adapter.pyc b/tests/mock_adapter.pyc
new file mode 100644
index 0000000..4dd517e
Binary files /dev/null and b/tests/mock_adapter.pyc differ
diff --git a/tests/test_aweber_ b/tests/test_aweber_
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_aweber_api.py b/tests/test_aweber_api.py
new file mode 100644
index 0000000..ff26c44
--- /dev/null
+++ b/tests/test_aweber_api.py
@@ -0,0 +1,105 @@
+from unittest import TestCase
+from mock_adapter import MockAdapter
+from dingus import Dingus
+from aweber_api import (AWeberAPI, AWeberUser, ACCESS_TOKEN_URL, AUTHORIZE_URL,
+ REQUEST_TOKEN_URL, AWeberEntry)
+
+key = 'XXXXX'
+secret = '3434534534534'
+class AWeberAPITest(TestCase):
+
+ def setUp(self):
+ self.aweber = AWeberAPI(key, secret)
+
+ def test_should_exist(self):
+ self.assertTrue(self.aweber)
+
+class WhenGettingARequestToken(AWeberAPITest):
+
+ def setUp(self):
+ AWeberAPITest.setUp(self)
+ self.response = "oauth_token=1234&oauth_token_secret=abcd"
+ self.aweber.adapter = Dingus()
+ self.aweber.adapter.user = AWeberUser()
+ self.aweber.adapter.request = Dingus(return_value=self.response)
+
+ def test_should_get_request_token(self):
+ token, secret = self.aweber.get_request_token('http://localhost/demo')
+ self.assertEqual(token, '1234')
+ self.assertEqual(secret, 'abcd')
+
+ def test_should_pass_args_to_request(self):
+ self.called = False
+ def _request(method, url, params):
+ self.assertEqual(url, REQUEST_TOKEN_URL)
+ self.assertEqual(method, 'POST')
+ self.assertEqual(params['oauth_callback'], 'http://localhost/demo')
+ self.called = True
+ return self.response
+ self.aweber.adapter.request = _request
+ token, secret = self.aweber.get_request_token('http://localhost/demo')
+ self.assertTrue(self.called, 'Called _request')
+
+ def test_should_set_up_user(self):
+ token, secret = self.aweber.get_request_token('http://localhost/demo')
+
+ self.assertEqual(self.aweber.user.request_token, token)
+ self.assertEqual(self.aweber.user.token_secret, secret)
+
+ def test_should_have_authorize_url(self):
+ token, secret = self.aweber.get_request_token('http://localhost/demo')
+ self.assertEqual(self.aweber.authorize_url,
+ "{0}?oauth_token={1}".format(AUTHORIZE_URL, token))
+
+
+class WhenGettingAnAccessToken(AWeberAPITest):
+
+ def setUp(self):
+ AWeberAPITest.setUp(self)
+ self.response = "oauth_token=cheeseburger&oauth_token_secret=hotdog"
+ self.aweber.adapter = Dingus()
+ self.aweber.adapter.user = AWeberUser()
+ self.aweber.adapter.request = Dingus(return_value=self.response)
+
+ self.aweber.user.request_token = '1234'
+ self.aweber.user.token_secret = 'abcd'
+ self.aweber.user.verifier = '234a35a1'
+
+
+ def test_should_get_access_token(self):
+ access_token, token_secret = self.aweber.get_access_token()
+ self.assertEqual(access_token, 'cheeseburger')
+ self.assertEqual(token_secret, 'hotdog')
+
+ def test_should_pass_args_to_request(self):
+ self.called = False
+ def _request(method, url, params={}):
+ self.assertEqual(url, ACCESS_TOKEN_URL)
+ self.assertEqual(method, 'POST')
+ self.assertEqual(params['oauth_verifier'], '234a35a1')
+ self.called = True
+ return self.response
+ self.aweber.adapter.request = _request
+ token, secret = self.aweber.get_access_token()
+ self.assertTrue(self.called, 'Called _request')
+
+ def test_should_set_up_user(self):
+ token, secret = self.aweber.get_access_token()
+ self.assertEqual(self.aweber.user.access_token, token)
+ self.assertEqual(self.aweber.user.token_secret, secret)
+
+class WhenGettingAnAccount(TestCase):
+
+ def setUp(self):
+ self.aweber = AWeberAPI(key, secret)
+ self.aweber.adapter = MockAdapter()
+
+ self.access_token = '1234'
+ self.token_secret = 'abcd'
+
+ def test_when_getting_an_account(self):
+ account = self.aweber.get_account(self.access_token, self.token_secret)
+ self.assertEqual(type(account), AWeberEntry)
+ self.assertEqual(account.id, 910)
+ self.assertEqual(account.type, 'account')
+
diff --git a/tests/test_aweber_api.pyc b/tests/test_aweber_api.pyc
new file mode 100644
index 0000000..4732a9d
Binary files /dev/null and b/tests/test_aweber_api.pyc differ
diff --git a/tests/test_aweber_collection.py b/tests/test_aweber_collection.py
new file mode 100644
index 0000000..51bac1e
--- /dev/null
+++ b/tests/test_aweber_collection.py
@@ -0,0 +1,37 @@
+from unittest import TestCase
+from aweber_api import AWeberAPI, AWeberCollection, AWeberEntry
+from mock_adapter import MockAdapter
+
+
+class TestAWeberCollection(TestCase):
+
+ def setUp(self):
+ self.aweber = AWeberAPI('1', '2')
+ self.aweber.adapter = MockAdapter()
+ self.lists = self.aweber.load_from_url('/accounts/1/lists')
+
+ def test_should_be_a_collection(self):
+ self.assertTrue(type(self.lists), AWeberCollection)
+
+ def test_should_have_24_lists(self):
+ self.assertTrue(len(self.lists), 24)
+
+ def test_should_be_able_get_each_via_offset(self):
+ for i in range(0, 23):
+ list = self.lists[i]
+ self.assertEqual(type(list), AWeberEntry)
+ self.assertEqual(list.type, 'list')
+
+ def test_should_be_able_to_iterate_on_collection(self):
+ list_number = 0
+ for list in self.lists:
+ self.assertEqual(type(list), AWeberEntry)
+ self.assertEqual(list.type, 'list')
+ list_number += 1
+ self.assertEqual(list_number, 24)
+
+ def test_should_support_get_by_id(self):
+ list = self.lists.get_by_id(303449)
+ self.assertEqual(type(list), AWeberEntry)
+ self.assertEqual(list.type, 'list')
+ self.assertEqual(list.id, 303449)
diff --git a/tests/test_aweber_collection.pyc b/tests/test_aweber_collection.pyc
new file mode 100644
index 0000000..7b358b9
Binary files /dev/null and b/tests/test_aweber_collection.pyc differ
diff --git a/tests/test_aweber_entry.py b/tests/test_aweber_entry.py
new file mode 100644
index 0000000..dd7d6a6
--- /dev/null
+++ b/tests/test_aweber_entry.py
@@ -0,0 +1,63 @@
+from unittest import TestCase
+from aweber_api import AWeberAPI, AWeberCollection, AWeberEntry
+from mock_adapter import MockAdapter
+import re
+
+class TestAWeberEntry(TestCase):
+
+ def setUp(self):
+ self.aweber = AWeberAPI('1', '2')
+ self.aweber.adapter = MockAdapter()
+ self.list = self.aweber.load_from_url('/accounts/1/lists/303449')
+
+ def test_should_be_an_entry(self):
+ self.assertEqual(type(self.list), AWeberEntry)
+ self.assertEqual(self.list.type, 'list')
+
+ def test_should_have_id(self):
+ self.assertEqual(self.list.id, 303449)
+
+ def test_should_have_other_properties(self):
+ self.assertEqual(self.list.name, 'default303449')
+
+ def test_should_have_child_collections(self):
+ campaigns = self.list.campaigns
+ self.assertEqual(type(campaigns), AWeberCollection)
+
+class TestAWeberAccountEntry(TestCase):
+
+ def setUp(self):
+ self.aweber = AWeberAPI('1', '2')
+ self.aweber.adapter = MockAdapter()
+ self.account = self.aweber.load_from_url('/accounts/1')
+
+ def test_should_be_an_entry(self):
+ self.assertEqual(type(self.account), AWeberEntry)
+ self.assertEqual(self.account.type, 'account')
+
+ def test_should_be_able_get_web_forms(self):
+ forms = self.account.get_web_forms()
+
+class TestAccountGetWebForms(TestAWeberAccountEntry):
+
+ def setUp(self):
+ TestAWeberAccountEntry.setUp(self)
+ self.forms = self.account.get_web_forms()
+
+ def test_should_be_a_list(self):
+ self.assertEqual(type(self.forms), list)
+
+ def test_should_have_23_web_forms(self):
+ self.assertEqual(len(self.forms), 23)
+
+ def test_each_should_be_entry(self):
+ for entry in self.forms:
+ self.assertEqual(type(entry), AWeberEntry)
+ self.assertEqual(entry.type, 'web_form')
+
+ def test_each_should_have_correct_url(self):
+ url_regex = '/accounts\/1\/lists\/\d*/web_forms/\d*'
+ for entry in self.forms:
+ self.assertTrue(re.match(url_regex, entry.url))
+
+
diff --git a/tests/test_aweber_entry.pyc b/tests/test_aweber_entry.pyc
new file mode 100644
index 0000000..a52c6ef
Binary files /dev/null and b/tests/test_aweber_entry.pyc differ