Hello Robot
This slice shows how to port Google's "Hello, Robot" bot to w2p. Please see http://code.google.com/intl/de/apis/wave/extensions/robots/python-tutorial.html for background info and API download links.
- 2009-11-04 There is a wave on wave development with web2py
- 2009-10-31 Added controller example for w2p 1.70.1
Prerequisites
- Google Wave API for Python (waveapi)
- Google Appengine SDK
Web2py
- Create "init" app
- Create "robot" controller (see code below)
- Rename web2py/routes.examples.py to routes.py
- Edit routes.py (see below)
- Copy waveapi directory to applications/init/modules
routes.py
Google Wave calls your bot (your bot never calls wave) on the following endpoints:
- http://<yourbot>.appspot.com/_wave/capabilities.xml (returns events your bot is interested in)
- http://<yourbot>.appspot.com/_wave/robot/profile (a json representation of your bot's icon URL, etc.)
- http:://<yourbot>.appspot.com/_wave/robot/jsonrpc (the rpc interface)
Change routes.py to forward all calls to /init/robot/process:
routes_in = ( ('/_wave/(?P<wave>.*)','/init/robot/process/\g<wave>'), )
Robot Controller Code (web2py 1.70.1)
This controller does not follow the Google example as closely as the pre 1.70.1 example below, it does however make use of w2p's new local_import function and the fact that the wsgi environment and start_response can now be accessed through request.wsgi. There appear to remain some issues with w2p's wsgi support - see http://groups.google.com/group/web2py/browse_thread/thread/5faa2e926e05e200
# coding: utf8
import sys
from google.appengine.ext import webapp
robot = local_import('waveapi.robot')
class MyRobot(robot.Robot):
def __init__(self, name, version, image_url, profile_url, controller_path):
self.controller_path = controller_path
robot.Robot.__init__(self, name,
version = version,
image_url = image_url,
profile_url = profile_url)
self.RegisterListener(self)
def _get_wsgi_app(self):
p = self.controller_path
return webapp.WSGIApplication([
('%s/capabilities.xml' % p,
lambda: robot.RobotCapabilitiesHandler(self)),
('%s/robot/profile' % p,
lambda: robot.RobotProfileHandler(self)),
('%s/robot/jsonrpc' % p,
lambda: robot.RobotEventHandler(self)),
], debug=False)
app = property(_get_wsgi_app)
def OnWaveletSelfAdded(self, properties, context):
root_wavelet = context.GetRootWavelet()
root_wavelet.CreateBlip().GetDocument().SetText("I'm alive!")
def OnWaveletSelfRemoved(self, properties, context):
pass
def OnWaveletParticipantsChanged(self, properties, context):
pass
def process():
server = 'http://wavedirectory.appspot.com'
bot = MyRobot(name = 'Wavedirectory',
version = '1.0',
image_url = server + URL(r=request, c='static', f='logo.png'),
profile_url = server + URL(r=request, f='profile'),
controller_path = URL(r=request))
# w2p does not include wsgi.input in environ for some reason
# also one has to rewind - seek(0) - the StringIO buffer
request.wsgi.environ['wsgi.input'] = request.env.wsgi_input
request.wsgi.environ['wsgi.input'].seek(0)
bot.app(request.wsgi.environ, request.wsgi.start_response)
# response_start() returns response.body.write which is used by
# Google's webapp.WSGIApplication to write its response.
return response.body.getvalue()
Notes
- Although in the API Wave does not seem to call the bot on the OnWaveletSelfRemoved event.
- Cron - also in the API and not yet supported: bot.RegisterCronJob(self, path, seconds)
Robot Controller Code (pre web2py 1.70.1)
# coding: utf8
import sys
import functools
from google.appengine.ext import webapp
path = 'applications/%s/modules' % request.application
if not path in sys.path: sys.path.append(path)
# Robot
# identical to the "Hello, Robot" tutorial except that in the last line
# the call myRobot.Run() is not made.
from waveapi import events
from waveapi import model
from waveapi import robot
def OnParticipantsChanged(properties, context):
"""Invoked when any participants have been added/removed."""
added = properties['participantsAdded']
for p in added:
Notify(context)
def OnRobotAdded(properties, context):
"""Invoked when the robot has been added."""
root_wavelet = context.GetRootWavelet()
root_wavelet.CreateBlip().GetDocument().SetText("I'm alive!")
def Notify(context):
root_wavelet = context.GetRootWavelet()
root_wavelet.CreateBlip().GetDocument().SetText("Hi everybody!")
myRobot = robot.Robot('wavedirectory',
image_url='http://wavedirectory.appspot.com/icon.png',
version='1',
profile_url='http://wavedirectory.appspot.com/')
myRobot.RegisterHandler(events.WAVELET_PARTICIPANTS_CHANGED,
OnParticipantsChanged)
myRobot.RegisterHandler(events.WAVELET_SELF_ADDED, OnRobotAdded)
# myRobot.Run()
# Web2py function
def process():
def start_response(status, headers, exc_info=None):
http_code, _ = status.split(' ',1)
if http_code == '200':
for k,v in headers:
response.headers[k] = v
else:
raise HTTP(http_code)
return functools.partial(response.write, escape=False)
environment = dict()
denormalize = lambda x: x.replace('_', '.', 1) if x.startswith('wsgi') \
else x.upper()
for k, v in request.env.items():
environment[denormalize(k)] = v
environment['wsgi.input'].seek(0)
app = webapp.WSGIApplication([
('/init/robot/process/capabilities.xml',
lambda: robot.RobotCapabilitiesHandler(myRobot)),
('/init/robot/process/robot/profile',
lambda: robot.RobotProfileHandler(myRobot)),
('/init/robot/process/robot/jsonrpc',
lambda: robot.RobotEventHandler(myRobot)),
], debug=False)
app(environment, start_response)
return response.body.getvalue()
Discussion
Wave bots are essentially WSGI applications (see http://wsgi.org). Google's webapp.WSGIApplication is invoked like any WSGI app
app(environment, start_response)
w2p's request.env does not contain the original environment as w2p changes evironment keys to lowercase and replaces dots with underscores. This is reversed in the denormalize lambda above.
Google's webapp.WSGIApplication expects start_response to return a 'write' function that it uses to write the HTTP body. I return w2p's response.write (which under the hood writes to the cStringIO buffer response.body) as a partial function application to stop web2py from escaping what is written to the buffer. Finally the controller returns the contents of this buffer.
Notes
- You need to run this web2py app inside GAE dev_appserver under Python 2.5.
- The same approach should work to call any WSGI app however according to the WSGI tuts I've read the body is usually not written by calling a write-function returned by start_response. Instead the call app(env, start_response) normally returns a generator.
Comments (0)