If you benefit from web2py hope you feel encouraged to pay it forward by contributing back to society in whatever form you choose!

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.

Related slices

Comments (0)


Hosting graciously provided by:
Python Anywhere