Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add token-base auth + response hook #85

Open
wants to merge 26 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
31a35e6
Update Git ignore list
davidfischer-ch Mar 19, 2014
623231d
Add access_token helper
davidfischer-ch Mar 19, 2014
4b7969a
Add response hook
davidfischer-ch Mar 19, 2014
1cd9e14
Merge branch 'response_hook' into dev
davidfischer-ch Mar 19, 2014
4d2c05d
Improve token-based authentication
davidfischer-ch Mar 21, 2014
a4e9d16
Remove unused variables
davidfischer-ch Mar 21, 2014
2bf04fb
Fix pep-8 E128
davidfischer-ch Mar 21, 2014
8442990
Fix tests
davidfischer-ch Apr 9, 2014
c95eda4
Now compatible with Python 3
davidfischer-ch Apr 9, 2014
a86b787
Convert bytes strings to unicode strings
davidfischer-ch Apr 17, 2014
5bde003
Enable Travis CI build
davidfischer-ch May 6, 2014
3babd7c
Cleanup setup script
davidfischer-ch May 6, 2014
c6efaf6
Add Python 3.4 to build matrix
davidfischer-ch May 6, 2014
392a4f0
Remove Python 2.6 from compatibility list
davidfischer-ch May 6, 2014
a33951a
Merge imports
davidfischer-ch May 6, 2014
e256397
Replace for loop by dict.update
davidfischer-ch May 6, 2014
fb0d384
Improve exception class to embed response + generate msg
davidfischer-ch May 6, 2014
441b89a
Rename resp -> response + cleanup code
davidfischer-ch May 6, 2014
fa2afe8
Add pypy to travis build matrix
davidfischer-ch May 6, 2014
3dce54a
Remove outdated comments
davidfischer-ch Jan 15, 2015
c9f50e5
Code cleanup
davidfischer-ch Jan 15, 2015
6e8182b
Add "self" to resource hook args
davidfischer-ch Jan 15, 2015
48a494b
Make subclassing easier
davidfischer-ch Jan 15, 2015
9c0d110
Protect against hidden bugs
davidfischer-ch Jan 15, 2015
e6c5715
Update docstrings
davidfischer-ch Jan 15, 2015
4f5febf
Make subclassing easier (bis)
davidfischer-ch Jan 15, 2015
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
build/*
dist/*
docs/_build/*
.tox
Expand Down
11 changes: 7 additions & 4 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
language: python
python:
- 2.6
- 2.7
- 3.1
- 3.2
install: pip install -r requirements.txt && pip install -r requirements-test.txt
script: python setup.py test
- 3.3
- 3.4
- pypy
install:
- travis_retry pip install -r requirements.txt -r requirements-test.txt
script:
- python setup.py test
11 changes: 11 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,17 @@ Changelog
development version
-------------------

0.7.1
-----

* Now compatible with Python 3.
* Convert bytes strings to unicode strings.

0.6.2
-----

* Add token to authenticate requests with a token.
* Add response_hook to allow tweaking the returned response.

0.6.0
-----
Expand Down
7 changes: 6 additions & 1 deletion README.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
Slumber
=======

.. image:: https://secure.travis-ci.org/davidfischer-ch/slumber.png
:target: http://travis-ci.org/davidfischer-ch/slumber

Afraid of red status ? Please click on the link, sometimes this is not my fault ;-)

Slumber is a Python library that provides a convenient yet powerful
object-oriented interface to ReSTful APIs. It acts as a wrapper around the
excellent requests_ library and abstracts away the handling of URLs, serialization,
Expand Down Expand Up @@ -34,7 +39,7 @@ Requirements

Slumber requires the following modules.

* Python 2.6+
* Python 2.7+
* requests
* pyyaml (If you are using the optional YAML serialization)

Expand Down
33 changes: 19 additions & 14 deletions setup.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,30 @@
import os
# -*- coding: utf-8 -*-

import os, sys
from setuptools import setup

install_requires = ["requests"]
tests_require = ["mock"]
kwargs = {}
if sys.version_info[0] >= 3:
print('Converting code to Python 3 helped by 2to3')
kwargs['use_2to3'] = True

base_dir = os.path.dirname(os.path.abspath(__file__))

setup(
name = "slumber",
version = "0.6.1.dev",
description = "A library that makes consuming a REST API easier and more convenient",
name="slumber",
version="0.7.1.dev",
description="A library that makes consuming a REST API easier and more convenient",
long_description="\n\n".join([
open(os.path.join(base_dir, "README.rst"), "r").read(),
open(os.path.join(base_dir, "CHANGELOG.rst"), "r").read()
]),
url = "http://slumber.in/",
author = "Donald Stufft",
author_email = "[email protected]",
packages = ["slumber"],
zip_safe = False,
install_requires = install_requires,
tests_require = tests_require,
test_suite = "tests.get_tests",
url="http://slumber.in/",
author="Donald Stufft",
author_email="[email protected]",
packages=["slumber"],
zip_safe=False,
install_requires=["requests"],
tests_require=["mock"],
test_suite="tests.get_tests",
**kwargs
)
136 changes: 65 additions & 71 deletions slumber/__init__.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
import posixpath
import urlparse
# -*- coding: utf-8 -*-

import requests
from __future__ import absolute_import, unicode_literals

from slumber import exceptions
from slumber.serialize import Serializer
import posixpath, urlparse, requests

from . import exceptions
from .serialize import Serializer

__all__ = ["Resource", "API"]


def url_join(base, *args):
"""
Helper function to join an arbitrary number of url segments together.
Helper function to join an arbitrary number of URL segments together.
"""
scheme, netloc, path, query, fragment = urlparse.urlsplit(base)
path = path if len(path) else "/"
Expand All @@ -21,39 +22,31 @@ def url_join(base, *args):

class ResourceAttributesMixin(object):
"""
A Mixin that makes it so that accessing an undefined attribute on a class
results in returning a Resource Instance. This Instance can then be used
A mixin that makes it so that accessing an undefined attribute on a class
results in returning a Resource instance. This instance can then be used
to make calls to the a Resource.

It assumes that a Meta class exists at self._meta with all the required
attributes.
"""

def __getattr__(self, item):
if item.startswith("_"):
raise AttributeError(item)
kwargs = self._store.copy()
kwargs["base_url"] = url_join(self._store["base_url"], item)
return self._get_resource(**kwargs)

kwargs = {}
for key, value in self._store.iteritems():
kwargs[key] = value

kwargs.update({"base_url": url_join(self._store["base_url"], item)})

return Resource(**kwargs)
def _get_resource(self, **kwargs):
return self.__class__(**kwargs)


class Resource(ResourceAttributesMixin, object):
"""
Resource provides the main functionality behind slumber. It handles the
attribute -> url, kwarg -> query param, and other related behind the scenes
python to HTTP transformations. It's goal is to represent a single resource
which may or may not have children.

It assumes that a Meta class exists at self._meta with all the required
attributes.
attribute -> URL, kwargs -> query parameters, and other related behind the
scenes python to HTTP transformations. It's goal is to represent a single
resource which may or may not have children.
"""

def __init__(self, *args, **kwargs):
def __init__(self, **kwargs):
self._store = kwargs

def __call__(self, id=None, format=None, url_override=None):
Expand All @@ -68,9 +61,7 @@ def __call__(self, id=None, format=None, url_override=None):
if id is None and format is None and url_override is None:
return self

kwargs = {}
for key, value in self._store.iteritems():
kwargs[key] = value
kwargs = self._store.copy()

if id is not None:
kwargs["base_url"] = url_join(self._store["base_url"], id)
Expand All @@ -84,9 +75,7 @@ def __call__(self, id=None, format=None, url_override=None):
# but a Location to an object that we need to GET.
kwargs["base_url"] = url_override

kwargs["session"] = self._store["session"]

return self.__class__(**kwargs)
return self._get_resource(**kwargs)

def _request(self, method, data=None, files=None, params=None):
s = self._store["serializer"]
Expand All @@ -97,81 +86,78 @@ def _request(self, method, data=None, files=None, params=None):

headers = {"accept": s.get_content_type()}

if self._store.get("token", None):
headers["Authorization"] = "{token_type} {access_token}".format(**self._store["token"])

if not files:
headers["content-type"] = s.get_content_type()
if data is not None:
data = s.dumps(data)

resp = self._store["session"].request(method, url, data=data, params=params, files=files, headers=headers)

if 400 <= resp.status_code <= 499:
raise exceptions.HttpClientError("Client Error %s: %s" % (resp.status_code, url), response=resp, content=resp.content)
elif 500 <= resp.status_code <= 599:
raise exceptions.HttpServerError("Server Error %s: %s" % (resp.status_code, url), response=resp, content=resp.content)
response = self._store["session"].request(method, url, data=data, params=params, files=files, headers=headers)

self._ = resp
if 400 <= response.status_code <= 499:
raise exceptions.HttpClientError(response)
elif 500 <= response.status_code <= 599:
raise exceptions.HttpServerError(response)

return resp
self._ = response
return response

def _handle_redirect(self, resp, **kwargs):
def _handle_redirect(self, response, **kwargs):
# @@@ Hacky, see description in __call__
resource_obj = self(url_override=resp.headers["location"])
resource_obj = self(url_override=response.headers["location"])
return resource_obj.get(params=kwargs)

def _try_to_serialize_response(self, resp):
def _try_to_serialize_response(self, response):
s = self._store["serializer"]

if resp.headers.get("content-type", None):
content_type = resp.headers.get("content-type").split(";")[0].strip()

if response.headers.get("content-type", None):
content_type = response.headers.get("content-type").split(";")[0].strip()
try:
stype = s.get_serializer(content_type=content_type)
response_content = stype.loads(response.content)
except exceptions.SerializerNotAvailable:
return resp.content

return stype.loads(resp.content)
response_content = response.content
else:
return resp.content
response_content = response.content
hook = self._store.get("response_hook")
return hook(self, response_content) if hook else response_content

def get(self, **kwargs):
resp = self._request("GET", params=kwargs)
if 200 <= resp.status_code <= 299:
return self._try_to_serialize_response(resp)
response = self._request("GET", params=kwargs)
if 200 <= response.status_code <= 299:
return self._try_to_serialize_response(response)
else:
return # @@@ We should probably do some sort of error here? (Is this even possible?)

def post(self, data=None, files=None, **kwargs):
s = self._store["serializer"]

resp = self._request("POST", data=data, files=files, params=kwargs)
if 200 <= resp.status_code <= 299:
return self._try_to_serialize_response(resp)
response = self._request("POST", data=data, files=files, params=kwargs)
if 200 <= response.status_code <= 299:
return self._try_to_serialize_response(response)
else:
# @@@ Need to be Some sort of Error Here or Something
return

def patch(self, data=None, files=None, **kwargs):
s = self._store["serializer"]

resp = self._request("PATCH", data=data, files=files, params=kwargs)
if 200 <= resp.status_code <= 299:
return self._try_to_serialize_response(resp)
response = self._request("PATCH", data=data, files=files, params=kwargs)
if 200 <= response.status_code <= 299:
return self._try_to_serialize_response(response)
else:
# @@@ Need to be Some sort of Error Here or Something
return

def put(self, data=None, files=None, **kwargs):
resp = self._request("PUT", data=data, files=files, params=kwargs)

if 200 <= resp.status_code <= 299:
return self._try_to_serialize_response(resp)
response = self._request("PUT", data=data, files=files, params=kwargs)
if 200 <= response.status_code <= 299:
return self._try_to_serialize_response(response)
else:
return False

def delete(self, **kwargs):
resp = self._request("DELETE", params=kwargs)
if 200 <= resp.status_code <= 299:
if resp.status_code == 204:
response = self._request("DELETE", params=kwargs)
if 200 <= response.status_code <= 299:
if response.status_code == 204:
return True
else:
return True # @@@ Should this really be True?
Expand All @@ -181,7 +167,10 @@ def delete(self, **kwargs):

class API(ResourceAttributesMixin, object):

def __init__(self, base_url=None, auth=None, format=None, append_slash=True, session=None, serializer=None):
resource_class = Resource

def __init__(self, base_url=None, auth=None, format=None, append_slash=True, session=None, serializer=None,
token=None, response_hook=None):
if serializer is None:
serializer = Serializer(default=format)

Expand All @@ -191,12 +180,17 @@ def __init__(self, base_url=None, auth=None, format=None, append_slash=True, ses

self._store = {
"base_url": base_url,
"format": format if format is not None else "json",
"format": "json" if format is None else format,
"append_slash": append_slash,
"session": session,
"serializer": serializer,
"token": token,
"response_hook": response_hook
}

# Do some Checks for Required Values
if self._store.get("base_url") is None:
raise exceptions.ImproperlyConfigured("base_url is required")

def _get_resource(self, **kwargs):
return self.resource_class(**kwargs)
14 changes: 10 additions & 4 deletions slumber/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
# -*- coding: utf-8 -*-

from __future__ import unicode_literals


class SlumberBaseException(Exception):
"""
All Slumber exceptions inherit from this exception.
Expand All @@ -9,10 +14,11 @@ class SlumberHttpBaseException(SlumberBaseException):
All Slumber HTTP Exceptions inherit from this exception.
"""

def __init__(self, *args, **kwargs):
for key, value in kwargs.iteritems():
setattr(self, key, value)
super(SlumberHttpBaseException, self).__init__(*args)
def __init__(self, response):
self.response = response
value = "%s Error %s %s: %s, text: %s" % ('Client' if response.status_code <= 499 else 'Server',
response.status_code, response.reason, response.url, response.text)
super(SlumberHttpBaseException, self).__init__(value)


class HttpClientError(SlumberHttpBaseException):
Expand Down
Loading