Dalke Scientific Software: More science. Less time. Products
[ previous | newer ]     /home/writings/diary/archive/2005/04/26/cherrypy

CherryPy

Back in the age of stone knives people developed web pages with CGI scripts. That for me was about a month ago. After the PyWebOff last month (a compare-and-contrast exercise to evaluate the strengths and weaknesses of some of the major Python web application frameworks) I decided to look at CherryPy. That seemed to be the easiest of the packages she discusssed. And lo, it was easy.

The CherryPy and PyWebOff sites have examples of general web development so I'll start off with one a bit more chemistry specific. This will be a server that prints a canonical SMILES string of a familiar string. No user input yet. I've provided details in the comments.

from cherrypy import cpg
from openeye.oechem import *

class ChemServer:

    # This function is like "index.html"; it shows the content
    # when a client asks for the root of the tree
    
    def index(self):

        # This should look familiar by now :)
        smiles = "c1ccccc1O"
        mol = OEMol()
        OEParseSmiles(mol, smiles)
        cansmi = OECreateCanSmiString(mol)

        # By default CherryPy expects HTML; I'm returning plain text
        cpg.response.headerMap["Content-Type"] = "text/plain"
        return "The OpenEye canonical form of %r is %r" % (smiles, cansmi)

    # Need to tell the web server to make this function available.
    index.exposed = True

# Tell the server how to find handlers for different URLs
cpg.root = ChemServer()

# By default the server will run on http://localhost:8080/
cpg.server.start()
Run the program on your local machine and go to http://localhost:8080/. (Or use another machine and the appropriate URL for it.) The page should show
The OpenEye canonical form of 'c1ccccc1O' is 'c1ccc(cc1)O'

I'll change the server to take an optional SMILES string through a CGI parameter.

from cherrypy import cpg
from openeye.oechem import *

# Needed because the SMILES string may contain characters
# that look like HTML (like '>' in a reaction)
from cgi import escape

class ChemServer:
    # The input SMILES is optional
    def index(self, smiles = None):
        html = "<HTML>"
        if smiles is not None:
            # SMILES parameter specified
            mol = OEMol()
            if not OEParseSmiles(mol, smiles):
                msg = "Cannot parse SMILES %r" % (escape(smiles),)
            else:
                cansmi = OECreateCanSmiString(mol)
                msg = ("The OpenEye canonical form of %r is %r" %
                       (escape(smiles), escape(cansmi)))
            html = html + msg + "<br>"

        # A simple form that goes back to the same URL on submit
        html = html + (
        '''<form method="GET">SMILES: <input type="text" name="smiles" />'''
        '''<input type="submit" /></form></HTML''')

        return html

    # Need to tell the web server to make this function available.
    index.exposed = True

# Tell the server how to find handlers for different URLs
cpg.root = ChemServer()

# By default the server will run on http://localhost:8080/
cpg.server.start()

Making the output by building up a single possibly large string is cumbersome. CherryPy also supports returning values through an iterator, which is easy to make with the yield keyword.

from cherrypy import cpg
from cgi import escape
from openeye.oechem import *

class ChemServer:
    def index(self, smiles = None):
        yield "<HTML>"
        if smiles is not None:
            mol = OEMol()
            if not OEParseSmiles(mol, smiles):
                yield "Cannot parse SMILES %r" % (escape(smiles),)
            else:
                cansmi = OECreateCanSmiString(mol)
                yield ("The OpenEye canonical form of %r is %r" %
                       (escape(smiles), escape(cansmi)))
            yield "<br>"
        yield '''<form method="GET">'''
        yield '''SMILES: <input type="text" name="smiles" />'''
        yield '''<input type="submit" /></form></HTML>'''

    # Need to tell the web server to make this function available.
    index.exposed = True

# Tell the server how to find handlers for different URLs
cpg.root = ChemServer()

# By default the server will run on http://localhost:8080/
cpg.server.start()
Of course you could also use a template. In this case though the template was more complex than the code.

Text is so boring. How about an image server? To do that I need a way to make images. Ogham includes a "mol2gif" program which is perfect for the task. First I'll write a helper library which I'll call "chemutils.py". It's very similar to the first essay I wrote on wrapping command-line programs. The program can only make an image for a single molecule so I can't even attempt to make it a coprocess.

import os, subprocess

from openeye import oechem

class ChemError(Exception):
    pass

def smi2gif(smiles, width = 200, height = 200, title = None):
    args = [os.environ["OE_DIR"] + "/bin/mol2gif",
            "-width", str(width), "-height", str(height)]
    if title is not None:
        args.append("-title")
        smiles = smiles + " " + title
    args.extend(["-gif", "-", "-"])
    
    p = subprocess.Popen(args,
                         stdin = subprocess.PIPE,
                         stdout = subprocess.PIPE,
                         stderr = subprocess.PIPE)
    p.stdin.write(smiles + "\n")
    p.stdin.close()
    gif = p.stdout.read()
    errmsg = p.stderr.read()
    errcode = p.wait()
    if errcode:
        raise ChemError("Could not convert %r into an image:\n%s" %
                        (smiles, errmsg))
    return gif

