Extension of the Restful Resource recipe from the cherrypy wiki (http://www.cherrypy.org/wiki/RestfulResource) to support nested resources and to dispatch based on HTTP verbs.
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 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 | """
REST Resource
cherrypy controller mixin to make it easy to build REST applications.
handles nested resources and method-based dispatching.
here's a rough sample of what a controller would look like using this:
cherrypy.root = MainController()
cherrypy.root.user = UserController()
class PostController(RESTResource):
def index(self,post):
return post.as_html()
index.expose_resource = True
def delete(self,post):
post.destroySelf()
return "ok"
delete.expose_resource = True
def update(self,post,title="",body=""):
post.title = title
post.body = body
return "ok"
update.expose_resource = True
def add(self, post, title="", body="")
post.title = title
post.body = body
return "ok"
update.expose_resource = True
def REST_instantiate(self, slug):
try:
return Post.select(Post.q.slug == slug, Post.q.userID = self.parent.id)[0]
except:
return None
def REST_create(self, slug):
return Post(slug=slug,user=self.parent)
class UserController(RESTResource):
REST_children = {'posts' : PostController()}
def index(self,user):
return user.as_html()
index.expose_resource = True
def delete(self,user):
user.destroySelf()
return "ok"
delete.expose_resource = True
def update(self,user,fullname="",email=""):
user.fullname = fullname
user.email = email
return "ok"
update.expose_resource = True
def add(self, user, fullname="", email=""):
user.fullname = fullname
user.email = email
return "ok"
add.expose_resource = True
def extra_action(self,user):
# do something else
extra_action.expose_resource = True
def REST_instantiate(self, username):
try:
return User.byUsername(username)
except:
return None
def REST_create(self, username):
return User(username=username)
then, the site would have urls like:
/user/bob
/user/bob/posts/my-first-post
/user/bob/posts/my-second-post
which represent REST resources. calling 'GET /usr/bob' would call the index() method on UserController
for the user bob. 'PUT /usr/joe' would create a new user with username 'joe'. 'DELETE /usr/joe'
would delete that user. 'GET /usr/bob/posts/my-first-post' would call index() on the Post Controller
with the post with the slug 'my-first-post' that is owned by bob.
"""
import cherrypy
class RESTResource:
# default method mapping. ie, if a GET request is made for
# the resource's url, it will try to call an index() method (if it exists);
# if a PUT request is made, it will try to call an add() method.
# if you prefer other method names, just override these values in your
# controller with REST_map
REST_defaults = {'DELETE' : 'delete',
'GET' : 'index',
'POST' : 'update',
'PUT' : 'add'}
REST_map = {}
# if the resource has children resources, list them here. format is
# a dictionary of name -> resource mappings. ie,
#
# REST_children = {'posts' : PostController()}
REST_children = {}
def REST_dispatch(self, resource, **params):
# if this gets called, we assume that default has already
# traversed down the tree to the right location and this is
# being called for a raw resource
method = cherrypy.request.method
if self.REST_map.has_key(method):
m = getattr(self,self.REST_map[method])
if m and getattr(m, "expose_resource"):
return m(resource,**params)
else:
if self.REST_defaults.has_key(method):
m = getattr(self,self.REST_defaults[method])
if m and getattr(m, "expose_resource"):
return m(resource,**params)
raise cherrypy.NotFound
@cherrypy.expose
def default(self, *vpath, **params):
if not vpath:
return self.list(**params)
# Make a copy of vpath in a list
vpath = list(vpath)
atom = vpath.pop(0)
# Coerce the ID to the correct db type
resource = self.REST_instantiate(atom)
if resource is None:
if cherrypy.request.method == "PUT":
# PUT is special since it can be used to create
# a resource
resource = self.REST_create(atom)
else:
raise cherrypy.NotFound
# There may be further virtual path components.
# Try to map them to methods in children or this class.
if vpath:
a = vpath.pop(0)
if self.REST_children.has_key(a):
c = self.REST_children[a]
c.parent = resource
return c.default(*vpath, **params)
method = getattr(self, a, None)
if method and getattr(method, "expose_resource"):
return method(resource, *vpath, **params)
else:
# path component was specified but doesn't
# map to anything exposed and callable
raise cherrypy.NotFound
# No further known vpath components. Call a default handler
# based on the method
return self.REST_dispatch(resource,**params)
def REST_instantiate(self,id):
""" instantiate a REST resource based on the id
this method MUST be overridden in your class. it will be passed
the id (from the url fragment) and should return a model object
corresponding to the resource.
if the object doesn't exist, it should return None rather than throwing
an error. if this method returns None and it is a PUT request,
REST_create() will be called so you can actually create the resource.
"""
raise cherrypy.NotFound
def REST_create(self,id):
""" create a REST resource with the specified id
this method should be overridden in your class.
this method will be called when a PUT request is made for a resource
that doesn't already exist. you should create the resource in this method
and return it.
"""
raise cherrypy.NotFound
|
makes extensive use of cherrypy's default() method. it works well enough for me, but i'm very open to improving it.
Tags: web
thread issues. The recipe is very nice, however be aware that the chain of parent-resources is not thread-safe as it is currently implemented.
I try to work around this and possibly post here the solution.