I recently resurrected an ancient project of mine called JBox. It’s an HTML/Javascript front-end for an MP3 player.
Way back in 2001 I had a headless Linux machine that I had setup as a router and firewall for my home network. Its proximity to the cable modem put it next to the TV and stereo. I soon got the idea to dump all my MP3s onto and connect it to the stereo. To play them, I would SSH onto the machine and queue them up with mpg123. That worked great for me, but no so great for my two roommates.
Teaching them SSH proved futile. Therefore, they could not skip songs or control the music in any way. We would just leave it running all the time and turn the stereo on and off. When it was playing, you got what you got. When it died, and it did so often, there was no music until I could remote it and re-queue it.
I threw about a couple of client-server models before settling on a web-based HTML interface. I looked at Icecast, building my own desktop app and protocol and streaming the sound over the network. None of it was appealing at the time. HTML was ideal since nothing had to be installed locally.
Fast-forward to today and I had this mad idea of putting it up on Github. Sure there are better ways of doing it now, but I still think it’s neat and it proves that I can (or could) program in Python. Now I have found myself getting carried away improving it.
I sought to improve it in two major ways:
- Convert to HTML5 and modern Javascript
- Make the backend more self-contained and easier to deploy.
In practice that meant:
- Rewriting the front-end as a single page application in AngularJS
- Integrating a web server into the back-end and eliminating the need for Apache, suexec and cgi-bin.
Lately I have been working with the Go programming language and one of the things that I absolutely love is the low level and elegant way that you can write web applications with it. For example, here is a minimal server from their documentation:
package main
import (
"fmt"
"net/http"
)
func handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hi there, I love %s!", r.URL.Path[1:])
}
func main() {
http.HandleFunc("/", handler)
http.ListenAndServe(":8080", nil)
}
Registering handlers and writing code is such a breath of fresh air from building .NET apps buried in layers of abstraction. I wanted to get this same elegance in Python. After searching and researching Python’s copious amount of web frameworks, I came across the beautiful CherryPy project. The example from their homepage was all I needed to know.
import cherrypy class HelloWorld(object): def index(self): return "Hello World!" index.exposed = True cherrypy.quickstart(HelloWorld())
Because I was writing the front-end in AngularJS, all I really need from the back-end is a series of REST based web resources. Therefore, the first thing I needed was to incorporate a static webserver. Typically I would let nginx play this role and just have it reverse proxy to my application. However, in this case I wanted a truly self contained application. Remember, this is a “desktop” application with a HTML interface, not a website.
It actually took a bit of searching to find how to do this in the simplest way possible. In fact, I ended up sending a pull request to improve the documentation, to make it easier for the next gal/guy.
import os import cherrypy class Root(object): pass if __name__ == '__main__': CONF = { '/': { 'tools.staticdir.root': os.path.abspath(os.getcwd()), 'tools.staticdir.on': True, 'tools.staticdir.dir': 'site', 'tools.staticdir.index': 'index.html' } } cherrypy.quickstart(Root(), '/', CONF)
It is basically all in the CONF
. CherryPy expects an object passed to quickstart
, so we just create an empty one. For the config, we simply turn staticdir
on, point it to the folder to serve (in this case ‘site’) and set the index file to index.html
.
To enable my REST endpoints, I had to add a few more lines. First
SETUP = {'/': {'request.dispatch': cherrypy.dispatch.MethodDispatcher()}}
The MethodDispatcher
tells CherryPy to map the request type to the corresponding method. For example, the http verb GET
will map to the GET(self)
method. Note that the key, '/'
, is relative off the mount point, not absolute from the root of the server.
Then I just mount a bunch of objects into the web server hierarchy and
cherrypy.tree.mount(songs.Songs(), '/api/songs', SETUP)
cherrypy.tree.mount(volume.Volume(), '/api/volume', SETUP)
cherrypy.tree.mount(nowplaying.NowPlaying(), '/api/nowplaying', SETUP)
...
Here I am mounting an instance of the Songs
class to the absolute path /api/songs
off the root of the website. It is configured to use the MethodDispatcher
. And so on with the Volume
and NowPlaying
classes.
Here is an example of one of the REST handlers:
import cherrypy from jbox.core import volume class Volume(object): def __init__(self, config): self.exposed = True self.vol = volume.Volume(config) @cherrypy.tools.json_out() def GET(self): return {'level': self.vol.level()} @cherrypy.tools.json_in() def PUT(self): json = cherrypy.request.json self.vol.set_level(json['level'])
I just love the simplicity of it.
There is, however, one darkside here that I am not terribly happy about. It may turn out that I am missing something, but as far as their documentation is concerned, this is how you send and receive JSON data. I find the implementation a little problematic. Without JSON, the methods would look like this:
def GET(self): return self.vol.level() # or maybe still # return {'level': self.vol.level()} def PUT(self, level): self.vol.set_level(level)
Here the PUT
parameters are pulled out of the request and passed into the method. As soon as we want JSON we have to remove the parameter, mark up the method as sending/receiving JSON and then manually pull the JSON out of the request. But what if we want XML?
What I see lacking from the framework/api is Content Negotiation. That is, if the client wants JSON, it should ask for it. And get it. If it wants XML, that should be supplied too. Hard-coding my api to JSON is fine since I am the only client, but in general resources should be separate from representations. /api/volume
is the resource, JSON is the representation. Ideally, it would be best to allow multiple request and response types with the same resource implementation.
Overall I am very pleased with CherryPy and am looking forward to completing my project with it. The final step is throwing in a little web socket IO with ws4py, which I may blog about down the road.
No comments:
Post a Comment