# -*- coding: utf-8 -
#
# This file is part of dj-webmachine released under the MIT license.
# See the NOTICE for more information.
"""
Minimal API building
++++++++++++++++++++
Combinating the power of Django and the :ref:`resources <resources>` it's relatively easy to buid an api. The process is also eased using the WM object. dj-webmachine offer a way to create automatically resources by using the ``route`` decorator.
Using this decorator, our helloworld example can be rewritten like that:
.. code-block:: python
from webmachine.ap import wm
import json
@wm.route(r"^$")
def hello(req, resp):
return "<html><p>hello world!</p></html>"
@wm.route(r"^$", provided=[("application/json", json.dumps)])
def hello_json(req, resp):
return {"ok": True, "message": "hellow world"}
and the urls.py:
.. code-block:: python
from django.conf.urls.defaults import *
import webmachine
webmachine.autodiscover()
urlpatterns = patterns('',
(r'^', include(webmachine.wm.urls))
)
The autodiscover will detect all resources modules and add then to the
url dispatching. The route decorator works a little like the one in
bottle_ or for that matter flask_ (though bottle was the first).
This decorator works differently though. It creates full
:class:`webmachine.resource.Resource` instancse registered in the wm
object. So we are abble to provide all the features available in a
resource:
- settings which content is accepted, provided
- assiciate serializers to the content types
- throttling
- authorization
"""
import webmachine.exc
from webmachine.resource.base import Resource, RESOURCE_METHODS
try:
from cStringIO import StringIO
except ImportError:
from StringIO import StringIO
def validate_ctype(value):
if isinstance(value, basestring):
return [value]
elif not isinstance(value, list) and value is not None:
raise TypeError("'%s' should be a list or a string, got %s" %
(value, type(value)))
return value
def serializer_cb(serializer, method):
if hasattr(serializer, method):
return getattr(serializer, method)
return serializer
def build_ctypes(ctypes,method):
for ctype in ctypes:
if isinstance(ctype, tuple):
cb = serializer_cb(ctype[1], method)
yield ctype[0], cb
else:
yield ctype, lambda v: v
class RouteResource(Resource):
def __init__(self, pattern, fun, **kwargs):
self.set_pattern(pattern, **kwargs)
methods = kwargs.get('methods') or ['GET', 'HEAD']
if isinstance(methods, basestring):
methods = [methods]
elif not isinstance(methods, (list, tuple,)):
raise TypeError("methods should be list or a tuple, '%s' provided" % type(methods))
# associate methods to the function
self.methods = {}
for m in methods:
self.methods[m.upper()] = fun
# build content provided list
provided = validate_ctype(kwargs.get('provided') or \
['text/html'])
self.provided = list(build_ctypes(provided, "serialize"))
# build content accepted list
accepted = validate_ctype(kwargs.get('accepted')) or []
self.accepted = list(build_ctypes(accepted, "unserialize"))
self.kwargs = kwargs
# override method if needed
for k, v in self.kwargs.items():
if k in RESOURCE_METHODS:
setattr(self, k, self.wrap(v))
def set_pattern(self, pattern, **kwargs):
self.url = (pattern, kwargs.get('name'))
def update(self, fun, **kwargs):
methods = kwargs.get('methods') or ['GET', 'HEAD']
if isinstance(methods, basestring):
methods = [methods]
elif not isinstance(methods, (list, tuple,)):
raise TypeError("methods should be list or a tuple, '%s' provided" % type(methods))
# associate methods to the function
for m in methods:
self.methods[m.upper()] = fun
# we probably should merge here
provided = validate_ctype(kwargs.get('provided'))
if provided is not None:
provided = list(build_ctypes(provided, "serialize"))
self.provided.extend(provided)
accepted = validate_ctype(kwargs.get('accepted'))
if accepted is not None:
accepted = list(build_ctypes(accepted, "unserialize"))
self.accepted.extend(accepted)
def wrap(self, f, cb=None):
def _wrapped(req, resp):
if cb is not None:
return cb(f(req, resp))
return f(req, resp)
return _wrapped
def first_match(self, media, expect):
for key, value in media:
if key == expect:
return value
return None
def accept_body(self, req, resp):
ctype = req.content_type or "application/octet-stream"
mtype = ctype.split(";", 1)[0]
funload = self.first_match(self.accepted, mtype)
if funload is None:
raise webmachine.exc.HTTPUnsupportedMediaType()
req._raw_post_data = funload(req.raw_post_data)
if isinstance(req._raw_post_data, basestring):
req._stream = StringIO(req._raw_post_data)
fun = self.methods[req.method]
body = fun(req, resp)
if isinstance(body, tuple):
resp._container, resp.location = body
else:
resp._container = body
return self.return_body(req, resp)
def return_body(self, req, resp):
fundump = self.first_match(self.provided, resp.content_type)
if fundump is None:
raise webmachine.exc.HTTPInternalServerError()
resp._container = fundump(resp._container)
if not isinstance(resp._container, basestring):
resp._is_tring = False
else:
resp._container = [resp._container]
resp._is_string = True
return resp._container
#### resources methods
def allowed_methods(self, req, resp):
return self.methods.keys()
def format_suffix_accepted(self, req, resp):
if 'formats' in self.kwargs:
return self.kwargs['formats']
return []
def content_types_accepted(self, req, resp):
if not self.accepted:
return None
return [(c, self.accept_body) for c, f in self.accepted]
def content_types_provided(self, req, resp):
fun = self.methods[req.method]
if not self.provided:
return [("text/html", self.wrap(fun))]
return [(c, self.wrap(fun, f)) for c, f in self.provided]
def delete_resource(self, req, resp):
fun = self.methods['DELETE']
ret = fun(req, resp)
if isinstance(ret, basestring) or hasattr(ret, '__iter__'):
resp._container = ret
self.return_body(req, resp)
return True
return False
def post_is_create(self, req, resp):
if req.method == 'POST':
return True
return False
def created_location(self, req, resp):
return resp.location
def process_post(self, req, resp):
return self.accept_body(req, resp)
def multiple_choices(self, req, resp):
return False
def get_urls(self):
from django.conf.urls.defaults import patterns, url
url_kwargs = self.kwargs.get('url_kwargs') or {}
if len(self.url) >2:
url1 =url(self.url[0], self, name=self.url[1], kwargs=url_kwargs)
else:
url1 =url(self.url[0], self, kwargs=url_kwargs)
return patterns('', url1)
[docs]class WM(object):
def __init__(self, name="webmachine", version=None):
self.name = name
self.version = version
self.resources = {}
self.routes = []
[docs] def route(self, pattern, **kwargs):
""" A decorator that is used to register a new resource using
this function to return response.
**Parameters**
:attr pattern: regular expression, like the one you give in
your urls.py
:attr methods: methods accepted on this function
:attr provides: list of provided contents tpes and associated
serializers::
[(MediaType, Handler)]
:attr accepted: list of content you accept in POST/PUT with
associated deserializers::
[(MediaType, Handler)]
A serializer can be a simple callable taking a value or a class:
.. code-block:: python
class Myserializer(object):
def unserialize(self, value):
# ... do something to value
return value
def serialize(self, value):
# ... do something to value
return value
:attr formats: return a list of format with their associated
contenttype::
[(Suffix, MediaType)]
:attr kwargs: any named parameter coresponding to a
:ref:`resource method <resource>`. Each value is a callable
taking a request and a response as arguments:
.. code-block:: python
def f(req, resp):
pass
"""
def _decorated(func):
self.add_route(pattern, func, **kwargs)
return func
return _decorated
def _wrap_urls(self, f, pattern):
from django.conf.urls.defaults import patterns, url, include
def _wrapped(*args):
return patterns('',
url(pattern, include(f(*args)))
)
return _wrapped
[docs] def add_resource(self, klass, pattern=None):
"""add one :ref:`Resource class<resource>` to the routing.
:attr klass: class inheriting from :class:webmachine.Resource
:attr pattern: regexp.
"""
res = klass()
if not pattern:
if hasattr(res._meta, "resource_path"):
kname = res._meta.resource_path
else:
kname = klass.__name__.lower()
pattern = r'^%s/' % res._meta.app_label
if kname:
pattern = r'%s/' % kname
res.get_urls = self._wrap_urls(res.get_urls, pattern)
self.resources[pattern] = res
[docs] def add_resources(self, *klasses):
""" allows you to add multiple Resource classes to the WM instance. You
can also pass a pattern by using a tupple instead of simply
provided the Resource class. Ex::
(MyResource, r"^some/path$")
"""
for klass in klasses:
if isinstance(klass, tuple):
klass, pattern = klass
else:
pattern = None
self.add_resource(klass, pattern=pattern)
[docs] def add_route(self, pattern, func, **kwargs):
if pattern in self.resources:
res = self.resources[pattern]
res.update(func, **kwargs)
else:
res = RouteResource(pattern, func, **kwargs)
self.resources[pattern] = res
self.routes.append((pattern, func, kwargs))
# associate the resource to the function
setattr(func, "_wmresource", res)
[docs] def get_urls(self):
from django.conf.urls.defaults import patterns
urlpatterns = patterns('')
for pattern, resource in self.resources.items():
urlpatterns += resource.get_urls()
return urlpatterns
urls = property(get_urls)
wm = WM()