diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..e5c9569
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,20 @@
+Copyright (c) 2013 Leandigo (www.leandigo.com)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 0000000..9c8317c
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1,2 @@
+include LICENSE
+include README.rst
\ No newline at end of file
diff --git a/README.rst b/README.rst
new file mode 100644
index 0000000..1042f44
--- /dev/null
+++ b/README.rst
@@ -0,0 +1,220 @@
+pyoneall - OneAll API Wrapper
+=============================
+
+**OneAll** (|oneall|_) provides web-applications with a unified API for **20+ social networks**.
+**pyoneall** provides developers with OneAll accounts a simple interface with the OneAll API for Python-based web-applications.
+
+Disclaimer
+----------
+*This package is new, and so far has been tested in a development of a small number of projects.*
+*Please be sure to test all edge-cases where this package is used with your application!*
+
+Implementation Overview
+-----------------------
+OneAll API documentation is available at |onealldoc|_. However, in order to use pyoneall with your application, it's
+enough to read the docs for the Connection API: `Connection API Documentation`_.
+
+So far, we have tested pyoneall within Flask and Django apps. To use OneAll as a Django authentication backend,
+please check out our ``django_oneall`` project, which relies on this package.
+
+pyoneall defines the ``OneAll`` class, which is the API client. As of now, it has the following methods:
+
+:``connections()``: Get a list of social connections to the site
+:``connection()``: Get detailed connection data including user details
+:``users()``: Get a list of users that have connected with the site
+:``user()``: Get detailed user data
+:``user_contacts()``: Get a list of user's contacts
+:``publish()``: Publish a message using user's social network account
+
+As pyoneall wraps a REST API which returns JSON objects, the objects returned by the methods behave in a somewhat
+JavaScript-like manner. This means that in addition to the ``dict``-style ``object['key']`` notation, you can also
+use ``object.key``.
+
+Also, arrays nested in the JSON responses, are represented by a class that defines a ``by_*()`` grouping and searching
+method in an addition to the ``list`` methods it inherits from.
+
+For more information on these classes, check out ``help(pyoneall.base.OADict)`` and ``help(pyoneall.base.OAList)``.
+
+Example
+-------
+
+Authentication
+~~~~~~~~~~~~~~
+Access to the OneAll API requires authentication. Obtain your API credentials following the procedure described at
+`Authentication Documentation`_.
+
+Create an instance of the OneAll client::
+
+ from pyoneall import OneAll
+
+ oa = OneAll(
+ site_name='NAME OF YOUR ONEALL SITE',
+ public_key='PUBLIC KEY OF YOUR SITE',
+ private_key='PRIVATE KEY OF YOUR SITE'
+ )
+
+The Connection API
+~~~~~~~~~~~~~~~~~~
+Fetching connections lists
+**************************
+::
+
+ connections = oa.connections()
+
+``connections`` now contains the "connections" portion of the result of the API call, as described in
+``_.
+Full response data (for debugging and whatnot) is in ``connections.response``.
+
+OneAll uses pagination for calls which contain many entries. Each call returns a page up to 500 entries. When the
+``OneAll.connections()`` method is executed without arguments, only the first page is loaded. You the access the
+pagination information in ``connections.pagination``.
+
+In order to load a custom range of pages, you can do something like::
+
+ connections = oa.connections(first_page=3, last_page=6)
+
+Or, if you want to load all pages, use::
+
+ connections = oa.connections(fetch_all=True)
+
+Of course, this will result in multiple API calls.
+
+The connections list itself is in ``connections.entries``::
+
+ >>> connections.entries
+ [{u'callback_uri': u'http://www.example.com/connect/',
+ u'connection_token': u'cf2fffc7-34dc-484e-95cd-13f8ab838e22',
+ u'date_creation': u'Sun, 23 Jun 2013 14:12:43 +0200',
+ u'status': u'succeeded'},
+ {u'callback_uri': u'http://m.example.com/connect/',
+ u'connection_token': u'4276bd23-3605-4679-acd2-963148c477cc',
+ u'date_creation': u'Sun, 23 Jun 2013 14:13:20 +0200',
+ u'status': u'succeeded'},
+ {u'callback_uri': u'http://www.example.com/connect/',
+ u'connection_token': u'58ad2a04-ed1e-4799-a3ca-2b26651e35a0',
+ u'date_creation': u'Sun, 23 Jun 2013 14:18:00 +0200',
+ u'status': u'succeeded'},
+ {u'callback_uri': u'http://m.example.com/connect/',
+ u'connection_token': u'e5231790-c6dc-4ce8-9922-792a2aebbba2',
+ u'date_creation': u'Sun, 23 Jun 2013 14:18:11 +0200',
+ u'status': u'succeeded'},
+ {u'callback_uri': u'http://www.example.com/connect/',
+ u'connection_token': u'f82ad1e5-113f-46a2-b1c5-2a57a6002401',
+ u'date_creation': u'Sun, 23 Jun 2013 14:21:14 +0200',
+ u'status': u'succeeded'}]
+
+In the example above, you can see that some connections were made with the callback of the desktop website
+(``http://www.example.com/connect/``), and some were made with the mobile webapp (``http://m.example.com/connect/``).
+We can get an object grouped by the "callback_uri" using::
+
+ >>> connections.entries.by_callback_uri()
+ {u'http://www.example.com/connect/': [
+ {u'callback_uri': u'http://www.example.com/connect/',
+ u'connection_token': u'cf2fffc7-34dc-484e-95cd-13f8ab838e22',
+ u'date_creation': u'Sun, 23 Jun 2013 14:12:43 +0200',
+ u'status': u'succeeded'},
+ {u'callback_uri': u'http://www.example.com/connect/',
+ u'connection_token': u'58ad2a04-ed1e-4799-a3ca-2b26651e35a0',
+ u'date_creation': u'Sun, 23 Jun 2013 14:18:00 +0200',
+ u'status': u'succeeded'}],
+ {u'callback_uri': u'http://www.example.com/connect/',
+ u'connection_token': u'f82ad1e5-113f-46a2-b1c5-2a57a6002401',
+ u'date_creation': u'Sun, 23 Jun 2013 14:21:14 +0200',
+ u'status': u'succeeded'},
+ u'http://m.example.com/connect/': [
+ {u'callback_uri': u'http://m.example.com/connect/',
+ u'connection_token': u'4276bd23-3605-4679-acd2-963148c477cc',
+ u'date_creation': u'Sun, 23 Jun 2013 14:13:20 +0200',
+ u'status': u'succeeded'},
+ {u'callback_uri': u'http://m.example.com/connect/',
+ u'connection_token': u'e5231790-c6dc-4ce8-9922-792a2aebbba2',
+ u'date_creation': u'Sun, 23 Jun 2013 14:18:11 +0200',
+ u'status': u'succeeded'}]}
+
+Or get a list of connections with a specific "callback_uri"::
+
+ >>> connections.entries.by_callback_uri('http://m.example.com/connect/')
+ [{u'callback_uri': u'http://m.example.com/connect/',
+ u'connection_token': u'4276bd23-3605-4679-acd2-963148c477cc',
+ u'date_creation': u'Sun, 23 Jun 2013 14:13:20 +0200',
+ u'status': u'succeeded'},
+ {u'callback_uri': u'http://m.example.com/connect/',
+ u'connection_token': u'e5231790-c6dc-4ce8-9922-792a2aebbba2',
+ u'date_creation': u'Sun, 23 Jun 2013 14:18:11 +0200',
+ u'status': u'succeeded'}]
+
+Reading connection details
+**************************
+In order to get the **user_token** and the user's social identity you can pass a **connection_token** to the
+``connection()`` method of the ``OneAll`` instance::
+
+ some_connection = oa.connection('e5231790-c6dc-4ce8-9922-792a2aebbba2')
+
+Or, alternatively you can fetch the connection details through the ``connection()`` method of an entry in the list
+of connections::
+
+ some_connection = connections.entries[3].connection()
+
+``some_connection`` will now contain the "connection" portion of the response described in the API documentation for
+`Read Connection Details`_, most importantly ``some_connection.user`` and ``some_connection.user.user_token``
+
+The User API
+~~~~~~~~~~~~
+Fetching user list
+******************
+``OneAll.users()`` behaves the same way ``OneAll.connections()`` does, arguments and all. This is due to the similarity
+of the List Users and the List Connections API, in terms of pagination and entries structure.
+::
+
+ users = oa.users()
+
+Now, you can access ``users.entries``, or even access detailed user data with ``users.entries[4].user()``.
+
+Reading user details
+********************
+Read user details using::
+
+ user_token = some_connection.user.user_token
+ some_user = oa.user(user_token)
+
+``some_user`` will contain the "user" portion of the response detailed at
+``_.
+
+Reading user's contacts
+***********************
+You can get the user's contacts (depending on the social network) with::
+
+ contacts = some_user.contacts()
+
+or, with::
+
+ contacts = oa.user_contacts(user_token)
+
+Publishing content on user's behalf
+***********************************
+First, you need to format a message as described at ``_.
+Afterwards, publish it using ``publish()``::
+
+ message = {
+ 'request': {
+ 'message': {
+ 'parts': {
+ 'text': {
+ 'body': 'Hello World!' }}}}}
+
+ oa.publish(user_token, message)
+
+License
+-------
+Copyright (c) 2013, Leandigo (|leandigo|_)
+Released under the MIT License. See the LICENSE file for details.
+
+.. |leandigo| replace:: www.leandigo.com
+.. _leandigo: http://www.leandigo.com
+.. |oneall| replace:: http://www.oneall.com
+.. _oneall: http://www.oneall.com
+.. |onealldoc| replace:: http://docs.oneall.com
+.. _onealldoc: http://docs.oneall.com
+.. _Connection API Documentation: http://docs.oneall.com/api/resources/connections/
+.. _Authentication Documentation: http://docs.oneall.com/api/basic/authentication/
+.. _Read Connection Details: http://docs.oneall.com/api/resources/connections/read-connection-details/
\ No newline at end of file
diff --git a/pyoneall/__init__.py b/pyoneall/__init__.py
new file mode 100644
index 0000000..b459761
--- /dev/null
+++ b/pyoneall/__init__.py
@@ -0,0 +1 @@
+from connection import OneAll
\ No newline at end of file
diff --git a/pyoneall/base.py b/pyoneall/base.py
new file mode 100644
index 0000000..52fb2ef
--- /dev/null
+++ b/pyoneall/base.py
@@ -0,0 +1,82 @@
+class OAList(list):
+ """
+ A List of representing a JSON array with nested objects.
+ """
+ each = None
+
+ def __init__(self, init_list=None):
+ """
+ A constructor which receives an iterable, and recursively converts all nested list and dict objects to
+ OAList and OADict respectively. It also creates by_* methods allowing to perform grouping and search
+ on OADict objects within the list.
+ If subclass has the `each` attribute set to a class, all objects in list will be initialized as instances
+ of that class.
+
+ :param iterable init_list: Values to initiate list with.
+ """
+ if self.each:
+ super(OAList, self).__init__([self.each(**item) for item in init_list])
+ else:
+ super(OAList, self).__init__([_convert_base(item) for item in init_list])
+
+ self._dict_items = [item for item in self if isinstance(item, OADict)]
+
+ for key in set().union(*[item.keys() for item in self._dict_items]):
+ setattr(self, 'by_%s' % key, self._by_attr(key))
+
+ def _by_attr(self, attr):
+ """
+ Create a custom by_attr function for grouping and searching
+ """
+ def by_attr(query=None, first=False):
+ if query:
+ ret = []
+ for item in self._dict_items:
+ if item.get(attr) == query:
+ if first:
+ return item
+ ret.append(item)
+ return ret
+
+ else:
+ ret = {}
+ [ret.setdefault(item.get(attr), []).append(item) for item in self._dict_items
+ if not (isinstance(item.get(attr), OADict) or isinstance(item.get(attr), OAList))]
+ return ret
+ return by_attr
+
+ def drop_nulls(self):
+ while self.count(None):
+ self.remove(None)
+ [item.drop_nulls() for item in self if isinstance(item, OADict) or isinstance(item, OAList)]
+
+class OADict(dict):
+ """
+ A JavaScript-object-like representing a JSON object with nested objects
+ """
+ ignored = ['response', 'oneall']
+
+ def __init__(self, *args, **kwargs):
+ super(OADict, self).__init__(
+ *args,
+ **dict((key, _convert_base(value)) for key, value in kwargs.iteritems())
+ )
+ [setattr(self, key, value) for key, value in self.iteritems()]
+ self._populate()
+
+ def __setattr__(self, key, value):
+ super(OADict, self).__setattr__(key, value)
+ if key not in self.ignored: super(OADict, self).__setitem__(key, value)
+
+ def __setitem__(self, key, value):
+ super(OADict, self).__setitem__(key, value)
+ super(OADict, self).__setattr__(key, value)
+
+ def _populate(self):
+ pass
+
+def _convert_base(item):
+ return {
+ dict : lambda: OADict(**item),
+ list : lambda: OAList(item),
+ }.get(type(item), lambda: item)()
diff --git a/pyoneall/classes.py b/pyoneall/classes.py
new file mode 100644
index 0000000..b4282df
--- /dev/null
+++ b/pyoneall/classes.py
@@ -0,0 +1,81 @@
+from base import OAList, OADict
+
+class Users(OADict):
+ """
+ Represents a /users/ OneAll API call
+ """
+ class UsersEntries(OAList):
+ """
+ Represents the `entries` attribute
+ """
+ class UsersEntry(OADict):
+ """
+ Represents each object within `entries`
+ """
+ pass
+
+ def user(self):
+ """
+ Returns full user data for user_token in entry
+ """
+ if self.oneall:
+ return self.oneall.user(self.user_token)
+
+ each = UsersEntry
+
+ @property
+ def entries(self):
+ return getattr(self, '_entries', [])
+
+ @entries.setter
+ def entries(self, value):
+ self._entries = Users.UsersEntries(value)
+
+class Connections(OADict):
+ """
+ Represents the /connections/ OneAll API call
+ """
+
+ class ConnectionsEntries(OAList):
+ """
+ Represents the `entries` attribute
+ """
+
+ class ConnectionsEntry(OADict):
+ """
+ Represents each object within `entries`
+ """
+
+ def connection(self):
+ """
+ Returns full connection data for connection_token in entry
+ """
+ if self.oneall:
+ return self.oneall.connection(self.connection_token)
+
+ each = ConnectionsEntry
+
+ @property
+ def entries(self):
+ return getattr(self, '_entries', [])
+
+ @entries.setter
+ def entries(self, value):
+ self._entries = Connections.ConnectionsEntries(value)
+
+class Connection(OADict):
+ """
+ A OneAll Connection
+ """
+ pass
+
+class User(OADict):
+ """
+ A OneAll User
+ """
+ def contacts(self):
+ """
+ Retrieve user's contacts
+ """
+ if self.oneall:
+ return self.oneall.user_contacts(self.user_token)
\ No newline at end of file
diff --git a/pyoneall/connection.py b/pyoneall/connection.py
new file mode 100644
index 0000000..9eedfb3
--- /dev/null
+++ b/pyoneall/connection.py
@@ -0,0 +1,156 @@
+from urllib2 import Request, urlopen
+from base64 import encodestring
+from json import dumps, loads
+from base import OADict
+from classes import Users, Connections, Connection, User
+
+class OneAll():
+ """
+ A worker for the OneAll REST API.
+ """
+ DEFAULT_API_DOMAIN = 'https://{site_name}.api.oneall.com'
+ FORMAT__JSON = 'json'
+
+ bindings = {}
+
+ def __init__(self, site_name, public_key, private_key, base_url=None):
+ """
+ :param str site_name: The name of the OneAll site
+ :param str public_key: API public key for the site
+ :param str private_key: API private key for the site
+ :param str base_url: An alternate format for the API URL
+ """
+ self.base_url = base_url if base_url else OneAll.DEFAULT_API_DOMAIN.format(site_name=site_name)
+ self.public_key = public_key
+ self.private_key = private_key
+
+ def _exec(self, action, params={}, post_params=None):
+ """
+ Execute an API action
+
+ :param str action: The action to be performed. Translated to REST call
+ :param str params: Additional GET parameters for action
+ :post_params: POST parameters for action
+ :returns dict: The JSON result of the call in a dictionary format
+ """
+ request_url = '%s/%s.%s' % (self.base_url, action, OneAll.FORMAT__JSON)
+ for ix, (param, value) in enumerate(params.iteritems()):
+ request_url += "%s%s=%s" % (('?' if ix == 0 else '&'), param, value)
+ req = Request(request_url, dumps(post_params) if post_params else None, {'Content-Type': 'application/json'})
+ auth = encodestring('%s:%s' % (self.public_key, self.private_key)).replace('\n', '')
+ req.add_header('Authorization', 'Basic %s' % auth)
+ return loads(urlopen(req).read())
+
+ def _paginated(self, action, data, page_number=1, last_page=1, fetch_all=False, rtype=OADict):
+ """
+ Wrapper for paginated API calls. Constructs a response object consisting of one or more pages for paginated
+ calls such as /users/ or /connections/. Returned object will have the ``pagination`` attribute equaling the
+ the ``pagination`` value of the last page that was loaded.
+
+ :param str action: The action to be performed.
+ :param str data: The data attribute that holds the response payload
+ :param int page_number: The first page number to load
+ :param int last_page: The last page number to load
+ :param bool fetch_all: Whether to fetch all records or not
+ :param type rtype: The return type of the of the method
+ :returns OADict: The API call result
+ """
+ oa_object = rtype()
+ while page_number <= last_page or fetch_all:
+ response = OADict(**self._exec(action, {'page' : page_number})).response
+ page = getattr(response.result.data, data)
+ oa_object.count = getattr(oa_object, 'count', 0) + getattr(page, 'count', 0)
+ oa_object.entries = getattr(oa_object, 'entries', []) + getattr(page, 'entries', [])
+ oa_object.pagination = page.pagination
+ oa_object.response = response
+ page_number += 1
+ if page.pagination.current_page == page.pagination.total_pages:
+ break
+ return oa_object
+
+ def users(self, page_number=1, last_page=1, fetch_all=False):
+ """
+ Get users
+
+ :param int page_number: The first page number to load
+ :param int last_page: The last page number to load
+ :param bool fetch_all: Whether to fetch all records or not
+ :returns Users: The users objects
+ """
+ users = self._paginated('users', 'users', page_number, last_page, fetch_all, Users)
+ users.oneall = self
+ [setattr(entry, 'oneall', self) for entry in users.entries]
+ return users
+
+ def user(self, user_token):
+ """
+ Get a user by user token
+
+ :param str user_token: The user token
+ :returns User: The user object
+ """
+ response = OADict(**self._exec('users/%s' % (user_token))).response
+ user = User(**response.result.data.user)
+ user.response = response
+ user.oneall = self
+ return user
+
+ def user_contacts(self, user_token):
+ """
+ Get user's contacts by user token
+
+ :param str user_token: The user token
+ :returns OADict: User's contacts object
+ """
+ response = OADict(**self._exec('users/%s/contacts' % (user_token))).response
+ user_contacts = OADict(**response.result.data.identities)
+ user_contacts.response = response
+ return user_contacts
+
+ def connections(self, page_number=1, last_page=1, fetch_all=False):
+ """
+ Get connections
+
+ :param int page_number: The first page number to load
+ :param int last_page: The last page number to load
+ :param bool fetch_all: Whether to fetch all records or not
+ :returns Users: The connections
+ """
+ connections = self._paginated('connections', 'connections', page_number, last_page, fetch_all, rtype=Connections)
+ connections.oneall = self
+ [setattr(entry, 'oneall', self) for entry in connections.entries]
+ return connections
+
+ def connection(self, connection_token):
+ """
+ Get connection details by connection token
+
+ :param str connection_token: The connection token
+ :returns Connection: The requested connection
+ """
+ response = OADict(**self._exec('connection/%s' % (connection_token))).response
+ connection = Connection(**response.result.data)
+ connection.response = response
+ return connection
+
+ def user_contacts(self, user_token):
+ """
+ Get user's contacts
+
+ :param str user_token: The user_token of the user whose contacts are to be fetched
+ :returns OADict: The user's contacts
+ """
+ response = OADict(**self._exec('users/%s/contacts' % (user_token))).response
+ contacts = OADict(**response.result.data.identities)
+ contacts.response = response
+ return contacts
+
+ def publish(self, user_token, post_params):
+ """
+ Publish a message on behalf of the user
+
+ :param str user_token: The user token
+ :param dict post_params: The message in the format described in OneAll documentation
+ :returns OADict: The API response
+ """
+ return OADict(**self._exec('users/%s/publish' % user_token, post_params=post_params))
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..05c2e3e
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,13 @@
+import os
+from distutils.core import setup
+README = open(os.path.join(os.path.dirname(__file__), 'README.rst')).read()
+setup(name='pyoneall',
+ version='0.1',
+ packages=['pyoneall'],
+ license='MIT License, see LICENSE file',
+ description='OneAll API wrapper (http://www.oneall.com). Provides unified API for 20+ social networks',
+ long_description=README,
+ url='http://www.leandigo.com/',
+ author='Michael Greenberg / Leandigo',
+ author_email='michael@leandigo.com'
+)
\ No newline at end of file