Skip to content

Commit

Permalink
Merge pull request #151 from AltSchool/feature/api-client-edge
Browse files Browse the repository at this point in the history
Feature/api client edge
  • Loading branch information
aleontiev authored Feb 7, 2017
2 parents dd72c83 + c3dc784 commit eb0a253
Show file tree
Hide file tree
Showing 21 changed files with 971 additions and 101 deletions.
1 change: 1 addition & 0 deletions dynamic_rest/client/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .client import DRESTClient # noqa
190 changes: 190 additions & 0 deletions dynamic_rest/client/client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
import json
import requests
from .exceptions import AuthenticationFailed, BadRequest, DoesNotExist
from .resource import DRESTResource
from dynamic_rest.conf import settings


class DRESTClient(object):
"""DREST Python client.
Exposes a DREST API to Python using a Django-esque interface.
Resources are available on the client through access-by-name.
Arguments:
host: hostname to a DREST API
version: version (defaults to no version)
client: HTTP client (defaults to requests.session)
scheme: defaults to https
authentication: if unset, authentication is disabled.
If set, provides credentials: {
usename: login username,
password: login password,
token: authorization token,
cookie: session cookie
}
Either username/password, token, or cookie should be provided.
Examples:
Assume there is a DREST resource at "https://my.api.io/v0/users",
and that we can access this resource with an auth token "secret".
Getting a client:
client = DRESTClient(
'my.api.io',
version='v0',
authentication={'token': 'secret'}
)
Getting a single record of the Users resource
client.Users.get('123')
Getting all records (automatic pagination):
client.Users.all()
Filtering records:
client.Users.filter(name__icontains='john')
other_users = client.Users.exclude(name__icontains='john')
Ordering records:
users = client.Users.sort('-name')
Including / excluding fields:
users = client.Users.all()
.excluding('birthday')
.including('events.*')
.get('123')
Mapping by field:
users_by_id = client.Users.map()
users_by_name = client.Users.map('name')
Updating records:
user = client.Users.first()
user.name = 'john'
user.save()
Creating records:
user = client.Users.create(name='john')
"""
def __init__(
self,
host,
version=None,
client=None,
scheme='https',
authentication=None
):
self._host = host
self._version = version
self._client = client or requests.session()
self._client.headers.update({
'Content-Type': 'application/json',
'Accept': 'application/json'
})
self._resources = {}
self._scheme = scheme
self._authenticated = True
self._authentication = authentication

if authentication:
self._authenticated = False
token = authentication.get('token')
cookie = authentication.get('cookie')
if token:
self._use_token(token)
if cookie:
self._use_cookie(cookie)

def __repr__(self):
return '%s%s' % (
self._host,
'/%s/' % self._version if self._version else ''
)

def _use_token(self, value):
self._token = value
self._authenticated = bool(value)
self._client.headers.update({
'Authorization': '%s %s' % (
settings.AUTH_TYPE, self._token if value else ''
)
})

def _use_cookie(self, value):
self._cookie = value
self._authenticated = bool(value)
self._client.headers.update({
'Cookie': '%s=%s' % (settings.AUTH_COOKIE_NAME, value)
})

def __getattr__(self, key):
key = key.lower()
return self._resources.get(key, DRESTResource(self, key))

def _login(self, raise_exception=True):
username = self._username
password = self._password
response = requests.post(
self._build_url(settings.AUTH_LOGIN_ENDPOINT),
data={
'login': username,
'password': password
},
allow_redirects=False
)
if raise_exception:
response.raise_for_status()

self._use_cookie(response.cookies.get(settings.AUTH_COOKIE_NAME))

def _authenticate(self, raise_exception=True):
response = None
if not self._authenticated:
self._login(self._username, self._password, raise_exception)
if raise_exception and not self._authenticated:
raise AuthenticationFailed(
response.text if response else 'Unknown error'
)
return self._authenticated

def _build_url(self, url, prefix=None):
if not url.startswith('/'):
url = '/%s' % url

if prefix:
if not prefix.startswith('/'):
prefix = '/%s' % prefix

url = '%s%s' % (prefix, url)
return '%s://%s%s' % (self._scheme, self._host, url)

