[three]Bean
Python + WSGI + protovis @ BarcampRoc Fall 2010
Oct 24, 2010 | categories: python, toscawidgets, turbogears View CommentsI gave a presentation at BarcampRoc yesterday on some of the new widgets I've been working on. I started with a two-minute Powerpoint and then jumped right into a live hack session. The audience helped chime in to tweak pieces of code, make the visualizations more interesting, and catch my clumsy syntax errors. Few people in the room had python experience and none had experience with WSGI or protovis but it felt like everyone walked out with a good sense of how to carry out the same work again.
Here's the rundown.
Setting up Turbogears
Before we do anything, you'll need Turbogears 2.1 and virtualenv setup on your system. If you're running an rpm-based distro, the following should cut it:
[rjbpop@grossman ~ []]# sudo yum install python-tg-devtools python-virtualenv
Start with a fresh Turbogears 2.1 quickstarted app. TG provides a wonderland of middleware to make writing robust web-apps easier and so will ask you some questions when quickstarting. We won't need any repoze authentication stuff and we'll use genshi templates.
[rjbpop@grossman ~ []]# mkdir devel [rjbpop@grossman ~ []]# cd devel [rjbpop@grossman devel []]# paster quickstart Enter project name: barcamp.roc.fall.2010 Enter package name [barcamprocfall2010]: melissa_is_a_babe Would you prefer mako templates? (yes/[no]): no Do you need authentication and authorization in this project? ([yes]/no): no
paster
should barf some output while setting up the app. We have some dependencies we need to install and since we don't want to risk messing up our system python environment, we'll use the handy virtualenv
tool.
[rjbpop@grossman devel []]# cd barcamp.roc.fall.2010 [rjbpop@grossman barcamp.roc.fall.2010 []]# virtualenv venv-test New python executable in venv-test/bin/python Installing setuptools............done. [rjbpop@grossman barcamp.roc.fall.2010 []]# source venv-test/bin/activate (venv-test)[rjbpop@grossman barcamp.roc.fall.2010 []]# which paster /usr/bin/paster (venv-test)[rjbpop@grossman barcamp.roc.fall.2010 []]# which pip ~/devel/barcamp.roc.fall.2010/venv-test/bin/pip (venv-test)[rjbpop@grossman barcamp.roc.fall.2010 []]# pip install --upgrade PasteScript tw2.protovis.custom
pip
should barf some output here too. To verify that an instance of paster local to our virtual environment was installed, check:
(venv-test)[rjbpop@grossman barcamp.roc.fall.2010 []]# which paster ~/devel/barcamp.roc.fall.2010/venv-test/bin/paster
Good stuff. Now, we're going to be using toscawidgets2 (package: tw2) not toscawidgets1 which turbogears2.1 has enabled by default. Let's change that.
Add the two highlighted lines to the bottom of melissa_is_a_babe/config/app_cfg.py
so that it looks like:
#Configure the base SQLALchemy Setup base_config.use_sqlalchemy = True base_config.model = melissa_is_a_babe.model base_config.DBSession = melissa_is_a_babe.model.DBSession base_config.use_toscawidgets = False base_config.use_toscawidgets2 = True
The WSGI stack of our Turbogears2 app is now using the toscawidgets2
middleware layer. Great! There's one more thing to do before we can launch and
check out our app: Turbogears2.1 has a single spurious reference to
toscawidgets1. Let's remove it. Edit
melissa_is_a_babe/lib/base.py
and remove the following highlighted
line.
from tg import TGController, tmpl_context from tg.render import render from pylons.i18n import _, ungettext, N_ from tw.api import WidgetBunch import melissa_is_a_babe.model as model __all__ = ['BaseController']
Word. We should be set to spin up the jumpdrives.
(venv-test)[rjbpop@grossman barcamp.roc.fall.2010 []]# paster serve --reload development.ini Starting subprocess with file monitor Starting server in PID 2250. serving on http://127.0.0.1:8080
God willing, if you point your browser at http://localhost:8080/ you should see the default quickstarted Turbogears index page.
Fiddling with Turbogears
Time to shave away stuff we don't need. Open up
melissa_is_a_babe/templates/index.html
(which is a
genshi
template) and scoop out its guts so it looks like:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:py="http://genshi.edgewall.org/" xmlns:xi="http://www.w3.org/2001/XInclude"> <xi:include href="master.html" /> <head> <meta content="text/html; charset=UTF-8" http-equiv="content-type" py:replace="''"/> <title>Welcome to TurboGears 2.0, standing on the shoulders of giants, since 2007</title> </head> <body> </body> </html>
Since we told paster
to launch our app with the
--reload
option, it should automatically notice we changed
something and be ready for us. Return to your browser, reload the page and
behold:
Not very exciting now, is it?
Our first tw2.protovis chart
Open up melissa_is_a_babe/controllers/root.py
. There you'll
find a class called RootController with a number of methods. When you visit a
particular location, say, http://localhost:8080/foobar
, your TG2.1
BaseController will redirect the flow of control to your RootController's
foobar
method (if it has one).
We don't need any of the methods other than index
so you can go
ahead and remove them.
Inside the index
method, we're going to configure an instance of
tw2.protovis.custom.BubbleChart
and return it to the WSGI context.
At the top root.py
add the appropriate import statements to bring
in the random
module and the BubbleChart
class from
the tw2.protovis.custom
module. As for the index
method, we'll need to do three things: 1) initialize some bogus data to
visualize, 2) fill in the parameters for BubbleChart
, and 3) return
our configured widget from the controller method.
As for our bogus data, the p_data
parameter requires a list of
python dictionaries (we'll create 40 of them), each with four keys: a
'name'
that provides the alt-text for the bubbles, a
'value'
that provides the size of each, a 'text'
that
provides the label, and a 'group'
which is the basis on which to
colorize them.
Mangle melissa_is_a_babe/controllers/root.py
to look like
this:
# -*- coding: utf-8 -*- """Main Controller""" from tg import expose, flash, require, url, request, redirect from pylons.i18n import ugettext as _, lazy_ugettext as l_ from melissa_is_a_babe.lib.base import BaseController from melissa_is_a_babe.model import DBSession, metadata from melissa_is_a_babe.controllers.error import ErrorController __all__ = ['RootController'] from tw2.protovis.custom import BubbleChart import random class RootController(BaseController): """ The root controller for the barcamp.roc.fall.2010 application. All the other controllers and WSGI applications should be mounted on this controller. For example:: panel = ControlPanelController() another_app = AnotherWSGIApplication() Keep in mind that WSGI applications shouldn't be mounted directly: They must be wrapped around with :class:`tg.controllers.WSGIAppController`. """ error = ErrorController() @expose('melissa_is_a_babe.templates.index') def index(self): """Handle the front-page.""" data = [ { 'name' : random.random(), 'value' : random.random(), 'text' : random.random(), 'group' : random.random(), } for i in range(40) ] chart = BubbleChart( id='a-chart-for-my-friends', p_width=750, p_height=750, p_data=data ) return dict(page='index', widget=chart)
Now that our bubble chart of random data is available to our templates, we'll
need to make mention of it in
melissa_is_a_babe/templates/index.html
. Just add the following
single line inside the body
tag.
<body> ${widget.display()} </body>
Again, paster
should have noticed we changed something and
restarted itself. Revisit http://localhost:8080
and you should see
something like:
Querying Google's Ajax Search API
Cool? Cool. Well, sort of. Random numbers are unfortunately not as
interesting as we might like them to be so let's do something a little less
entropic. Google has a sweet
ajax search API we can use. Add the following to the top of
melissa_is_a_babe/controllers/root.py
import itertools import urllib import simplejson import itertools import math base_url = "http://ajax.googleapis.com/ajax/services/search/web?v=1.0" def make_entry(combo): phrase = '"%s"' % " ".join(combo) print "Querying for %s." % phrase query = urllib.urlencode({ 'q': phrase }) url = "%s&%s" % (base_url, query) results = urllib.urlopen(url) json = simplejson.loads(results.read()) if 'estimatedResultCount' in json['responseData']['cursor']: count = int(json['responseData']['cursor']['estimatedResultCount']) else: count = len(json['responseData']['results']) value = math.log(count+1.000000001) return { 'name' : "%s : %i " % (phrase, count), 'value' : value, 'text' : phrase[:10], 'group' : len(combo), }
The above function will, given a list of words (combo), perform a google search on that exact phrase and return a python dictionary ready for BubbleChart the value of which is the estimatedResultCount of the search term. We use the urllib module to prepare and make our query and the simplejson module to convert the json (javascript object notation) that google hands us into a python dictionary.
We'll need to make use of the make_entry
function in our
controller.
Keyword arguments passed to our app via the URL's query string arrive as
keyword arguments to controller methods. Let's add one to the
index
method called sentence
.
Furthermore, we'll take that sentence, break it up into its constituent words and, making use of the itertools module, prepare a list of every unique combination of words in that sentence.
With our make_entry
function and such a list of word
combinations, its trivial to map our list to a map of BubbleChart-ready
dict
objects.
Modify melissa_is_a_babe/controllers/root.py
as follows:
@expose('melissa_is_a_babe.templates.index') def index(self, sentence="word to your moms"): """Handle the front-page.""" words = str(sentence).split() combos = [] for i in range(len(words)): combos += list(itertools.combinations(words, i+1)) data = map(make_entry, combos) chart = BubbleChart( id='a-chart-for-my-friends', p_width=750, p_height=750, p_data=data ) return dict(page='index', widget=chart)
Reload the page http://localhost:8080/
and you should get
something like:
Since we made sentence
a keyword argument to our
index
method, we should also be able to visit
http://localhost:8080/index?sentence=the+quick+brown+fox+jumps+over+the+lazy+dog
and see a pretty dope chart.
Speeding things up with multiprocessing
Wow.. that was cool but it took forever to make all those queries.
What exactly is it that's taking so much time? It seems to be all the
handshaking and waiting for a response before making the next request. We can
likely speed this up greatly by making use of python's
multiprocessing
module.Just below your definition of the make_entity
function, add the
following lines:
return { 'name' : "%s : %i " % (phrase, count), 'value' : value, 'text' : phrase[:10], 'group' : len(combo), } from multiprocessing import Pool pool = Pool(processes=150) class RootController(BaseController):
The Pool class has a map method that performs the same operation as the
builtin map, but distributes the workload among all available processes. Inside
the index
method, make use of the thread pool when mapping
make_entity
onto the list of word combinations. Change the
highlighted line as follows:
for i in range(len(words)): combos += list(itertools.combinations(words, i+1)) data = pool.map(make_entry, combos) chart = BubbleChart( id='a-chart-for-my-friends',
Repoint your browser at
http://localhost:8080/index?sentence=the+quick+brown+fox+jumps+over+the+lazy+dog
and it should fly!
Sorting our data gives different results
One last thing: BubbleChart places the bubbles in the order they are given
and tries to pack each subsequent entry as tightly as it can. We can achieve
differently flavored charts by reordering our list of dicts. In
melissa_is_a_babe/controllers/root.py
try adding the following
line:
for i in range(len(words)): combos += list(itertools.combinations(words, i+1)) data = pool.map(make_entry, combos) data.sort(lambda x, y: cmp(y['value'], x['value'])) chart = BubbleChart( id='a-chart-for-my-friends',
Which gives us the following:
Resources
You can find the full source code for this tutorial on my github account
Live demonstrations of all my widgets are available at http://tw2-demos.threebean.org/
Cheers!
Turbo JQPlot
Aug 03, 2009 | categories: python, toscawidgets, turbogears View CommentsSo a while back, Luke Macken wrote the FlotWidget class for ToscaWidgets. It rocked. However, the underlying javascript library (flot) has since been surpassed by the hot, hot jqPlot. After submitting my first patch to a project, ToscaWidgets is back in the gravy!
Python wrapped in python
Aug 03, 2009 | categories: python View CommentsEDIT 04-26-2011: Check out http://learnpython.org!
So I started a little python project called trypy the other day. Its a TurboGears 2 app in development inspired by the tryruby app written by Why the Lucky Stiff. At the core, I need to be able to interpret python commands in an already running python program. Having learned of the pexpect module, here's what I came up with.
import pexpect class PythonInterpreter(object): def __init__(self, forbidden=[], quiet=True): self.forbidden = forbidden self.eof = False self.lastline = None self.py = pexpect.spawn('python') self.eoc = "# __end_of_command__" if quiet: # Chew the first three 'version' lines [self.readline() for i in range(3)] self.cmd_history = [ 'print "Happy python interpreter reporting for duty."'] def readline(self): self.lastline = self.py.readline().rstrip() return self.lastline def prompt(self): if self.eoc in self.lastline: i = self.lastline.index(self.eoc) return self.lastline[:i] return ">>>" def command(self, index): return self.cmd_history[index] def process(self, cmd): lines = [] self.cmd_history.append(cmd) if self.eof: lines.append('** PythonInterpreter: EOF') try: if any([item in cmd for item in self.forbidden]): return [' "%s"' % cmd, ' Disallowed. Sorry.'] self.py.sendline(cmd + "\n" + self.eoc) garbage, line = self.readline(), self.readline() while self.eoc not in line: lines.append(line) line = self.readline() except Exception, e: lines.append( '** PythonInterpreter: There was an error.' ) self.eof = True return lines
if __name__ == '__main__': interp = PythonInterpreter(forbidden=['import']) while not interp.eof: cmd = raw_input(interp.prompt()) lines = interp.process(cmd) for line in lines: print line print " Goodbye."
Hope you enjoy. More on trypy soon.
« Previous Page