<filename>src/python/api/appengine_robot_runner.py
#!/usr/bin/python
#
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements.  See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership.  The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License.  You may obtain a copy of the License at
#
#   http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied.  See the License for the
# specific language governing permissions and limitations
# under the License.

"""A module to run wave robots on app engine."""


import logging
import sys

import events

from google.appengine.api import urlfetch
from google.appengine.ext import webapp
from google.appengine.ext.webapp.util import run_wsgi_app


class CapabilitiesHandler(webapp.RequestHandler):
  """Handler to forward a request ot a handler of a robot."""

  def __init__(self, method, contenttype):
    """Initializes this handler with a specific robot."""
    self._method = method
    self._contenttype = contenttype

  def get(self):
    """Handles HTTP GET request."""
    self.response.headers['Content-Type'] = self._contenttype
    self.response.out.write(self._method())

class ProfileHandler(webapp.RequestHandler):
  """Handler to forward a request ot a handler of a robot."""

  def __init__(self, method, contenttype):
    """Initializes this handler with a specific robot."""
    self._method = method
    self._contenttype = contenttype

  def get(self):
    """Handles HTTP GET request."""
    self.response.headers['Content-Type'] = self._contenttype
    # Respond with proxied profile if name specified
    if self.request.get('name'):
      self.response.out.write(self._method(self.request.get('name')))
    else:
      self.response.out.write(self._method())

class RobotEventHandler(webapp.RequestHandler):
  """Handler for the dispatching of events to various handlers to a robot.

  This handler only responds to post events with a JSON post body. Its primary
  task is to separate out the context data from the events in the post body
  and dispatch all events in order. Once all events have been dispatched
  it serializes the context data and its associated operations as a response.
  """

  def __init__(self, robot):
    """Initializes self with a specific robot."""
    self._robot = robot

  def get(self):
    """Handles the get event for debugging.

    This is useful for debugging but since event bundles tend to be
    rather big it often won't fit for more complex requests.
    """
    ops = self.request.get('events')
    if ops:
      self.request.body = events
      self.post()

  def post(self):
    """Handles HTTP POST requests."""
    json_body = self.request.body
    if not json_body:
      # TODO(davidbyttow): Log error?
      return

    # Redirect stdout to stderr while executing handlers. This way, any stray
    # "print" statements in bot code go to the error logs instead of breaking
    # the JSON response sent to the HTTP channel.
    saved_stdout, sys.stdout = sys.stdout, sys.stderr

    json_body = unicode(json_body, 'utf8')
    logging.info('Incoming: %s', json_body)
    json_response = self._robot.process_events(json_body)
    logging.info('Outgoing: %s', json_response)

    sys.stdout = saved_stdout

    # Build the response.
    self.response.headers['Content-Type'] = 'application/json; charset=utf-8'
    self.response.out.write(json_response.encode('utf-8'))


def operation_error_handler(event, wavelet):
  """Default operation error handler, logging what went wrong."""
  if isinstance(event, events.OperationError):
    logging.error('Previously operation failed: id=%s, message: %s',
                  event.operation_id, event.error_message)


def appengine_post(url, data, headers):
  result = urlfetch.fetch(
      method='POST',
      url=url,
      payload=data,
      headers=headers,
      deadline=10)
  return result.status_code, result.content


class RobotVerifyTokenHandler(webapp.RequestHandler):
  """Handler for the token_verify request."""

  def __init__(self, robot):
    """Initializes self with a specific robot."""
    self._robot = robot

  def get(self):
    """Handles the get event for debugging. Ops usually too long."""
    token, st = self._robot.get_verification_token_info()
    logging.info('token=' + token)
    if token is None:
      self.error(404)
      self.response.out.write('No token set')
      return
    if st is not None:
      if self.request.get('st') != st:
        self.response.out.write('Invalid st value passed')
        return
    self.response.out.write(token)


def create_robot_webapp(robot, debug=False, extra_handlers=None):
  """Returns an instance of webapp.WSGIApplication with robot handlers."""
  if not extra_handlers:
    extra_handlers = []
  return webapp.WSGIApplication([('.*/_wave/capabilities.xml',
                                  lambda: CapabilitiesHandler(
                                                     robot.capabilities_xml,
                                                     'application/xml')),
                                 ('.*/_wave/robot/profile',
                                  lambda: ProfileHandler(
                                                     robot.profile_json,
                                                     'application/json')),
                                 ('.*/_wave/robot/jsonrpc',
                                  lambda: RobotEventHandler(robot)),
                                 ('.*/_wave/verify_token',
                                  lambda: RobotVerifyTokenHandler(robot)),
                                ] + extra_handlers,
                                debug=debug)


