Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
branch: master
Fetching contributors…

Cannot retrieve contributors at this time

file 172 lines (131 sloc) 5.876 kb
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172
import posixpath
import urlparse

import requests

from slumber import exceptions
from slumber.serialize import Serializer

__all__ = ["Resource", "API"]


def url_join(base, *args):
    """
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 "/"
    path = posixpath.join(path, *[str(x) for x in args])
    return urlparse.urlunsplit([scheme, netloc, path, query, fragment])


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
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 = {}
        for key, value in self._store.iteritems():
            kwargs[key] = value

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

        return Resource(**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.
"""

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

    def __call__(self, id=None, format=None, url_override=None):
        """
Returns a new instance of self modified by one or more of the available
parameters. These allows us to do things like override format for a
specific request, and enables the api.resource(ID).get() syntax to get
a specific resource by it's ID.
"""

        # Short Circuit out if the call is empty
        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

        if id is not None:
            kwargs["base_url"] = url_join(self._store["base_url"], id)

        if format is not None:
            kwargs["format"] = format

        if url_override is not None:
            # @@@ This is hacky and we should probably figure out a better way
            # of handling the case when a POST/PUT doesn't return an object
            # 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)

    def get_serializer(self):
        return Serializer(default_format=self._store["format"])

    def _request(self, method, data=None, params=None):
        s = self.get_serializer()
        url = self._store["base_url"]

        if self._store["append_slash"] and not url.endswith("/"):
            url = url + "/"

        resp = self._store["session"].request(method, url, data=data, params=params, headers={"content-type": s.get_content_type()})

        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)

        return resp

    def get(self, **kwargs):
        s = self.get_serializer()

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

    def post(self, data, **kwargs):
        s = self.get_serializer()

        resp = self._request("POST", data=s.dumps(data), params=kwargs)
        if 200 <= resp.status_code <= 299:
            if resp.status_code == 201:
                # @@@ Hacky, see description in __call__
                resource_obj = self(url_override=resp.headers["location"])
                return resource_obj.get(params=kwargs)
            else:
                return resp.content
        else:
            # @@@ Need to be Some sort of Error Here or Something
            return

    def put(self, data, **kwargs):
        s = self.get_serializer()

        resp = self._request("PUT", data=s.dumps(data), params=kwargs)
        if 200 <= resp.status_code <= 299:
            if resp.status_code == 204:
                return True
            else:
                return True # @@@ Should this really be True?
        else:
            return False

    def delete(self, **kwargs):
        resp = self._request("DELETE", params=kwargs)
        if 200 <= resp.status_code <= 299:
            if resp.status_code == 204:
                return True
            else:
                return True # @@@ Should this really be True?
        else:
            return False


class API(ResourceAttributesMixin, object):

    def __init__(self, base_url=None, auth=None, format=None, append_slash=True):
        self._store = {
            "base_url": base_url,
            "format": format if format is not None else "json",
            "append_slash": append_slash,
            "session": requests.session(auth=auth),
        }

        # Do some Checks for Required Values
        if self._store.get("base_url") is None:
            raise exceptions.ImproperlyConfigured("base_url is required")
Something went wrong with that request. Please try again.