def request(self, method, url, params=None, data=None):
self._authenticate()
response = self._client.request(
method,
self._build_url(url, prefix=self._version),
params=params,
data=data
)

if response.status_code == 401:
raise AuthenticationFailed()

if response.status_code == 404:
raise DoesNotExist()

if response.status_code >= 400:
raise BadRequest()

return json.loads(response.content.decode('utf-8'))
10 changes: 10 additions & 0 deletions dynamic_rest/client/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
class DoesNotExist(Exception):
pass


class AuthenticationFailed(Exception):
pass


class BadRequest(Exception):
pass
173 changes: 173 additions & 0 deletions dynamic_rest/client/query.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
from copy import copy
from dynamic_rest.utils import unpack
from six import string_types


class DRESTQuery(object):

def __init__(
self,
resource=None,
filters=None,
orders=None,
includes=None,
excludes=None,
extras=None
):
self.resource = resource
self.filters = filters or {}
self.includes = includes or []
self.excludes = excludes or []
self.orders = orders or []
self.extras = extras or {}
# disable sideloading for easy loading
self.extras['sideloading'] = 'false'
# enable debug for inline types
self.extras['debug'] = 'true'
self._reset()

def __repr__(self):
return 'Query: %s' % self.resource.name

def all(self):
return self._copy()

def list(self):
return list(self)

def first(self):
l = self.list()
return l[0] if l else None

def last(self):
l = self.list()
return l[-1] if l else None

def map(self, field='id'):
return dict((
(getattr(k, field), k) for k in self.list()
))

def get(self, id):
"""Returns a single record by ID.
Arguments:
id: a resource ID
"""
resource = self.resource
response = resource.request('get', id=id, params=self._get_params())
return self._load(response)

def filter(self, **kwargs):
return self._copy(filters=kwargs)

def exclude(self, **kwargs):
filters = dict(('-' + k, v) for k, v in kwargs.items())
return self._copy(filters=filters)

def including(self, *args):
return self._copy(includes=args)

def excluding(self, *args):
return self._copy(excludes=args)

def extra(self, **kwargs):
return self._copy(extras=kwargs)

def sort(self, *args):
return self._copy(orders=args)

def order_by(self, *args):
return self.sort(*args)

def _get_params(self):
filters = self.filters
includes = self.includes
excludes = self.excludes
orders = self.orders
extras = self.extras

params = {}
for key, value in filters.items():
filter_key = 'filter{%s}' % key.replace('__', '.')
params[filter_key] = value

if includes:
params['include[]'] = includes

if excludes:
params['exclude[]'] = excludes

if orders:
params['sort[]'] = orders

for key, value in extras.items():
params[key] = value
return params

def _reset(self):
# current page of data
self._data = None
# iteration index on current page
self._index = None
# page number
self._page = None
# total number of pages
self._pages = None

def _copy(self, **kwargs):
data = self.__dict__
new_data = {
k: copy(v)
for k, v in data.items()
if not k.startswith('_')
}
for key, value in kwargs.items():
new_value = data.get(key)
if isinstance(new_value, dict):
if value != new_value:
new_value = copy(new_value)
new_value.update(value)
elif (
isinstance(new_value, (list, tuple)) and
not isinstance(new_value, string_types)
):
if value != new_value:
new_value = list(set(new_value + list(value)))
new_data[key] = new_value
return DRESTQuery(**new_data)

def _get_page(self, params):
if self._page is None:
self._page = 1
else:
self._page += 1

resource = self.resource
params['page'] = self._page
data = resource.request('get', params=params)
meta = data.get('meta', {})
pages = meta.get('total_pages', 1)

self._data = self._load(data)
self._pages = pages
self._index = 0

def _load(self, data):
return self.resource.load(unpack(data))

def __iter__(self):
# TODO: implement __getitem__ for random access
params = self._get_params()
self._get_page(params)
while True:
if self._index == len(self._data):
# end of page
if self._page == self._pages:
# end of results
self._reset()
raise StopIteration()
self._get_page(params)

yield self._data[self._index]
self._index += 1
Loading

0 comments on commit eb0a253

Please sign in to comment.