# -*- coding: utf-8 -*-
"""
################################
UNB Platform API Client Library
################################
"""
from __future__ import print_function
import urlparse
import requests
# =============================================================================
# Errors
# =============================================================================
class UNBPlatformError(Exception):
"""Base error for UNBPlatform client."""
pass
class UNBPlatformUnauthorized(UNBPlatformError):
"""401"""
pass
# =============================================================================
# Client
# =============================================================================
class TokenAPIClient(object):
def __init__(self, api_root, client_id, client_secret, token_url,
token=None, refresh_token=None):
if api_root[-1] != '/':
api_root = api_root + '/'
self.__api_root = api_root
self.__client_id = client_id
self.__client_secret = client_secret
self.__token_url = token_url
self.__token = token
self.__refresh_token = refresh_token
def _make_path(self, path):
"""Create a path."""
if not isinstance(path, basestring):
# Path is (hopefully) an iterable
#
path = '/'.join([str(part) for part in path])
if not path.endswith('/'):
path = path + '/'
return urlparse.urljoin(self.__api_root, path)
def get_token(self, username, password):
"""Login a user and obtain an access token.
Parameters
----------
username : str
The user's username, email or other identifier used for logging in.
password : str
The user's plain-text password.
Returns
-------
str
The access token that should be used on subsequent requests.
Raises
------
UNBPlatformUnauthorized
If the login attempt was unsuccessful.
"""
data = {
'grant_type': 'password',
'username': username,
'password': password,
}
r = requests.post(self.__token_url,
auth=(self.__client_id, self.__client_secret),
data=data)
if r.status_code == 401:
raise UNBPlatformUnauthorized
if r.status_code == 200:
d = r.json()
self.__token = d.get('access_token')
self.__refresh_token = d.get('refresh_token')
return self.__token
raise UNBPlatformError
def request(self, method, path, **kwargs):
"""Make an API request.
Parameters
----------
method : str
HTTP method to use ('get', 'post', 'patch', 'options', 'etc.')
path : Union(str : list)
The path to request. The path can be an absolute url, relative
url, or an iterable of path-parts.
params : dict
Query arguments to be urlencoded and added to the path.
.. NOTE:: Any key set to ``None`` will not be included.
**kwargs
Additional keyword arguments that will be passed to
``requests.request``.
Returns
-------
requests.Response
The response received.
"""
# Convert data to json (this may not be necessary?)
#
# if 'data' in kwargs and isinstance(kwargs['data'], (dict, list)):
# kwargs['data'] = json.dumps(kwargs['data'])
full_path = self._make_path(path)
# TODO(nick): The headers should be merged... but I don't want to think
# about that at the moment.
#
if 'headers' in kwargs:
raise NotImplemented
# Create the (default) headers for the request
#
headers = {
# The content-type we're sending
#
'Content-Type': 'application/json',
# The content-type we will accept as a response
#
'Accept': 'application/json',
}
if self.__token:
headers['Authorization'] = 'Bearer ' + self.__token
# TODO(nick): What to do about requests exceptions (Timeout,
# ConnectionError, etc.)?
#
# http://docs.python-requests.org/en/latest/api/#exceptions
#
# add params
response = requests.request(method, full_path, headers=headers,
**kwargs)
if response.status_code == 401:
raise UNBPlatformUnauthorized
return response
# =============================================================================
# Resource
# =============================================================================
class SimpleResource(object):
def __init__(self, data=None): # , client=None, resource_path=None):
self._data = data or {}
@staticmethod
def _decode(response):
return response.json()
@classmethod
def _list(cls, **kwargs):
resp = cls._client.request('get', cls._resource_path, params=kwargs)
return cls(cls._decode(resp))
@classmethod
def _create(cls, data, params=None, **kwargs):
resp = cls._client.request('post', cls._resource_path, params=params,
data=data, **kwargs)
return cls(cls._decode(resp))
@classmethod
def _read(cls, pk, params=None, **kwargs):
resp = cls._client.request('get', [cls._resource_path, pk],
params=params, **kwargs)
return cls(cls._decode(resp))
def _update(self, pk, data, params=None, **kwargs):
raise NotImplemented
# resp = self._client.request('patch', [self._resource_path, pk],
# data=data, params=params, **kwargs)
# return cls(cls._decode(resp))
class HALResource(SimpleResource):
def __init__(self, hal=None):
self._hal = hal or {}
def __getattr__(self, attr):
return self._hal.get(attr)
def _get_link(self, name):
return self._hal.get('_links').get(name)
def _get_embedded(self, name=None, resource=None):
embedded_resources = self._hal.get('_embedded')
if name:
embedded_resource = embedded_resources.get(
name, embedded_resources.get('items'))
else:
return embedded_resources
if resource:
return [resource(hal=r) for r in embedded_resource]
else:
if not name:
return None
return embedded_resource
@classmethod
def _list(cls, _path=None, **kwargs):
path = _path or cls._resource_path
resp = cls._client.request('get', path, params=kwargs)
hal = cls._decode(resp)
hal_resource_list = HALResourceList(hal, cls._profile, cls)
return hal_resource_list
class HALResourceList(HALResource):
def __init__(self, hal, profile, resource_cls):
super(HALResourceList, self).__init__(hal=hal)
self._hal = hal
self._resource_cls = resource_cls
self._profile = profile
embedded_resource = self._get_embedded(profile, resource_cls)
self._resource_list = embedded_resource
def __getitem__(self, key):
return self._resource_list.__getitem__(key)
def __getslice__(self, i, j):
return self._resource_list.__getslice__(i, j)
def __iter__(self):
return self._resource_list.__iter__()
# ======================================================================
# UNB Platform Resources
# ======================================================================
class Article(HALResource):
_profile = 'unb:article'
_resource_path = 'write/articles'
@classmethod
def _list_public(cls, **kwargs):
public_path = 'write/articles/public'
return cls._list(_path=public_path, **kwargs)
class ArticleComment(HALResource):
_profile = 'unb:article-comment'
_resource_path = 'write/article-comments'
class UNBPlatform(object):
def __init__(self, client_config=None):
client = self.make_client(client_config)
self._client = client
self.resources = {}
self.Article = self._make_resource(Article)
self.ArticleComment = self._make_resource(ArticleComment)
def _make_resource(self, resource_cls, profile=None, path=None):
if profile:
setattr(resource_cls, '_profile', profile)
if path:
setattr(resource_cls, '_resource_path', path)
setattr(resource_cls, '_client', self._client)
self.resources[resource_cls._profile] = resource_cls
return resource_cls
def make_client(self, client_config):
client = TokenAPIClient(**client_config)
# client.get_token(None, None)
return client
def get_token(self, username, password):
return self._client.get_token(username, password)