Remote procedure calls

web2py provides a mechanism to turn any function into a web service. The mechanism described here differs from the mechanism described before because:

  • The function may take arguments
  • The function may be defined in a model or a module instead of controller
  • You may want to specify in detail which RPC method should be supported
  • It enforces a more strict URL naming convention
  • It is smarter than the previous methods because it works for a fixed set of protocols. For the same reason it is not as easily extensible.

To use this feature:

First, you must import and initiate a service object.

  1. from gluon.tools import Service
  2. service = Service()

This is already done in the “db.py” model file in the scaffolding application.

Second, you must expose the service handler in the controller:

  1. def call():
  2. session.forget()
  3. return service()

This is already done in the “default.py” controller of the scaffolding application. Remove session.forget() if you plan to use session cookies with the services.

Third, you must decorate those functions you want to expose as a service. Here is a list of currently supported decorators:

  1. @service.run
  2. @service.xml
  3. @service.json
  4. @service.rss
  5. @service.csv
  6. @service.xmlrpc
  7. @service.jsonrpc
  8. @service.jsonrpc2
  9. @service.amfrpc3('domain')
  10. @service.soap('FunctionName', returns={'result': type}, args={'param1': type,})

As an example, consider the following decorated function:

  1. @service.run
  2. def concat(a, b):
  3. return a + b

This function can be defined in a model or in the controller where the call action is defined. This function can now be called remotely in two ways:

  1. http://127.0.0.1:8000/app/default/call/run/concat?a=hello&b=world
  2. http://127.0.0.1:8000/app/default/call/run/concat/hello/world

In both cases the http request returns:

  1. helloworld

If the @service.xml decorator is used, the function can be called via:

  1. http://127.0.0.1:8000/app/default/call/xml/concat?a=hello&b=world
  2. http://127.0.0.1:8000/app/default/call/xml/concat/hello/world

and the output is returned as XML:

  1. <document>
  2. <result>helloworld</result>
  3. </document>

It can serialize the output of the function even if this is a DAL Rows object. In this case, in fact, it will call as_list() automatically.

If the @service.json decorator is used, the function can be called via:

  1. http://127.0.0.1:8000/app/default/call/json/concat?a=hello&b=world
  2. http://127.0.0.1:8000/app/default/call/json/concat/hello/world

and the output returned as JSON.

If the @service.csv decorator is used, the service handler requires, as the return value, an iterable object of iterable objects, such as a list of lists. Here is an example:

  1. @service.csv
  2. def table1(a, b):
  3. return [[a, b], [1, 2]]

This service can be called by visiting one of the following URLs:

  1. http://127.0.0.1:8000/app/default/call/csv/table1?a=hello&b=world
  2. http://127.0.0.1:8000/app/default/call/csv/table1/hello/world

and it returns:

  1. hello,world
  2. 1,2

The @service.rss decorator expects a return value in the same format as the “generic.rss” view discussed in the previous section.

Multiple decorators are allowed for each function.

So far, everything discussed in this section is simply an alternative to the method described in the previous section. The real power of the service object comes with XMLRPC, JSONRPC and AMFRPC, as discussed below.

XMLRPC

Consider the following code, for example, in the “default.py” controller:

  1. @service.xmlrpc
  2. def add(a, b):
  3. return a + b
  4. @service.xmlrpc
  5. def div(a, b):
  6. return a / b

Now in a python shell you can do

  1. >>> from xmlrpclib import ServerProxy
  2. >>> server = ServerProxy(
  3. 'http://127.0.0.1:8000/app/default/call/xmlrpc')
  4. >>> print server.add(3, 4)
  5. 7
  6. >>> print server.add('hello', 'world')
  7. 'helloworld'
  8. >>> print server.div(12, 4)
  9. 3
  10. >>> print server.div(1, 0)
  11. ZeroDivisionError: integer division or modulo by zero

The Python xmlrpclib module provides a client for the XMLRPC protocol. web2py acts as the server.