# Test that the sizes are correct using PIL (the Python
# Image Library) to read the created string.
def test_smi2gif():
    import cStringIO
    import Image

    gif = smi2gif("C")
    img = Image.open(cStringIO.StringIO(gif))
    if img.size != (200, 200):
        raise AssertionError(img.size)
    gif = smi2gif("CC", width = 100, height=88)
    img = Image.open(cStringIO.StringIO(gif))
    if img.size != (100, 88):
        raise AssertionError(img.size)
    try:
        # check that I can catch the failure
        gif = smi2gif("1")
    except ChemError:
        pass
    else:
        raise AssertionError("Should have failed")

def test():
    test_smi2gif()

if __name__ == "__main__":
    test()
    print "All tests passed."

Here's the new method for the ChemServer to handle the "/smi2gif" request. It takes the SMILES string as "s", the optional image sizes as "w" and "h" and the optional title as "t".

import chemutils
  ...
    def smi2gif(self, s, w=None, h=None, t=None):
        # default width and height is 200 pixels
        if w is None:
            w = 200
        else:
            w = int(w)
        if h is None:
            h = 200
        else:
            h = int(h)

        mol = OEMol()
        if not OEParseSmiles(mol, s):
            # The SMILES may have been partially parsed.  Clean it
            # up a bit to show some idea about the incomplete SMILES
            OEAssignAromaticFlags(mol)

        s = OECreateCanSmiString(mol)

        gif = chemutils.smi2gif(s, w, h, t)

        cpg.response.headerMap["Content-Type"] = "image/gif"
        # During development I had problems because the image
        # was cached.  This helped prevent caching.
        #cpg.response.headerMap["Expires"] = "Tue, 08 Oct 1996 08:00:00 GMT"

        return gif

    smi2gif.exposed = True
Quit and restart the server. Try the URL http://localhost:8080/smi2gif?s=c1ccccc1O&t;=phenol. This should depict phenol, like the image to the right.

If the SMILES is incomplete or contains an error then the OEMol will have a partial structure it in. While chemically incorrect it's helpful for users to see an approximation to the SMILES as its being typed. For instance it helps people learn SMILES. The Ogham smi2gif program doesn't allow chemically incorrect SMILES inputs so the above code checks if the SMILES is incorrect. If so it cleans up the structure a bit so the depiction has a better chance of working.

At this point, and with a bit of Javascript, I can make an interactive SMILES viewer with a text entry box and an image. Every time the text changes the image src URL will be changed to show the new depiction. Note: what you see next to this paragraph is only an image. I don't have a live server set up for doing depictions.

To detect when the text changes I'll use the onKeyUp event to call the new function change_img(). This gets the current text and uses it to construct a new URL for the image's "src" field. This in turn tells the browser to load a new image. Very simple, and very cool. There is one tricky part; the SMILES may contain characters, like the "+" in "[CH4+]", which have a special meaning in a URL. I'll escape the entered SMILES string using the built-in encodeURIComponent() Javascript function.

from cherrypy import cpg
from openeye.oechem import *
import chemutils

class ChemServer:
    def index(self):
        return """\
<html><head><title>Interactive smi2gif</title></head>
<body>
<script>
function change_img() {
  var smiles = document.getElementById("smi").value;
  document.getElementById("dep").src = "/smi2gif?s=" + encodeURIComponent(smiles);
}
</script>
<form onsubmit="return false">
Enter SMILES:<br />
  <input type="text" id="smi" onKeyUp="change_img()" autocomplete="off">
</form><br>
Image will update as you type<br />
<img id="dep" width="200" height="200">
</body></html>"""

    # Need to tell the web server to make this function available.
    index.exposed = True

    def smi2gif(self, s, w=None, h=None, t=None):
        if w is None:
            w = 200
        else:
            w = int(w)
        if h is None:
            h = 200
        else:
            h = int(h)

        mol = OEMol()
        if not OEParseSmiles(mol, s):
            OEAssignAromaticFlags(mol)
        s = OECreateCanSmiString(mol)

        gif = chemutils.smi2gif(s, w, h, t)

        cpg.response.headerMap["Content-Type"] = "image/gif"
        return gif

    smi2gif.exposed = True


# Tell the server how to find handlers for different URLs
cpg.root = ChemServer()

# By default the server will run on http://localhost:8080/
cpg.server.start()

Quit and restart the server then visit http://localhost:8080/. I found it very fun to play around with it. Try adding the ability to set the title or change the size interactively.

Because the CherryPy server is persistant (it's always runnning) one performance option you can do is implement a cache for the images calculations. It's easy to set up a basic one. For more complete solutions you might consider memcached or set up a reverse proxy.


Andrew Dalke is an independent consultant focusing on software development for computational chemistry and biology. Need contract programming, help, or training? Contact me



Copyright © 2001-2020 Andrew Dalke Scientific AB