summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authormoigagoo <devnull@localhost>2013-03-21 00:12:50 +0400
committermoigagoo <devnull@localhost>2013-03-21 00:12:50 +0400
commit5bcd1bcb6c6e881fe945a75bcb7618433bbb7b3d (patch)
tree805e7c51d936ad37518105eee6fc3748dc15a302
parente4a3017cf0aece7aea52c500062efec9d4e2c0d3 (diff)
downloadcherrypy-5bcd1bcb6c6e881fe945a75bcb7618433bbb7b3d.tar.gz
[~] RESTful API tutorial improved; example file added.
-rw-r--r--sphinx/source/tutorial/REST.rst233
-rw-r--r--sphinx/source/tutorial/files/songs.py75
2 files changed, 261 insertions, 47 deletions
diff --git a/sphinx/source/tutorial/REST.rst b/sphinx/source/tutorial/REST.rst
index 2fb4549a..0e7f3a72 100644
--- a/sphinx/source/tutorial/REST.rst
+++ b/sphinx/source/tutorial/REST.rst
@@ -13,33 +13,52 @@ Overview
In this tutorial, we will create a RESTful backend for a song collection management web app.
+A song is a *resource* with certain data (called *state*). Let's assume, every song has **title** and **artist**, and is identified by a unique **ID**.
+
+There are also *methods* to view and change the state of a resource. The basic set of methods is called `CRUD <http://en.wikipedia.org/wiki/Create,_read,_update_and_delete>`_—Create, Read, Update, and Delete.
+
Let's assume that the frontend part is developed by someone else, and can interact with our backend part only with API requests. Our jobs is only to handle those requests, perform actions, and return the proper response.
Therefore, we will not take care about templating or page rendering.
+We will also not use a database in this tutorial for the sake of concentrating solely on the RESTful API concept.
+
.. note::
REST principles assume that a response status must always be meaningful. HTTP 1.1 specification already has all necessary error codes, and a developer should properly map erroneous backend events with according HTTP error codes.
Fortunately, CherryPy has done it for us. For instance, if our backend app receives a request with wrong parameters, CherryPy will raise a ``400 Bad Request`` response automatically.
-Resource class, MethodDispatcher, and GET handler
-=================================================
+Download the :download:`complete tutorial file <files/songs.py>`.
+
+Getting Started
+===============
Create a file called ``songs.py`` with the following content::
import cherrypy
+ songs = {
+ '1': {
+ 'title': 'Lumberjack Song',
+ 'artist': 'Canadian Guard Choir'
+ },
+
+ '2': {
+ 'title': 'Always Look On the Bright Side of Life',
+ 'artist': 'Eric Idle'
+ },
+
+ '3': {
+ 'title': 'Spam Spam Spam',
+ 'artist': 'Monty Python'
+ }
+ }
+
class Songs:
exposed = True
- def GET(self, id=None):
- if id:
- return('Show info about the song with the ID %s' % id)
- else:
- return('Show info about all the available songs')
-
if __name__ == '__main__':
cherrypy.tree.mount(
@@ -52,33 +71,42 @@ Create a file called ``songs.py`` with the following content::
cherrypy.engine.start()
cherrypy.engine.block()
-Let's go through this code line by line:
+Let's go through this code line by line.
Import CherryPy::
import cherrypy
-Create a class to represent the *songs* resource::
+Define the song "database", which is a simple Python dictionary::
- class Songs:
+ songs = {
+ '1': {
+ 'title': 'Lumberjack Song',
+ 'artist': 'Canadian Guard Choir'
+ },
-Expose all the class methods at once::
+ '2': {
+ 'title': 'Always Look On the Bright Side of Life',
+ 'artist': 'Eric Idle'
+ },
- exposed = True
+ '3': {
+ 'title': 'Spam Spam Spam',
+ 'artist': 'Monty Python'
+ }
+ }
-Create the GET method to handle HTTP GET requests::
+Note that we are using *strings* as dict keys, not *integers*. This is done only to avoid extra type convertings when we will parse the request parameters (which are always strings.) Normally, the ID handling is performed by a database automatically, but since we do not use any, we have to deal with it manually.
- def GET(self, id=None):
- if id:
- return('Show info about the song with the ID %s' % id)
- else:
- return('Show info about all the available songs')
+Create a class to represent the *songs* resource::
+
+ class Songs:
-This method will show the first message when the URL ``/api/songs/<id>`` is requested, the second one—when the URL ``/api/songs`` is requested.
+Expose all the (future) class methods at once::
-.. note:: The method name matters! Class methods must correspond to the actual HTTP methods. See the explanation below.
+ exposed = True
-Standard Python direct check on whether the file is used directly or as module::
+Standard Python check on whether the file is used directly or as module::
if __name__ == '__main__':
@@ -87,14 +115,15 @@ Create an instance of the class (called a CherryPy application) and mount it to
cherrypy.tree.mount(
Songs(), '/api/songs',
-
This means that this app will handle requests coming to the URLs starting with ``/api/songs``.
Now, here goes the interesting part.
CherryPy has a very helpful tool for creating RESTful APIs—the **MethodDispatcher**.
-Briefly speaking, it is a special sort of dispatcher which automatically connects the HTTP requests with proper handlers based on the request method. All you have to do is just name the handlers accordingly, so the dispatcher can find it.
+Learn it and love it.
+
+Briefly speaking, it is a special sort of dispatcher which automatically connects the HTTP requests to the according handlers based on the request method. All you have to do is just name the handlers to correspond to the HTTP method names.
Long story short, just call the HTTP GET handler ``GET``, and the HTTP POST handle ``POST``.
@@ -107,55 +136,165 @@ Activate this dispatcher for our app::
Note that the ``/`` path in this config is relative to the application mount point (``/api/songs``), and will apply only to it.
-The last 2 lines do just the same as ``.quickstart()``, only written a bit more explicitly—run the server::
+The last 2 lines do just the same as the ``quickstart`` method, only written a bit more explicitly—run the server::
cherrypy.engine.start()
cherrypy.engine.block()
-Now, if you run this file on you local machine with Python, you will have a working GET request handler at ``127.0.0.1:8080/api/songs``!
+GET
+===
+
+Represents the Read method in CRUD.
+
+Add a new method to the file ``songs.py``, called ``GET``::
+
+ def GET(self, id=None):
+
+ if id == None:
+ return('Here are all the songs we have: %s' % songs)
+ elif id in songs:
+ song = songs[id]
+ return('Song with the ID %s is called %s, and the artist is %s' % (id, song['title'], song['artist']))
+ else:
+ return('No song with the ID %s :-(' % id)
+
+This method will return the whole song dictionary if the ID is not specified (``/api/songs``), a particular song data if the ID is specified and exists (``/api/songs/1`` ), and the message about a not existing song otherwise (``/api/songs/42``.)
+
+Try it out in your browser by going to ``127.0.0.1:8080/api/songs/``, ``127.0.0.1:8080/api/songs/1``, or ``127.0.0.1:8080/api/songs/42``.
+
+POST
+====
+
+Represents the Create method in CRUD.
+
+Add a new method to the ``Songs`` class, called ``POST``::
+
+ def POST(self, title, artist):
+
+ id = str(max([int(_) for _ in songs.keys()]) + 1)
+
+ songs[id] = {
+ 'title': title,
+ 'artist': artist
+ }
+
+ return ('Create a new song with the ID: %s' % id)
-Try it out in your browser by going to ``127.0.0.1:8080/api/songs/`` or ``127.0.0.1:8080/api/songs/42``.
+This method defines the next unique ID and adds an item to the ``songs`` dictionary.
-It does not do much yet, but it already properly handles GET requests and responses with the correct HTTP status codes.
+Note that we do not validate the input arguments. CherryPy does it for us. If any parameter is missing or and extra one is provided, the 400 Bad Request error will be raised automatically.
-POST, PUT, and DELETE
-=====================
+.. hint:: Sending POST, PUT, and DELETE requests
-In order to have a persistent system, we must have 4 basic actions implemented by our app—so called `CRUD <http://en.wikipedia.org/wiki/Create,_read,_update_and_delete>`_.
+ Unlike GET request, POST, PUT, and DELETE requests cannot be sent via the browser address field.
-We already have GET to read. According to REST, now we need to add:
+ You will need to use some special software to do it.
- * POST to create
- * PUT to update
- * DELETE to delete
+ The recommendation here is to use `cURL <http://en.wikipedia.org/wiki/CURL>`_, which is available by default in most GNU/Linux distributions and is available for Windows and Mac.
-Let's do so!
+ You can send GET requests with cURL too, but using a browser is easier.
-In the file ``songs.py``, add the following methods into the ``Songs`` class (you probably can guess the method names already)::
+Send a POST HTTP request to ``127.0.0.1:8080/api/songs/`` with cURL:
- def POST(self, **kwargs):
- return ('Create a new song with the following parameters: %s' % kwargs)
+.. code-block:: bash
- def PUT(self, id, **kwargs):
- return ('Update the data of the song with the ID %s with the following parameters: %s' % (id, kwargs))
+ curl -d title='Frozen' -d artist='Madonna' -X POST '127.0.0.1:8080/api/songs/'
+
+You will see the response:
+
+ Create a new song with the ID: 4%
+
+Now, if you go to ``127.0.0.1:8080/api/songs/4`` in your browser you will se the following message:
+
+ Song with the ID 4 is called Frozen, and the artist is Madonna
+
+So it actually works!
+
+PUT
+===
+
+Represents the Update method in CRUD.
+
+Add a new method to the ``Songs`` class, called ``PUT``::
+
+ def PUT(self, id, title=None, artist=None):
+ if id in songs:
+ song = songs['id']
+
+ song['title'] = title or song['title']
+ song['artist'] = artist or song['artist']
+
+ return('Song with the ID %s is now called %s, and the artist is now %s' % (id, song['title'], song['artist']))
+ else:
+ return('No song with the ID %s :-(' % id)
+
+This method checks whether the requested song exists and updates the fields that are provided. If some field is not specified, the corresponding value will not be updated.
+
+Try sending some PUT HTTP requests to ``127.0.0.1:8080/api/songs/3`` via cURL, and check the result by requesting ``127.0.0.1:8080/api/songs/4`` in your browser:
+
+* .. code-block:: bash
+
+ curl -d title='Yesterday' -X PUT '127.0.0.1:8080/api/songs/3'
+
+ The response:
+
+ Song with the ID 3 is now called Yesterday, and the artist is now Monty Python%
+
+ What you'll see in the browser:
+
+ Song with the ID 3 is called Yesterday, and the artist is Monty Python
+
+* .. code-block:: bash
+
+ curl -d artist='Beatles' -X PUT '127.0.0.1:8080/api/songs/3'
+
+ The response:
+
+ Song with the ID 3 is now called Yesterday, and the artist is now Beatles%
+
+ What you'll see in the browser:
+
+ Song with the ID 3 is called Yesterday, and the artist is Beatles
+
+DELETE
+======
+
+Represents the DELETE method in CRUD.
+
+Add a new method to the ``Songs`` class, called ``DELETE``::
def DELETE(self, id):
- return('Delete the song with the ID %s' % id)
+ if id in songs:
+ songs.pop(id)
+
+ return('Song with the ID %s has been deleted.' % id)
+ else:
+ return('No song with the ID %s :-(' % id)
+
+This method, like the previous ones, check if the given ID point to an existing song and pops it out of the ``songs`` dictionary.
+
+Send a DELETE HTTP request to ``127.0.0.1:8080/api/songs/2`` via cURL:
+
+.. code-block:: bash
+
+ curl -X DELETE '127.0.0.1:8080/api/songs/2'
+
+The response:
-Note that unlike the ``GET`` method, ``PUT`` and ``DELETE`` have the ``id`` argument mandatory (no ``id=None``). This is a good idea since we want to update and delete only a particular song, but not all of them.
+ Song with the ID 2 has been deleted.%
-Also note that ``POST`` does not have the ``id`` argument at all. It is not needed as there is logically no ID to relate to.
+And the browser output:
-Now, if you use `cURL <http://en.wikipedia.org/wiki/CURL>`_ or any similar tool to send a POST, PUT, or DELETE request to the ``/api/songs/`` or ``/api/songs/<id>``, you will see that it is properly processed—valid requests are responded with status 200 and the according message, invalid requests are rejected.
+ No song with the ID 2 :-(
-Multiple resources
+Multiple Resources
==================
You can have any number of resources represented this way. Each resource is a CherryPy application, i.e. a class.
For another resource, say, *users*, just create a class ``Users`` the same way you created ``Songs``, and mount it to ``/api/users`` with the same config.
-Conclusion and further steps
+Conclusion and Further Steps
============================
This is pretty much it about the logic of REST API in CherryPy.
diff --git a/sphinx/source/tutorial/files/songs.py b/sphinx/source/tutorial/files/songs.py
new file mode 100644
index 00000000..4dd4ab8e
--- /dev/null
+++ b/sphinx/source/tutorial/files/songs.py
@@ -0,0 +1,75 @@
+import cherrypy
+
+songs = {
+ '1': {
+ 'title': 'Lumberjack Song',
+ 'artist': 'Canadian Guard Choir'
+ },
+
+ '2': {
+ 'title': 'Always Look On the Bright Side of Life',
+ 'artist': 'Eric Idle'
+ },
+
+ '3': {
+ 'title': 'Spam Spam Spam',
+ 'artist': 'Monty Python'
+ }
+}
+
+class Songs:
+
+ exposed = True
+
+ def GET(self, id=None):
+
+ if id == None:
+ return('Here are all the songs we have: %s' % songs)
+ elif id in songs:
+ song = songs[id]
+
+ return('Song with the ID %s is called %s, and the artist is %s' % (id, song['title'], song['artist']))
+ else:
+ return('No song with the ID %s :-(' % id)
+
+ def POST(self, title, artist):
+
+ id = str(max([int(_) for _ in songs.keys()]) + 1)
+
+ songs[id] = {
+ 'title': title,
+ 'artist': artist
+ }
+
+ return ('Create a new song with the ID: %s' % id)
+
+ def PUT(self, id, title=None, artist=None):
+ if id in songs:
+ song = songs[id]
+
+ song['title'] = title or song['title']
+ song['artist'] = artist or song['artist']
+
+ return('Song with the ID %s is now called %s, and the artist is now %s' % (id, song['title'], song['artist']))
+ else:
+ return('No song with the ID %s :-(' % id)
+
+ def DELETE(self, id):
+ if id in songs:
+ songs.pop(id)
+
+ return('Song with the ID %s has been deleted.' % id)
+ else:
+ return('No song with the ID %s :-(' % id)
+
+if __name__ == '__main__':
+
+ cherrypy.tree.mount(
+ Songs(), '/api/songs',
+ {'/':
+ {'request.dispatch': cherrypy.dispatch.MethodDispatcher()}
+ }
+ )
+
+ cherrypy.engine.start()
+ cherrypy.engine.block()