The W3C defines a web service as "a software system designed to support interoperable machine-to-machine interaction over a network". This is a broad definition, and it encompass a large number of protocols designed not for machine-to-human communication, but for machine-to-machine communication such as XML, JSON, RSS, etc.
web2py provides, out of the box, support for many protocols, including XML, JSON, RSS, CSV, XMLRPC, JSONRPC, AMFRPC, and SOAP. web2py can also be extended to support additional protocols.
Each of those protocols is supported in multiple ways, and we make a distinction between:
Consider the following action:
1. | def count(): |
This action returns a counter that is increased by one when a visitor reloads the page, and the timestamp of the current page request.
Normally this page would be requested via:
http://127.0.0.1:8000/app/default/count
and rendered in HTML. Without writing one line of code, we can ask web2py to render this page using a different protocols by adding an extension to the URL:
http://127.0.0.1:8000/app/default/count.html
http://127.0.0.1:8000/app/default/count.xml
http://127.0.0.1:8000/app/default/count.json
The dictionary returned by the action will be rendered in HTML, XML and JSON, respectively.
Here is the XML output:
1. | <document> |
Here is the JSON output:
1. | { 'counter':3, 'now':'2009-08-01 13:00:00' } |
Notice that date, time, and datetime objects are rendered as strings in ISO format. This is not part of the JSON standard, but rather a web2py convention.
When, for example, the ".xml" extension is called, web2py looks for a template file called "default/count.xml", and if it does not find it, looks for a template called "generic.xml". The files "generic.html, "generic.xml", "generic.json" are provided with the current scaffolding application. Other extensions can be easily defined by the user.
Nothing needs to be done to enable this in a web2py app. To use it in an older web2py app, you may need to copy the "generic.*" files from a later scaffolding app (after version 1.60).
Here is the code for "generic.html"
1. | {{extend 'layout.html'}} |
Here is the code for "generic.xml"
1. | {{ |
And here is the code for "generic.json"
1. | {{ |
Any dictionary can be rendered in HTML, XML and JSON as long as it only contains python primitive types (int, float, string, list, tuple, dictionary). response._vars contains the dictionary returned by the action.
If the dictionary contains other user-defined or web2py-specific objects, they must be rendered by a custom view.
If you need to render a set of Rows as returned by a select in XML or JSON or another format,
first transform the Rows object into a list of dictionaries using the as_list() method.
Consider for example the following mode:
1. | db.define_table('person', Field('name')) |
The following action can be rendered in HTML, but not in XML or JSON:
1. | def everybody(): |
while the following action can rendered in XML and JSON:
1. | def everybody(): |
If, for example, you want to render an action as a Python pickle:
http://127.0.0.1:8000/app/default/count.pickle
you just need to create a new view file "default/count.pickle" that contains:
1. | {{ |
If you want to be able to render any action as a pickled file, you need only to save the above file with the name "generic.pickle".
Not all objects are pickleable, and not all pickled objects can be un-pickled. It is safe to stick to primitive Python objects and combinations of them. Objects that do not contain references to file streams or database connections are usually pickleable, but they can only be un-pickled in an environment where the classes of all pickled objects are already defined.
web2py includes a "generic.rss" view that can render the dictionary returned by the action as an RSS feed.
Because the RSS feeds have a fixed structure (title, link, description, items, etc.) then for this to work, the dictionary returned by the action must have the proper structure:
1. | {'title' : ", |
and each entry in entries must have the same similar structure:
1. | {'title' : ", |
For example the following action can be rendered as an RSS feed:
1. | def feed(): |
by simply visiting the URL:
http://127.0.0.1:8000/app/default/feed.rss
Alternatively, assuming the following model:
1. | db.define_table('rss_entry', |
the following action can also be rendered as an RSS feed:
1. | def feed(): |
The as_list() method of a Rows object converts the rows into a list of dictionaries.
If additional dictionary items are found with key names not explicitly listed here, they are ignored.
Here is the "generic.rss" view provided by web2py:
1. | {{ |
As one more example of an RSS application, we consider an RSS aggregator that collects data from the "slashdot" feed and returns a new web2py rss feed.
1. | def aggregator(): |
It can be accessed at:
http://127.0.0.1:8000/app/default/aggregator.rss
The Comma Separated Values (CSV) format is a protocol to represent tabular data.
Consider the following model:
1. | db.define_model('animal', |
and the following action:
1. | def animals(): |
web2py does not provide a "generic.csv"; you must define a custom view "default/animals.csv" that serializes the animals into CSV. Here is a possible implementation:
1. | {{ |
Notice that one could also define a "generic.csv" file, but one would have to specify the name of the object to be serialized ("animals" in the example). This is why we do not provide a "generic.csv" file.
web2py provides a mechanism to turn any function into a web service. The mechanism described here differs from the mechanism described before because:
To use this feature:
First, you must import and instantiate a service object.
1. | from gluon.tools import 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(): |
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 |
As an example, consider the following decorated function:
1. | @service.run |
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:
http://127.0.0.1:8000/app/default/call/run/concat?a=hello&b=world
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:
http://127.0.0.1:8000/app/default/call/xml/concat?a=hello&b=world
http://127.0.0.1:8000/app/default/call/xml/concat/hello/world
and the output is returned as XML:
1. | <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:
http://127.0.0.1:8000/app/default/call/json/concat?a=hello&b=world
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 |
This service can be called by visiting one of the following URLs:
http://127.0.0.1:8000/app/default/call/csv/table1?a=hello&b=world
http://127.0.0.1:8000/app/default/call/csv/table1/hello/world
and it returns:
1. | hello,world |
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.
Consider the following code, for example, in the "default.py" controller:
1. | @service.xmlrpc |
Now in a python shell you can do
1. | >>> from xmlrpclib import ServerProxy |
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.
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() |
JSONRPC is very similar to XMLRPC, but uses the JSON-based protocol instead of XML to encode the data. As an example of application here, we discuss its usage 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=SQLDB('sqlite://storage.sqlite') |
Third, in "controllers/default.py", enter the following code:
1. | def index(): |
The purpose of each function should be obvious.
Fourth, in "views/default/todoApp.html", enter the following code:
1. | <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 |
Sixth, run Pyjamas before serving the application:
1. | cd /path/to/todo/static/ |
This will translate the Python code into JavaScript so that it can be executed in the browser.
To access this application, visit the URL:
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.74.
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; |
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') |
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:
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 suing 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:
http://127.0.0.1:8000/pyamf_test/default/call/amfrpc
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,}) |
Now in a python shell you can do:
1. | >>> from gluon.contrib.pysimplesoap.client import SoapClient |
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
http://127.0.0.1:8000/app/default/call/soap?WSDL
And you can obtain documentation for any of the exposed methods:
http://127.0.0.1:8000/app/default/call/soap
web2py includes gluon.contrib.simplejson, developed by Bob Ippolito. This module provides the most standard Python-JSON encoder-decoder.
SimpleJSON consists of two functions:
gluon.contrib.simplesjson.dumps(a) encodes a Python object a into JSON.gluon.contrib.simplejson.loads(b) decodes a JavaScript object b into a Python object.Object types that can be serialized include primitive types, lists, and dictionaries. Compound objects can be serialized with the exception of user defined classes.
Here is a sample action (for example in controller "default.py") that serializes the Python list containing weekdays using this low level API:
1. | def weekdays(): |
Below is a sample HTML page that sends an Ajax request to the above action, receives the JSON message and stores the list in a corresponding JavaScript variable:
1. | {{extend 'layout.html'}} |
The code uses the jQuery function $.getJSON, which performs the Ajax call and, on response, stores the weekdays names in a local JavaScript variable data and passes the variable to the callback function. In the example the callback function simply alerts the visitor that the data has been received.
Another common need of web sites is that of generating Word-readable text documents. The simplest way to do so is using the Rich Text Format (RTF) document format. This format was invented by Microsoft and it has since become a standard.
web2py includes gluon.contrib.pyrtf, developed by Simon Cusack and revised by Grant Edwards. This module allows you to generate RTF documents programmatically including colored formatted text and pictures.
In the following example we instantiate two basic RTF classes, Document and Section, append the latter to the former and insert some dummy text in the latter:
1. | def makertf(): |
In the end the Document is serialized by q.dumps(doc). Notice that before returning an RTF document it is necessary to specify the content-type in the header else the browser does not know how to handle the file.
Depending on the configuration, the browser may ask you whether to save this file or open it using a text editor.
web2py can also generate PDF documents, with an additional library called "ReportLab"73 .
If you are running web2py from source, it is sufficient to have ReportLab installed. If you are running the Windows binary distribution, you need to unzip ReportLab in the "web2py/" folder. If you are running the Mac binary distribution, you need to unzip ReportLab in the folder:
1. | web2py.app/Contents/Resources/ |
From now on we assume ReportLab is installed and that web2py can find it. We will create a simple action called "get_me_a_pdf" that generates a PDF document.
1. | from reportlab.platypus import * |
Notice how we generate the PDF into a unique temporary file, tmpfilename, we read the generated PDF from the file, then we deleted the file.
For more information about the ReportLab API, refer to the ReportLab documentation. We strongly recommend using the Platypus API of ReportLab, such as Paragraph, Spacer, etc.
In the previous chapter we have discussed the use of the following decorators:
1. | @auth.requires_login() |
For normal actions (not decorated as services), these decorators can be used even if the output is rendered in a format other than HTML.
For functions defined as services and decorated using the @service... decorators, the @auth... decorators should not be used. The two types of decorators cannot be mixed. If authentication is to be performed, it is the call actions that needs to be decorated:
1. | @auth.requires_login() |
Notice that it also possible to instantiate multiple service objects, register the same different functions with them, and expose some of them with authentication and some not:
1. | public_services=Service(globals()) |
This assumes that the caller is passing credentials in the HTTP header (a valid session cookie or using basic authentication, as discussed in the previous section). The client must support it; not all clients do.