App Engine ProtoRPC Basics

What is ProtoRPC?

I found out about ProtoRPC when I was looking for a way to expose a web service from a Google App Engine application.  I was investigating several different ways that would allow me to receive requests, process those requests, and send back a response.  Out of the options of manually serializing XML or JSON, extracting GET/POST parameters from the HTTP request, or ProtoRPC, ProtoRPC was a great choice.

One of the cool things about ProtoRPC is that it automatically handles the serialization and deserialization of the responses and requests to/from JSON.  This means that you don’t need a simplejson.loads(…) at the beginning of each web service nor a simplejson.dumps(…) at the end.

If you’re looking for a flexible yet compact way to expose a web service from your Google App Engine app, check out what ProtoRPC can offer you.

ProtoRPC Changes

To start off, I just want to point out a few things about ProtoRPC that have changed from a lot of the documentation that I’ve seen.  Since it is still in the experimental stage, I have a feeling that this blog post will be outdated soon enough, too, as Google continues development on the ProtoRPC API.

  • service_handlers has moved from the protorpc namespace to protorpc.webapp (The Google overview of ProtoRPC is outdated)
    • I found this out initially by experiencing this error: ImportError: cannot import name service_handlers

Change

from protorpc import service_handlers

to

from protorpc.webapp import service_handlers
  • The @remote.remote(Request,Response) decorator has changed to @remote.method(Request,Response)

Change

@remote.remote(GetRequest, GetResponse)
def get(self, request):

to

@remote.method(GetRequest, GetResponse)
def get(self, request):

Creating the Data Model

I’d like to create a tracking application for my collection of LOLcats, and where better to host it than in the Google App Engine cloud.  For now, it’ll be basic tracking of only their name, color, and age, but there’s always room for expansion in the future.

from google.appengine.ext import webapp
from google.appengine.ext.webapp import util
from google.appengine.ext import db

class MainHandler(webapp.RequestHandler):
    def get(self):
        self.response.out.write('Welcome to the VexedLogic ProtoRPC test application!')

class LolCat(db.Model):
	name = db.StringProperty(required=True)
	color = db.StringProperty(required=True)
	age = db.IntegerProperty(required=True)

def main():
    application = webapp.WSGIApplication([('/', MainHandler)],
                                         debug=True)
    util.run_wsgi_app(application)

if __name__ == '__main__':
    main()

Creating the ProtoRPC Service Methods

Now we’re to the fun part… actually creating the new ProtoRPC service to manage my collection of LOLcats.  Since I would never dream of getting rid of one of them, the only two method that I will implement are add and get.  I’m going to start a new Python file called lolcat_service.py to contain all of my service logic.

Add Method

First, the request and response of the add method need to be defined.  Since there isn’t anything in particular that needs to be returned to the caller, I’ll use VoidMessage, which is the built-in “empty” type.

Unfortunately, we cannot reuse the same model class that was created in main.py, the request/response classes need to inherit from protorpc.messages.Message.

from google.appengine.ext import webapp
from google.appengine.ext.webapp import util
from google.appengine.api import users

from protorpc import messages
from protorpc import message_types
from protorpc import remote
import main

class Cat(messages.Message):
 name = messages.StringField(1, required=True)
 color = messages.StringField(2, required=True)
 age = messages.IntegerField(3, required=True)

class AddCatRequest(messages.Message):
 cat = messages.MessageField(Cat, 1, required=True)

You’ll notice the definition of Cat message looks very similar to the LolCat model with a few differences.  The main difference is that the fields on the message class need unique integer values provided, which are used for field identification by ProtoRPC.  I’d like to reuse the Cat definition in the get method, which is why I make a separate Cat message instead of include all 3 properties in the AddCatRequest message

The add method looks like this:

class lolcat_service(remote.Service):
	@remote.method(AddCatRequest, message_types.VoidMessage)
	def add_cat(self, request):
		main.LolCat(name = request.cat.name,
					color = request.cat.color,
					age = request.cat.age).put()

		return message_types.VoidMessage()

Get Method

On to the get method… this one is a little bit more complex since the user should be allowed to specify some criteria with the request to get the listing of cats.

When the get method is all complete, the lolcat_service.py looks like this:

from google.appengine.ext import webapp
from google.appengine.ext.webapp import util
from google.appengine.api import users

from protorpc import messages
from protorpc import message_types
from protorpc import remote
import main

class Cat(messages.Message):
	name = messages.StringField(1, required=True)
	color = messages.StringField(2, required=True)
	age = messages.IntegerField(3, required=True)

class AddCatRequest(messages.Message):
	cat = messages.MessageField(Cat, 1, required=True)

class GetCatsRequest(messages.Message):
	min_age = messages.IntegerField(1)
	color = messages.StringField(2)
	max_results = messages.IntegerField(3, default=100)

class GetCatsResponse(messages.Message):
	cats = messages.MessageField(Cat, 1, repeated=True)

