223 lines
8.7 KiB
ReStructuredText
223 lines
8.7 KiB
ReStructuredText
.. _rest:
|
|
|
|
Writing RESTful Web Services with Generic Controllers
|
|
=====================================================
|
|
|
|
Pecan simplifies RESTful web services by providing a way to overload URLs based
|
|
on the request method. For most API's, the use of `generic controller`
|
|
definitions give you everything you need to build out robust RESTful
|
|
interfaces (and is the *recommended* approach to writing RESTful web services
|
|
in pecan):
|
|
|
|
::
|
|
|
|
from pecan import abort, expose
|
|
|
|
# Note: this is *not* thread-safe. In real life, use a persistent data store.
|
|
BOOKS = {
|
|
'0': 'The Last of the Mohicans',
|
|
'1': 'Catch-22'
|
|
}
|
|
|
|
|
|
class BookController(object):
|
|
|
|
def __init__(self, id_):
|
|
self.id_ = id_
|
|
assert self.book
|
|
|
|
@property
|
|
def book(self):
|
|
if self.id_ in BOOKS:
|
|
return dict(id=self.id_, name=BOOKS[self.id_])
|
|
abort(404)
|
|
|
|
# HTTP GET /<id>/
|
|
@expose(generic=True, template='json')
|
|
def index(self):
|
|
return self.book
|
|
|
|
# HTTP PUT /<id>/
|
|
@index.when(method='PUT', template='json')
|
|
def index_PUT(self, **kw):
|
|
BOOKS[self.id_] = kw['name']
|
|
return self.book
|
|
|
|
# HTTP DELETE /<id>/
|
|
@index.when(method='DELETE', template='json')
|
|
def index_DELETE(self):
|
|
del BOOKS[self.id_]
|
|
return dict()
|
|
|
|
|
|
class RootController(object):
|
|
|
|
@expose()
|
|
def _lookup(self, id_, *remainder):
|
|
return BookController(id_), remainder
|
|
|
|
# HTTP GET /
|
|
@expose(generic=True, template='json')
|
|
def index(self):
|
|
return [dict(id=k, name=v) for k, v in BOOKS.items()]
|
|
|
|
# HTTP POST /
|
|
@index.when(method='POST', template='json')
|
|
def index_POST(self, **kw):
|
|
id_ = len(BOOKS)
|
|
BOOKS[id_] = kw['name']
|
|
return dict(id=id_, name=kw['name'])
|
|
|
|
|
|
Writing RESTful Web Services with RestController
|
|
================================================
|
|
|
|
.. _TurboGears2: http://turbogears.org
|
|
|
|
For compatability with the TurboGears2_ library, Pecan also provides
|
|
a class-based solution to RESTful routing, :class:`~pecan.rest.RestController`:
|
|
|
|
::
|
|
|
|
from pecan import expose
|
|
from pecan.rest import RestController
|
|
|
|
from mymodel import Book
|
|
|
|
class BooksController(RestController):
|
|
|
|
@expose()
|
|
def get(self, id):
|
|
book = Book.get(id)
|
|
if not book:
|
|
abort(404)
|
|
return book.title
|
|
|
|
URL Mapping
|
|
-----------
|
|
|
|
By default, :class:`~pecan.rest.RestController` routes as follows:
|
|
|
|
+-----------------+--------------------------------------------------------------+--------------------------------------------+
|
|
| Method | Description | Example Method(s) / URL(s) |
|
|
+=================+==============================================================+============================================+
|
|
| get_one | Display one record. | GET /books/1 |
|
|
+-----------------+--------------------------------------------------------------+--------------------------------------------+
|
|
| get_all | Display all records in a resource. | GET /books/ |
|
|
+-----------------+--------------------------------------------------------------+--------------------------------------------+
|
|
| get | A combo of get_one and get_all. | GET /books/ |
|
|
| | +--------------------------------------------+
|
|
| | | GET /books/1 |
|
|
+-----------------+--------------------------------------------------------------+--------------------------------------------+
|
|
| new | Display a page to create a new resource. | GET /books/new |
|
|
+-----------------+--------------------------------------------------------------+--------------------------------------------+
|
|
| edit | Display a page to edit an existing resource. | GET /books/1/edit |
|
|
+-----------------+--------------------------------------------------------------+--------------------------------------------+
|
|
| post | Create a new record. | POST /books/ |
|
|
+-----------------+--------------------------------------------------------------+--------------------------------------------+
|
|
| put | Update an existing record. | POST /books/1?_method=put |
|
|
| | +--------------------------------------------+
|
|
| | | PUT /books/1 |
|
|
+-----------------+--------------------------------------------------------------+--------------------------------------------+
|
|
| get_delete | Display a delete confirmation page. | GET /books/1/delete |
|
|
+-----------------+--------------------------------------------------------------+--------------------------------------------+
|
|
| delete | Delete an existing record. | POST /books/1?_method=delete |
|
|
| | +--------------------------------------------+
|
|
| | | DELETE /books/1 |
|
|
+-----------------+--------------------------------------------------------------+--------------------------------------------+
|
|
|
|
Pecan's :class:`~pecan.rest.RestController` uses the ``?_method=`` query string
|
|
to work around the lack of support for the PUT and DELETE verbs when
|
|
submitting forms in most current browsers.
|
|
|
|
In addition to handling REST, the :class:`~pecan.rest.RestController` also
|
|
supports the :meth:`index`, :meth:`_default`, and :meth:`_lookup`
|
|
routing overrides.
|
|
|
|
.. warning::
|
|
|
|
If you need to override :meth:`_route`, make sure to call
|
|
:func:`RestController._route` at the end of your custom method so
|
|
that the REST routing described above still occurs.
|
|
|
|
Nesting ``RestController``
|
|
---------------------------
|
|
|
|
:class:`~pecan.rest.RestController` instances can be nested so that child
|
|
resources receive the parameters necessary to look up parent resources.
|
|
|
|
For example::
|
|
|
|
from pecan import expose
|
|
from pecan.rest import RestController
|
|
|
|
from mymodel import Author, Book
|
|
|
|
class BooksController(RestController):
|
|
|
|
@expose()
|
|
def get(self, author_id, id):
|
|
author = Author.get(author_id)
|
|
if not author_id:
|
|
abort(404)
|
|
book = author.get_book(id)
|
|
if not book:
|
|
abort(404)
|
|
return book.title
|
|
|
|
class AuthorsController(RestController):
|
|
|
|
books = BooksController()
|
|
|
|
@expose()
|
|
def get(self, id):
|
|
author = Author.get(id)
|
|
if not author:
|
|
abort(404)
|
|
return author.name
|
|
|
|
class RootController(object):
|
|
|
|
authors = AuthorsController()
|
|
|
|
Accessing ``/authors/1/books/2`` invokes :func:`BooksController.get` with
|
|
``author_id`` set to ``1`` and ``id`` set to ``2``.
|
|
|
|
To determine which arguments are associated with the parent resource, Pecan
|
|
looks at the :func:`get_one` then :func:`get` method signatures, in that order,
|
|
in the parent controller. If the parent resource takes a variable number of
|
|
arguments, Pecan will pass it everything up to the child resource controller
|
|
name (e.g., ``books`` in the above example).
|
|
|
|
Defining Custom Actions
|
|
-----------------------
|
|
|
|
In addition to the default methods defined above, you can add additional
|
|
behaviors to a :class:`~pecan.rest.RestController` by defining a special
|
|
:attr:`_custom_actions`
|
|
dictionary.
|
|
|
|
For example::
|
|
|
|
from pecan import expose
|
|
from pecan.rest import RestController
|
|
|
|
from mymodel import Book
|
|
|
|
class BooksController(RestController):
|
|
|
|
_custom_actions = {
|
|
'checkout': ['POST']
|
|
}
|
|
|
|
@expose()
|
|
def checkout(self, id):
|
|
book = Book.get(id)
|
|
if not book:
|
|
abort(404)
|
|
book.checkout()
|
|
|
|
:attr:`_custom_actions` maps method names to the list of valid HTTP
|
|
verbs for those custom actions. In this case :func:`checkout` supports
|
|
``POST``.
|