def run(robot, debug=False, log_errors=True, extra_handlers=None):
  """Sets up the webapp handlers for this robot and starts listening.

    A robot is typically setup in the following steps:
      1. Instantiate and define robot.
      2. Register various handlers that it is interested in.
      3. Call Run, which will setup the handlers for the app.
    For example:
      robot = Robot('Terminator',
                    image_url='http://www.sky.net/models/t800.png',
                    profile_url='http://www.sky.net/models/t800.html')
      robot.register_handler(WAVELET_PARTICIPANTS_CHANGED, KillParticipant)
      run(robot)

    Args:
      robot: the robot to run. This robot is modified to use app engines
          urlfetch for posting http.
      debug: Optional variable that defaults to False and is passed through
          to the webapp application to determine if it should show debug info.
      log_errors: Optional flag that defaults to True and determines whether
          a default handlers to catch errors should be setup that uses the
          app engine logging to log errors.
      extra_handlers: Optional list of tuples that are passed to the webapp
          to install more handlers. For example, passing
            [('/about', AboutHandler),] would install an extra about handler
            for the robot.
  """
  # App Engine expects to construct a class with no arguments, so we
  # pass a lambda that constructs the appropriate handler with
  # arguments from the enclosing scope.
  if log_errors:
    robot.register_handler(events.OperationError, operation_error_handler)
  robot.set_http_post(appengine_post)
  app = create_robot_webapp(robot, debug, extra_handlers)
  run_wsgi_app(app)

<filename>src/python/api/appengine_robot_runner.py
#!/usr/bin/python
#
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements.  See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership.  The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License.  You may obtain a copy of the License at
#
#   http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied.  See the License for the
# specific language governing permissions and limitations
# under the License.

"""A module to run wave robots on app engine."""


import logging
import sys

import events

from google.appengine.api import urlfetch
from google.appengine.ext import webapp
from google.appengine.ext.webapp.util import run_wsgi_app


class CapabilitiesHandler(webapp.RequestHandler):
  """Handler to forward a request ot a handler of a robot."""

  def __init__(self, method, contenttype):
    """Initializes this handler with a specific robot."""
    self._method = method
    self._contenttype = contenttype

  def get(self):
    """Handles HTTP GET request."""
    self.response.headers['Content-Type'] = self._contenttype
    self.response.out.write(self._method())

class ProfileHandler(webapp.RequestHandler):
  """Handler to forward a request ot a handler of a robot."""

  def __init__(self, method, contenttype):
    """Initializes this handler with a specific robot."""
    self._method = method
    self._contenttype = contenttype

  def get(self):
    """Handles HTTP GET request."""
    self.response.headers['Content-Type'] = self._contenttype
    # Respond with proxied profile if name specified
    if self.request.get('name'):
      self.response.out.write(self._method(self.request.get('name')))
    else:
      self.response.out.write(self._method())

class RobotEventHandler(webapp.RequestHandler):
  """Handler for the dispatching of events to various handlers to a robot.

  This handler only responds to post events with a JSON post body. Its primary
  task is to separate out the context data from the events in the post body
  and dispatch all events in order. Once all events have been dispatched
  it serializes the context data and its associated operations as a response.
  """

  def __init__(self, robot):
    """Initializes self with a specific robot."""
    self._robot = robot

  def get(self):
    """Handles the get event for debugging.

    This is useful for debugging but since event bundles tend to be
    rather big it often won't fit for more complex requests.
    """
    ops = self.request.get('events')
    if ops:
      self.request.body = events
      self.post()

  def post(self):
    """Handles HTTP POST requests."""
    json_body = self.request.body
    if not json_body:
      # TODO(davidbyttow): Log error?
      return

    # Redirect stdout to stderr while executing handlers. This way, any stray
    # "print" statements in bot code go to the error logs instead of breaking
    # the JSON response sent to the HTTP channel.
    saved_stdout, sys.stdout = sys.stdout, sys.stderr

    json_body = unicode(json_body, 'utf8')
    logging.info('Incoming: %s', json_body)
    json_response = self._robot.process_events(json_body)
    logging.info('Outgoing: %s', json_response)

    sys.stdout = saved_stdout

    # Build the response.
    self.response.headers['Content-Type'] = 'application/json; charset=utf-8'
    self.response.out.write(json_response.encode('utf-8'))


def operation_error_handler(event, wavelet):
  """Default operation error handler, logging what went wrong."""
  if isinstance(event, events.OperationError):
    logging.error('Previously operation failed: id=%s, message: %s',
                  event.operation_id, event.error_message)


def appengine_post(url, data, headers):
  result = urlfetch.fetch(
      method='POST',
      url=url,
      payload=data,
      headers=headers,
      deadline=10)
  return result.status_code, result.content


class RobotVerifyTokenHandler(webapp.RequestHandler):
  """Handler for the token_verify request."""

  def __init__(self, robot):
    """Initializes self with a specific robot."""
    self._robot = robot

  def get(self):
    """Handles the get event for debugging. Ops usually too long."""
    token, st = self._robot.get_verification_token_info()
    logging.info('token=' + token)
    if token is None:
      self.error(404)
      self.response.out.write('No token set')
      return
    if st is not None:
      if self.request.get('st') != st:
        self.response.out.write('Invalid st value passed')
        return
    self.response.out.write(token)