class lolcat_service(remote.Service):
	@remote.method(AddCatRequest, message_types.VoidMessage)
	def add_cat(self, request):
		main.LolCat(name = request.cat.name,
					color = request.cat.color,
					age = request.cat.age).put()

		return message_types.VoidMessage()

	@remote.method(GetCatsRequest, GetCatsResponse)
	def get_cats(self, request):
		query = main.LolCat.all()

		if request.min_age:
			query.filter('age >=', request.min_age)
		if request.color:
			query.filter('color =', request.color)

		cats = []
		for cat_model in query.fetch(request.max_results):
			cat = Cat(name=cat_model.name,
					  color=cat_model.color,
					  age=cat_model.age)
			cats.append(cat)

		return GetCatsResponse(cats = cats)

Registering the Service

Create a new services.py file that will define the service mapping for the service that was just created.

from google.appengine.ext import webapp
from google.appengine.ext.webapp import util

from protorpc.webapp import service_handlers

import lolcat_service

# Register mapping with application.
application = webapp.WSGIApplication(
  service_handlers.service_mapping(
    [('/service', lolcat_service.lolcat_service)]),
  debug=True)

def main():
  util.run_wsgi_app(application)

if __name__ == '__main__':
  main()

Finally, map the handler in the app.yaml file by adding the following block under the handlers section:

- url: /service.*
  script: services.py

That’s it!  You can now visit the service at http://localhost:8080/service.add_cat (replace 8080 with the correct port number) and be greeted with the “/service.add_cat is a ProtoRPC method.” message.

Testing your ProtoRPC Service

Since the generic “/service.add_cat is a ProtoRPC method.” isn’t very comforting in assuring me that my service works as expected, I put together two HTML files that will call these services and display the results in the alert box.

This requires adding one more handler mapping in the app.yaml file:

- url: /static
  static_dir: static

Create a new static folder in the root of your project directory.

Testing the Add Method

The following HTML file will make three requests to the add_cat method to add the following cats:

  • Long Cat (Color: White / Age: 1)
  • Tacgnol (Color: Black/ Age: 7)
  • Monorail Cat (Color: Brown / Age: 10)

If successful, when you browse to http://localhost:8080/static/TestAdd.html, you should see three alert dialogs that contain “{}”, which is the contents of the VoidMessage that the add_cat method returns.

<html>
  <head>
    <script type="text/javascript"
    src="https://ajax.googleapis.com/ajax/libs/jquery/1.6.2/jquery.min.js">
    </script>
  </head>
  <script>
  $.ajax({url: '/service.add_cat', type: 'POST', contentType:
  'application/json',
  data: '{"cat" : {"name": "Long Cat", "color" : "white", "age" : 1}}',
  dataType: 'html',
  error:function(response, one, two) {
  alert(response.responseText); alert(one); alert(two); }, success:
  function(json) { alert(json); } });

  $.ajax({url: '/service.add_cat', type: 'POST', contentType:
  'application/json',
  data: '{"cat" : {"name": "Tacgnol", "color" : "black", "age" : 7}}',
  dataType: 'html',
  error:function(response, one, two) {
  alert(response.responseText); alert(one); alert(two); }, success:
  function(json) { alert(json); } });

  $.ajax({url: '/service.add_cat', type: 'POST', contentType:
  'application/json',
  data: '{"cat" : {"name": "Monorail Cat", "color" : "brown", "age" : 10}}',
  dataType: 'html',
  error:function(response, one, two) {
  alert(response.responseText); alert(one); alert(two); }, success:
  function(json) { alert(json); } });
  </script>
  <body></body>
</html>

Testing the Get Method

The following HTML file will make a request to the get_cats method.  By default, if this method is called without the data property, it will return a maximum of 100 cats with any age and color.

The data property of the AJAX request can be manipulated to specify the criteria for the get_cats method.  This property is deserialized into a GetCatsRequest object when received by the get_cats method.  I’ve provided a few examples of different search criteria that are commented out below.

<html>
  <head>
    <script type="text/javascript"
    src="https://ajax.googleapis.com/ajax/libs/jquery/1.6.2/jquery.min.js">
    </script>
  </head>
  <script>
  $.ajax({url: '/service.get_cats', type: 'POST', contentType:
  'application/json',
  <!-- data: '{"min_age": 7, "color": "black"}', -->
  <!-- data: '{"min_age": 7}', -->
  <!-- data: '{"max_results": 2}', -->
  <!-- data: '{"min_age": 7, "color": "black"}', -->
  dataType: 'html',
  error:function(response, one, two) {
  alert(response.responseText); alert(one); alert(two); }, success:
  function(json) { alert(json); } });
  </script>
  <body></body>
</html>

Tags: , , , , , ,

This entry was posted on Saturday, August 20th, 2011 at 2:13 pm and is filed under App Engine. You can follow any responses to this entry through the RSS 2.0 feed. You can leave a response, or trackback from your own site.

Leave a Reply

You must be logged in to post a comment.