This post originated from an RSS feed registered with Python Buzz
by Andrew Dalke.
Original Post: CherryPy
Feed Title: Andrew Dalke's writings
Feed URL: http://www.dalkescientific.com/writings/diary/diary-rss.xml
Feed Description: Writings from the software side of bioinformatics and chemical informatics, with a heaping of Python thrown in for good measure.
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
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.