Commits

Anonymous committed 800f59d

[~] RESTful API tutorial improved; example file added.

  • Participants
  • Parent commits 2c4b14d

Comments (1)

  1. space one

    lol… using the real HTTP methods does not make REST... it is only the first step... e.g. you are returning the string 'id not found', in a REST archtiecture this would be a HTTP 404 NOT FOUND

Files changed (2)

File sphinx/source/tutorial/REST.rst

 
 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(
         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
 
+Define the song "database", which is a simple Python dictionary::
+
+    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'
+        }
+    }
+
+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.
+
 Create a class to represent the *songs* resource::
 
     class Songs:
 
-Expose all the class methods at once::
+Expose all the (future) class methods at once::
 
     exposed = True
 
-Create the GET method to handle HTTP GET requests::
-
-    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')
-
-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.
-
-.. note:: The method name matters! Class methods must correspond to the actual HTTP methods. See the explanation below.
-
-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__':
 
     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``.
 
 
 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
+===
 
-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``.
+Represents the Read method in CRUD.
 
-It does not do much yet, but it already properly handles GET requests and responses with the correct HTTP status codes.
+Add a new method to the file ``songs.py``, called ``GET``::
 
-POST, PUT, and DELETE
-=====================
+    def GET(self, id=None):
 
-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>`_.
+        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)
 
-We already have GET to read. According to REST, now we need to add:
+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``.)
 
- * POST to create
- * PUT to update
- * DELETE to delete
+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``.
 
-Let's do so!
+POST
+====
 
-In the file ``songs.py``, add the following methods into the ``Songs`` class (you probably can guess the method names already)::
+Represents the Create method in CRUD.
 
-    def POST(self, **kwargs):
-        return ('Create a new song with the following parameters: %s' % kwargs)
+Add a new method to the ``Songs`` class, called ``POST``::
 
-    def PUT(self, id, **kwargs):
-        return ('Update the data of the song with the ID %s with the following parameters: %s' % (id, kwargs))
+    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)
+
+This method defines the next unique ID and adds an item to the ``songs`` dictionary.
+
+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.
+
+.. hint:: Sending POST, PUT, and DELETE requests
+
+    Unlike GET request, POST, PUT, and DELETE requests cannot be sent via the browser address field.
+
+    You will need to use some special software to do it.
+
+    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.
+
+    You can send GET requests with cURL too, but using a browser is easier.
+
+Send a POST HTTP request to ``127.0.0.1:8080/api/songs/`` with cURL:
+
+.. code-block:: bash
+
+    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)
 
-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.
+            return('Song with the ID %s has been deleted.' % id)
+        else:
+            return('No song with the ID %s :-(' % id)
 
-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.
+This method, like the previous ones, check if the given ID point to an existing song and pops it out of the ``songs`` dictionary.
 
-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.
+Send a DELETE HTTP request to ``127.0.0.1:8080/api/songs/2`` via cURL:
 
-Multiple resources
+.. code-block:: bash
+
+    curl -X DELETE '127.0.0.1:8080/api/songs/2'
+
+The response:
+
+    Song with the ID 2 has been deleted.%
+
+And the browser output:
+
+    No song with the ID 2 :-(
+
+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.

File sphinx/source/tutorial/files/songs.py

+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()