One of the promises of Web services is interoperability. As we explained in previous columns, Web services built with different programming languages and on different platforms should work together just as well as services provided by the same programming language. In this installment of The Python Web services developer, I will take the client and server for the calendar Web service that we implemented in previous articles in this column and see how they interoperate with each other.
To use the samples within this article, follow the installation steps for SOAP.py in the fifth installment in this series and for ZSI in the sixth. (You should review both those articles for background on SOAP.py and ZSI; you can find links to them in the Resources section.) As a side note, a newer version of ZSI has recently become available: version 1.2 was released in March of this year. The samples presented in this column do work with both ZSI 1.1 and 1.2. For those interested in upgrading, see the Resources section below for a URL where you can download the latest version. Installation uses standard distutils commands. If you have a previous version of ZSI installed, you should remove it first to avoid conflicts. To install the new version, untar the distribution and change into the ZSI-1.2 directory. Execute the install command as follows:
[molson@penny ZSI-1.2]# python setup.py install |
As the first part of the analysis, you will run the ZSI client against the SOAP.py server. The first thing that you need to do is change the ports that the client and the server communicate on. The SOAP.py example listens on port 8888, and the ZSI client listens on port 8080. Either port is fine, and you'll have all examples set to port 8888.
Initially, you will try to run the scripts exactly as they were originally presented (except for the port change, of course). Remember, to start the server, in one window, run the script soapy-server.py:
python soapy-server.py |
In a second window, run the script zsi-client.py:
python zsi-client.py |
In the client window, you should get an error message that says:
Traceback (most recent call last):
File "zsi-client.py", line 21, in ?
print b.Receive(TC.Any(aslist = b.aslist))
File "/usr/local/lib/python2.1/site-packages/ZSI/client.py", line 227, in Receive
raise TypeError, "Unexpected SOAP fault: " + msg.string
TypeError: Unexpected SOAP fault: No method getMonth found
|
Examining the debugging messages that you get using the tips shown in the previous columns (see the Resources for the fifth column on SOAP.py), you can see the problem straightaway. The namespace of the method being invoked is incorrect. The method is referenced in the SOAP body without a prefix, and there is no default namespace defined. The SOAP.py server defined getMonth in the http://uche.ogbuji.net/eg/ws/simple-cal namespace, so you need to adjust the ZSI client so it makes the request for the proper method in the proper namespace.
You cannot solve this by simply calling getMonth on the ZSI Binding object; you need to use some of the lower-level interfaces so that you can add key arguments where needed. What you need to specify is the namespace of the method being called. To do this, you need to change the name of the method being called (add a prefix to the name) and specify the prefix-to-namespace mapping in the nsdict argument. Listing 1 shows what the ZSI client looks with the changes:
Listing 1. Updated ZSI client
#zsi-client.py
import sys
#Import the ZSI client
from ZSI.client import Binding
from ZSI import TC
u = ''
n = 'http://uche.ogbuji.net/eg/ws/simple-cal'
b = Binding(url=u, ns=n, host='localhost', port=8888)
b.Send(None,
"ns1:getMonth",
(2002,2),
requesttypecode=TC.Any('ns1:getMonth', aslist=b.aslist),
nsdict={'ns1':n},
)
res = b.Receive(TC.Any(aslist = b.aslist))
print res[0]
|
With these changes, the ZSI client has no problems printing out the calendar for February of 2002 returned by the SOAP.py client. It looks like you have achieved success with the first half of the analysis. Now let's see how communication works going the other direction.
Flipping it around: ZSI server, SOAP.py client
The first thing you need to do to in order to test a SOAP.py client with the ZSI server is to write the SOAP.py client. If you remember, we wrote a very low-level client to test the SOAP.py server using httplib (see the Resources for the sixth column on ZSI). Since you want to see how SOAP.py interacts with ZSI, this will not do. A quick client can be written to use your calendar Web service, as shown in Listing 2.
Listing 2. Updated ZSI client
#!/usr/bin/env python
import sys
#Import the SOAP.py machinery
from WebServices import SOAP
CAL_NS = "http://uche.ogbuji.net/eg/ws/simple-cal"
remote = SOAP.SOAPProxy(
"localhost:8888",
#namespace=CAL_NS,
soapaction=""
)
print remote.getMonth(2002,2)
|
This client is implemented similarly to the curses client from the fifth column. One thing to note is that the namespace argument to the SOAPProxy is commented out. Just as the default implementation of the ZSI client was not namespace aware, the server is not either.
Again, to run the sample scripts, start the SOAP.py server in one window:
python zsi-server.py |
And the ZSI client in a second window:
python soappy-client.py |
As expected (since this client was written specifically to communicate with the SOAP.py server) you get back a calendar for February 2002.
As the next test, you will expand the ZSI client so that it does support methods inside of a namespace. There are some hurdles to overcome to do this; but Rich Salz, the author of the ZSI package, says that support for namespaces on methods is coming soon. He says that his intended approach is to use a new feature in Python 2.2 that allows you to define arbitrary attributes to functions. You would add a namespace attribute on a function definition, and then the ZSI-dispatching code would query for this attribute to determine which namespace the method is defined in. For future reference, you can add any attribute to a function by setting it on the function object. Below is a short example:
>>> def foo(a): ... pass ... >>> foo.namespace = "http://foo.com" >>> print foo.namespace http://foo.com >>> |
Salz also mentioned that he might add support for other attributes on functions besides namespace information, such as argument typing information. This would allow ZSI to typecheck incoming arguments before the function is called.
At any rate, to make the ZSI server namespace aware, you need to add a bit of logic to the getMonth method (and any other method that we would like to be in a namespace). The first thing that you can try is to get a ClientBinding for the current invocation. This contains all of the relevant information about the call, including its namespace. (By default, ZSI ignores the namespace.) You can then compare the namespace from the ClientBinding to the namespace that you are expecting. If there is a match, then you can return the proper results; if not, you can throw an exception to say that the method was not defined. Listing 3 shows an example of how the refactored getMonth method would look.
Listing 3. Refactored getMonth method
def getMonth(year, month):
cb = dispatch.GetClientBinding()
if cb.GetNS() != CAL_NS:
raise TypeError, "Unimplemented method %s %s" % (cb.GetNS(),name)
return calendar.month(year, month)
|
Unfortunately, to use this technique, you would need to add this logic to every function that is defined in the module. Listing 4 takes a more general approach that will allow all of the functions that you define in a module to be easily defined inside of a namespace. You can even easily define the same method in multiple namespaces, or multiple methods with different namespaces (but the same method names). To accomplish this, the function called _functionMap will generically map a request using a mapping dictionary. The dictionary is keyed off a namespace, and each entry references a function to be invoked when a request in the corresponding namespace is received. Then, to give the appearance of top-level functions in the module (as required by ZSI), you use a simple lambda that will stand in the place of the actual function definition. In the case of getMonth, the getMonth lambda is called; this calls _functionMap, which looks at the namespace of the call, as well as the mapping of functions available. If a match is found, the function is invoked; if not, an exception is raised. Now, to add new functions to the module, you would define a function and a corresponding lambda.
One thing to note: you need to put a reference to _functionMap in the defaulted arguments of each lambda definition. This is because when you are done creating the lambdas, you want to delete the module's reference to the _functionMap function; if you don't, this function would be exposed over your Web services interface, as ZSI allows any method defined at the top level to be called.
Listing 4 shows the file zsi-ns-server.py, which implements the namespace-aware server in ZSI.
Listing 4. zsi-ns-server.py: A more general approach
#!/usr/bin/env python
import sys, calendar
#Import the ZSI machinery
from ZSI import dispatch
CAL_NS = "http://uche.ogbuji.net/eg/ws/simple-cal"
#The actual implementations
def getMonth(year, month):
return calendar.month(year, month)
def getYear(year):
return calendar.calendar(year)
#Generic function to check the namespace
def _functionMap(name,mapping,*args):
cb = dispatch.GetClientBinding()
func = mapping.get(cb.GetNS())
if func is None:
raise TypeError, "Unimplemented method %s %s" % (cb.GetNS(),name)
return apply(func,args)
#Publicly defined methods
getMonthMap = {CAL_NS:getMonth,
}
getMonth = lambda year,month,_functionMap=_functionMap,map=getMonthMap:_functionMap
("getMonth",map,year,month)
getYearMap = {CAL_NS:getYear,
}
getYear = lambda year,_functionMap=_functionMap,map=getYearMap:_functionMap("getYear",map,year)
#Delete this so it is not available as a service.
del _functionMap
print "Starting server..."
dispatch.AsServer(port=8888)
|
In the next installment of this column, we will compare some different distributed programming technologies and look at some of the performance characteristics of each. We will compare SOAP, CORBA (Common Object Request Broker Architecture), XML-RPC, and a roll-your-own implementation written with the pickle library. We will look at the message overhead, application memory footprint, message transmission times, and relative source code size needed when implementing each of these methods.
- Participate in the discussion forum.
- The fifth column in this series introduces you to SOAP.py.
- The sixth column in this series focuses on ZSI.
- The most recent installment of this article took a break from coding at the 10th International Python Conference.
-
The Python Web services page at SourceForge has information on SOAP.py and links to ZSI version 1.2.
- Find out more about SOAP.py SOAP.py Home Page

Mike Olson is a consultant and co-founder of Fourthought Inc., a software vendor and consultancy specializing in XML solutions for enterprise knowledge management applications. Fourthought develops 4Suite, and 4Suite Server, open source platforms for XML middleware. You can contact Mr. Olson at mike.olson@fourthought.com.

Uche Ogbuji is a consultant and co-founder of Fourthought Inc., a software vendor and consultancy specializing in XML solutions for enterprise knowledge management applications. Fourthought develops 4Suite, and 4Suite Server, open source platforms for XML middleware. Mr. Ogbuji is a Computer Engineer and writer born in Nigeria, living and working in Boulder, Colorado, USA. You can contact Mr. Ogbuji at uche@ogbuji.net.
Comments (Undergoing maintenance)