The client connects to the server via ServerProxy and can remotely call decorated functions in the server. The data (a, b) is passed to the function(s), not via GET/POST variables, but properly encoded in the request body using the XMLPRC protocol, and thus it carries with itself type information (int or string or other). The same is true for the return value(s). Moreover, any exception raised on the server propagates back to the client.

ServerProxy signature

  1. a_server = ServerProxy(location, transport=None, encoding=None, verbose=False, version=None)

The important arguments are:

  • location is the remote URL for the server. There are examples below.
  • verbose=True activates useful diagnostics
  • version sets the jsonrpc version. It is ignored by jsonrpc. Set this to version='2.0' to support jsonrpc2. Because it is ignored by jsonrpc, setting it gains support for both versions. It is not supported by XMLRPC.

XMLRPC Libraries

There are XMLRPC libraries for many programming languages (including C, C++, Java, C#, Ruby, and Perl), and they can interoperate with each other. This is one the best methods to create applications that talk to each other independent of the programming language.

The XMLRPC client can also be implemented inside a web2py action, so that one action can talk to another web2py application (even within the same installation) using XMLRPC. Beware of session deadlocks in this case. If an action calls via XMLRPC a function in the same app, the caller must release the session lock before the call:

  1. session.forget(response)

JSONRPC

In this section we are going to use the same code example as for XMLRPC but we will expose the service using JSONRPC instead:

  1. @service.jsonrpc
  2. @service.jsonrpc2
  3. def add(a, b):
  4. return a + b
  5. def call():
  6. return service()

JSONRPC is very similar to XMLRPC but uses JSON instead of XML as data serialization protocol.

Accessing JSONRPC services from web2py

Of course we can call the service from any program in any language but here we will do it in Python. web2py ships with a module “gluon/contrib/simplejsonrpc.py” created by Mariano Reingart. Here is an example of how to use to call the above service:

  1. >>> from gluon.contrib.simplejsonrpc import ServerProxy
  2. >>> URL = "http://127.0.0.1:8000/app/default/call/jsonrpc"
  3. >>> service = ServerProxy(URL, verbose=True)
  4. >>> print service.add(1, 2)

Use “http://127.0.0.1:8000/app/default/call/jsonrpc2“ for jsonrpc2, and create the service object like this:

  1. service = ServerProxy(URL, verbose=True, version='2.0')

JSONRPC and Pyjamas

As an example of application here, we discuss the usage of JSON Remote Procedure Calls with Pyjamas. Pyjamas is a Python port of the Google Web Toolkit (originally written in Java). Pyjamas allows writing a client application in Python. Pyjamas translates this code into JavaScript. web2py serves the JavaScript and communicates with it via AJAX requests originating from the client and triggered by user actions.

Here we describe how to make Pyjamas work with web2py. It does not require any additional libraries other than web2py and Pyjamas.

We are going to build a simple “todo” application with a Pyjamas client (all JavaScript) that talks to the server exclusively via JSONRPC.

First, create a new application called “todo”.

Second, in “models/db.py”, enter the following code:

  1. db=DAL('sqlite://storage.sqlite')
  2. db.define_table('todo', Field('task'))
  3. service = Service()

(Note: Service class is from gluon.tools).

Third, in “controllers/default.py”, enter the following code:

  1. def index():
  2. redirect(URL('todoApp'))
  3. @service.jsonrpc
  4. def getTasks():
  5. todos = db(db.todo).select()
  6. return [(todo.task, todo.id) for todo in todos]
  7. @service.jsonrpc
  8. def addTask(taskFromJson):
  9. db.todo.insert(task=taskFromJson)
  10. return getTasks()
  11. @service.jsonrpc
  12. def deleteTask (idFromJson):
  13. del db.todo[idFromJson]
  14. return getTasks()
  15. def call():
  16. session.forget()
  17. return service()
  18. def todoApp():
  19. return dict()

The purpose of each function should be obvious.

Fourth, in “views/default/todoApp.html”, enter the following code:

  1. <html>
  2. <head>
  3. <meta name="pygwt:module"
  4. content="{{=URL('static', 'output/TodoApp')}}" />
  5. <title>
  6. simple todo application
  7. </title>
  8. </head>
  9. <body bgcolor="white">
  10. <h1>
  11. simple todo application
  12. </h1>
  13. <i>
  14. type a new task to insert in db,
  15. click on existing task to delete it
  16. </i>
  17. <script language="javascript"
  18. src="{{=URL('static', 'output/pygwt.js')}}">
  19. </script>
  20. </body>
  21. </html>

This view just executes the Pyjamas code in “static/output/todoapp” - code that we have not yet created.

Fifth, in “static/TodoApp.py” (notice it is TodoApp, not todoApp!), enter the following client code:

  1. from pyjamas.ui.RootPanel import RootPanel
  2. from pyjamas.ui.Label import Label
  3. from pyjamas.ui.VerticalPanel import VerticalPanel
  4. from pyjamas.ui.TextBox import TextBox
  5. import pyjamas.ui.KeyboardListener
  6. from pyjamas.ui.ListBox import ListBox
  7. from pyjamas.ui.HTML import HTML
  8. from pyjamas.JSONService import JSONProxy
  9. class TodoApp:
  10. def onModuleLoad(self):
  11. self.remote = DataService()
  12. panel = VerticalPanel()
  13. self.todoTextBox = TextBox()
  14. self.todoTextBox.addKeyboardListener(self)
  15. self.todoList = ListBox()
  16. self.todoList.setVisibleItemCount(7)
  17. self.todoList.setWidth("200px")
  18. self.todoList.addClickListener(self)
  19. self.Status = Label("")
  20. panel.add(Label("Add New Todo:"))
  21. panel.add(self.todoTextBox)
  22. panel.add(Label("Click to Remove:"))
  23. panel.add(self.todoList)
  24. panel.add(self.Status)
  25. self.remote.getTasks(self)
  26. RootPanel().add(panel)
  27. def onKeyUp(self, sender, keyCode, modifiers):
  28. pass
  29. def onKeyDown(self, sender, keyCode, modifiers):
  30. pass
  31. def onKeyPress(self, sender, keyCode, modifiers):
  32. """
  33. This function handles the onKeyPress event, and will add the
  34. item in the text box to the list when the user presses the
  35. enter key. In the future, this method will also handle the
  36. auto complete feature.
  37. """
  38. if keyCode == KeyboardListener.KEY_ENTER and sender == self.todoTextBox:
  39. id = self.remote.addTask(sender.getText(), self)
  40. sender.setText("")
  41. if id<0:
  42. RootPanel().add(HTML("Server Error or Invalid Response"))
  43. def onClick(self, sender):
  44. id = self.remote.deleteTask(
  45. sender.getValue(sender.getSelectedIndex()), self)
  46. if id<0:
  47. RootPanel().add(
  48. HTML("Server Error or Invalid Response"))
  49. def onRemoteResponse(self, response, request_info):
  50. self.todoList.clear()
  51. for task in response:
  52. self.todoList.addItem(task[0])
  53. self.todoList.setValue(self.todoList.getItemCount()-1, task[1])
  54. def onRemoteError(self, code, message, request_info):
  55. self.Status.setText("Server Error or Invalid Response: " + "ERROR " + code + " - " + message)
  56. class DataService(JSONProxy):
  57. def __init__(self):
  58. JSONProxy.__init__(self, "../../default/call/jsonrpc",
  59. ["getTasks", "addTask", "deleteTask"])
  60. if __name__ == '__main__':
  61. app = TodoApp()
  62. app.onModuleLoad()

Sixth, run Pyjamas before serving the application:

  1. cd /path/to/todo/static/
  2. python /python/pyjamas-0.5p1/bin/pyjsbuild TodoApp.py

This will translate the Python code into JavaScript so that it can be executed in the browser.

To access this application, visit the URL:

  1. http://127.0.0.1:8000/todo/default/todoApp

This subsection was created by Chris Prinos with help from Luke Kenneth Casson Leighton (creators of Pyjamas), updated by Alexei Vinidiktov. It has been tested with Pyjamas 0.5p1. The example was inspired by this Django page in ref.[blogspot1].

AMFRPC

AMFRPC is the Remote Procedure Call protocol used by Flash clients to communicate with a server. web2py supports AMFRPC, but it requires that you run web2py from source and that you preinstall the PyAMF library. This can be installed from the Linux or Windows shell by typing:

  1. easy_install pyamf

(please consult the PyAMF documentation for more details).

In this subsection we assume that you are already familiar with ActionScript programming.

We will create a simple service that takes two numerical values, adds them together, and returns the sum. We will call our web2py application “pyamf_test”, and we will call the service addNumbers.

First, using Adobe Flash (any version starting from MX 2004), create the Flash client application by starting with a new Flash FLA file. In the first frame of the file, add these lines:

  1. import mx.remoting.Service;
  2. import mx.rpc.RelayResponder;
  3. import mx.rpc.FaultEvent;
  4. import mx.rpc.ResultEvent;
  5. import mx.remoting.PendingCall;
  6. var val1 = 23;
  7. var val2 = 86;
  8. service = new Service(
  9. "http://127.0.0.1:8000/pyamf_test/default/call/amfrpc3",
  10. null, "mydomain", null, null);
  11. var pc:PendingCall = service.addNumbers(val1, val2);
  12. pc.responder = new RelayResponder(this, "onResult", "onFault");
  13. function onResult(re:ResultEvent):Void {
  14. trace("Result : " + re.result);
  15. txt_result.text = re.result;
  16. }
  17. function onFault(fault:FaultEvent):Void {
  18. trace("Fault: " + fault.fault.faultstring);
  19. }
  20. stop();

This code allows the Flash client to connect to a service that corresponds to a function called “addNumbers” in the file “/pyamf_test/default/gateway”. You must also import ActionScript version 2 MX remoting classes to enable Remoting in Flash. Add the path to these classes to the classpath settings in the Adobe Flash IDE, or just place the “mx” folder next to the newly created file.

Notice the arguments of the Service constructor. The first argument is the URL corresponding to the service that we want will create. The third argument is the domain of the service. We choose to call this domain “mydomain”.

Second, create a dynamic text field called “txt_result” and place it on the stage.

Third, you need to set up a web2py gateway that can communicate with the Flash client defined above.

Proceed by creating a new web2py app called pyamf_test that will host the new service and the AMF gateway for the flash client. Edit the “default.py” controller and make sure it contains

  1. @service.amfrpc3('mydomain')
  2. def addNumbers(val1, val2):
  3. return val1 + val2
  4. def call(): return service()

Fourth, compile and export/publish the SWF flash client as pyamf_test.swf, place the “pyamf_test.amf”, “pyamf_test.html”, “AC_RunActiveContent.js”, and “crossdomain.xml” files in the “static” folder of the newly created appliance that is hosting the gateway, “pyamf_test”.

You can now test the client by visiting:

  1. http://127.0.0.1:8000/pyamf_test/static/pyamf_test.html

The gateway is called in the background when the client connects to addNumbers.

If you are using AMF0 instead of AMF3 you can also use the decorator:

  1. @service.amfrpc

instead of:

  1. @service.amfrpc3('mydomain')

In this case you also need to change the service URL to:

  1. http://127.0.0.1:8000/pyamf_test/default/call/amfrpc

SOAP

web2py includes a SOAP client and server created by Mariano Reingart. It can be used very much like XML-RPC:

Consider the following code, for example, in the “default.py” controller:

  1. @service.soap('MyAdd', returns={'result':int}, args={'a':int, 'b':int, })
  2. def add(a, b):
  3. return a + b

Now in a python shell you can do:

  1. >>> from gluon.contrib.pysimplesoap.client import SoapClient
  2. >>> client = SoapClient(wsdl="http://localhost:8000/app/default/call/soap?WSDL")
  3. >>> print client.MyAdd(a=1, b=2)
  4. {'result': 3}

To get proper encoding when returning a text values, specify string as u’proper utf8 text’.

You can obtain the WSDL for the service at

  1. http://127.0.0.1:8000/app/default/call/soap?WSDL

And you can obtain documentation for any of the exposed methods:

  1. http://127.0.0.1:8000/app/default/call/soap