Client Library (How To)
The Social OS API is straightforward to use, particularly with a modern
HTTP library such as Python's Requests. But for advanced use,
particularly in applications that use admin accounts and stored s
essions, there are enough intricacies that it's valuable to have an API
client library to smooth the way. This example presents a simple API
client library for Python.
Our client module presents just two classes: API for the actual API
access, and Session for managing application sessions. This naming may be a
little confusing when first reading the code, as the API module also
manages sessions, in this case for efficient communication with the
Social OS API. These API sessions are generally short-lived,
and only used to bundle multiple requests into a single HTTPS
connection, to avoid the SSL setup delay.
The application sessions are designed to be stored in a local database.
Here we use MongoDB or TokuMX. Later we'll examine other options for
session storage, and how to create a pluggable session engine.
This version of the API client doesn't explicitly wrap the API classes
and methods, instead simply providing a convenient way to construct the
API requests and return the decoded results. Future articles will
examine more advanced client-side functionality.
Here is the client code:
# coding=utf-8
import datetime
import json
from hashlib import sha1
import requests
import database
from conf import apiurl, adminurl, adminuser, adminpass, securekey, ttl
from tools import decrypt
from errors import Unauthorized, Unimplemented
import cherrypy
class Session(object):
def __init__(self, id=None, data=None, token=None):
'''To initialise a session from user login, provide an id and data. To initialise
from session id, provide the key token. id is a hash of the token so that even
in the event of a security breach, it is not possible to extract both the token
and the password for a key.'''
if id and data:
session = database.Session.objects.find_one(id=id)
if session:
self.data = session.data
session.accessed = datetime.datetime.now()
session.save()
else:
raise Unauthorized
self.id = id
self.data = data
elif token:
id = sha1(token).hexdigest()
session = database.Session.objects.find_one(id=id)
if not session:
raise Unauthorized
self.id = id
self.data = session.data
else:
raise Unauthorized
@classmethod
def login(_class, user, password):
api = API()
key = api.post('key', data={'ttl': ttl}, user=user, password=password)
id = sha1(key['token']).hexdigest()
pcrypt = encrypt(key['password'], '%s%s' % (securekey, key['token']))
setcookie('session', key['token'])
expirecookie(['session'], ttl)
return _class.__init__(id, data={'auth': pcrypt, 'user': user})
def logout(self):
'''To log out, delete the API key, the session record, the data in the
session object, and the session cookie, n that order.'''
assert self.id, 'No session data available.'
api = API()
api.auth()
api.delete('key/%s' % cherrypy.response.cookie['session'])
database.Session.objects.delete_one(id=self.id)
self.id = None
self.data = None
delcookie('session')
def authdetails(self):
assert self.id, 'No session data available.'
session = database.Session.objects.find_one(id=self.id)
if session:
data = session.data
user = data['user']
password = decrypt(data['auth'], '%s%s' %
(securekey, cherrypy.response.cookie['session']))
return user, password
raise Unauthorized
class API(object):
def __init__(self):
self.session = requests.Session()
def auth(self, user=None, password=None):
if user and password:
self.session.auth(user, password)
else:
self.session.auth(Session().authdetails())
def admin(self, method, data=None, action='GET', adminuser=adminuser, adminpass=adminpass):
return self.call(method, adminuser, adminpass, data=data, action=action, base=adminurl)
def call(self, method, user=None, password=None, data=None, action='GET', base=apiurl):
if user and password:
self.session.auth(user, password)
url = '%s/%s' % (base, method)
return json.loads(requests.request(action, url, data=data))
@staticmethod
def easyadmin(method, data=None, action='GET', base=adminurl,
adminuser=adminuser, adminpass=adminpass):
session = requests.Session()
session.auth(adminuser, adminpass)
url = '%s/%s' % (base, method)
return json.loads(requests.request(action, url, data=data))
@staticmethod
def easy(method, data=None, action='GET', base=apiurl):
session = requests.Session()
session.auth(Session().authdetails())
url = '%s/%s' % (base, method)
return json.loads(requests.request(action, url, data=data))
get = call
def post(self, method, action='POST', **kw):
self.call(method, action=action, **kw)
def put(self, method, action='PUT', **kw):
self.call(method, action=action, **kw)
def patch(self, method, action='PATCH', **kw):
self.call(method, action=action, **kw)
def delete(self, method, action='DELETE', **kw):
self.call(method, action=action, **kw)
def search(self, method, action='SEARCH', **kw):
self.call(method, action=action, **kw)
And here are the database definitions, using the MongoEngine library:
# coding=utf-8
from conf import dbname
from MongoEngine import *
class Session(Document):
id = StringField()
data = DictField()
accessed = DateTimeField()
meta = {'indexes': [{'fields': ['accessed'], 'expireAfterSeconds': 3600, 'unique': False},
],
}
connect(dbname)
The tools module is a simple collection of functions that helps to
remove duplicate code and centralise dependencies:
# coding=utf-8
import base64
import cherrypy
from Crypto.Cipher import AES
from conf import securekey
assert securekey, 'Required security settings missing in conf.py.'
log = cherrypy.log
start = cherrypy.quickstart
def encrypt(cleartext, key=securekey):
secret = AES.new(key[:32])
paddedtext = (str(cleartext) + (AES.block_size - len(str(cleartext)) % AES.block_size) * '\0')
ciphertext = base64.b64encode(secret.encrypt(paddedtext))
return ciphertext
def decrypt(ciphertext, key=securekey):
secret = AES.new(key[:32])
decrypted = secret.decrypt(base64.b64decode(ciphertext))
cleartext = decrypted.rstrip('\0')
return cleartext
def getcookie(name):
return cherrypy.response.cookie[name]
def setcookie(name, value):
cherrypy.response.cookie[name] = value
def expirecookie(name, ttl):
cherrypy.response.cookie[name]['expires'] = ttl
Updated over 7 years ago