Welcome, guest | Sign In | My Account | Store | Cart

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.

Python, 191 lines
  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.

1 comment

Diez Roggisch 16 years ago  # | flag

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.

Created by anders pearson on Thu, 10 Nov 2005 (PSF)
Python recipes (4591)
anders pearson's recipes (1)

Required Modules

Other Information and Tasks