def create_robot_webapp(robot, debug=False, extra_handlers=None):
  """Returns an instance of webapp.WSGIApplication with robot handlers."""
  if not extra_handlers:
    extra_handlers = []
  return webapp.WSGIApplication([('.*/_wave/capabilities.xml',
                                  lambda: CapabilitiesHandler(
                                                     robot.capabilities_xml,
                                                     'application/xml')),
                                 ('.*/_wave/robot/profile',
                                  lambda: ProfileHandler(
                                                     robot.profile_json,
                                                     'application/json')),
                                 ('.*/_wave/robot/jsonrpc',
                                  lambda: RobotEventHandler(robot)),
                                 ('.*/_wave/verify_token',
                                  lambda: RobotVerifyTokenHandler(robot)),
                                ] + extra_handlers,
                                debug=debug)


def run(robot, debug=False, log_errors=True, extra_handlers=None):
  """Sets up the webapp handlers for this robot and starts listening.

    A robot is typically setup in the following steps:
      1. Instantiate and define robot.
      2. Register various handlers that it is interested in.
      3. Call Run, which will setup the handlers for the app.
    For example:
      robot = Robot('Terminator',
                    image_url='http://www.sky.net/models/t800.png',
                    profile_url='http://www.sky.net/models/t800.html')
      robot.register_handler(WAVELET_PARTICIPANTS_CHANGED, KillParticipant)
      run(robot)

    Args:
      robot: the robot to run. This robot is modified to use app engines
          urlfetch for posting http.
      debug: Optional variable that defaults to False and is passed through
          to the webapp application to determine if it should show debug info.
      log_errors: Optional flag that defaults to True and determines whether
          a default handlers to catch errors should be setup that uses the
          app engine logging to log errors.
      extra_handlers: Optional list of tuples that are passed to the webapp
          to install more handlers. For example, passing
            [('/about', AboutHandler),] would install an extra about handler
            for the robot.
  """
  # App Engine expects to construct a class with no arguments, so we
  # pass a lambda that constructs the appropriate handler with
  # arguments from the enclosing scope.
  if log_errors:
    robot.register_handler(events.OperationError, operation_error_handler)
  robot.set_http_post(appengine_post)
  app = create_robot_webapp(robot, debug, extra_handlers)
  run_wsgi_app(app)

#!/usr/bin/python # # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements.  See the NOTICE file # distributed with this work for additional information # regarding copyright ownership.  The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License.  You may obtain a copy of the License at # #   http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied.  See the License for the # specific language governing permissions and limitations # under the License. A module to run wave robots on app engine. Handler to forward a request ot a handler of a robot. Initializes this handler with a specific robot. Handles HTTP GET request. Handler to forward a request ot a handler of a robot. Initializes this handler with a specific robot. Handles HTTP GET request. # Respond with proxied profile if name specified Handler for the dispatching of events to various handlers to a robot.    This handler only responds to post events with a JSON post body. Its primary   task is to separate out the context data from the events in the post body   and dispatch all events in order. Once all events have been dispatched   it serializes the context data and its associated operations as a response. Initializes self with a specific robot. Handles the get event for debugging.      This is useful for debugging but since event bundles tend to be     rather big it often won't fit for more complex requests. Handles HTTP POST requests. # TODO(davidbyttow): Log error? # Redirect stdout to stderr while executing handlers. This way, any stray # "print" statements in bot code go to the error logs instead of breaking # the JSON response sent to the HTTP channel. # Build the response. Default operation error handler, logging what went wrong. Handler for the token_verify request. Initializes self with a specific robot. Handles the get event for debugging. Ops usually too long. Returns an instance of webapp.WSGIApplication with robot handlers. Sets up the webapp handlers for this robot and starts listening.      A robot is typically setup in the following steps:       1. Instantiate and define robot.       2. Register various handlers that it is interested in.       3. Call Run, which will setup the handlers for the app.     For example:       robot = Robot('Terminator',                     image_url='http://www.sky.net/models/t800.png',                     profile_url='http://www.sky.net/models/t800.html')       robot.register_handler(WAVELET_PARTICIPANTS_CHANGED, KillParticipant)       run(robot)      Args:       robot: the robot to run. This robot is modified to use app engines           urlfetch for posting http.       debug: Optional variable that defaults to False and is passed through           to the webapp application to determine if it should show debug info.       log_errors: Optional flag that defaults to True and determines whether           a default handlers to catch errors should be setup that uses the           app engine logging to log errors.       extra_handlers: Optional list of tuples that are passed to the webapp           to install more handlers. For example, passing             [('/about', AboutHandler),] would install an extra about handler             for the robot. # App Engine expects to construct a class with no arguments, so we # pass a lambda that constructs the appropriate handler with # arguments from the enclosing scope.