Test bench

SOAP server can not be considered a good one if SOAP clients don’t work with it. Therefore we need a lot of tests proving interoperability. This chapter describes a test bench used by numerous tests proving interoperability included in pyws. The next chapter Running tests explains how to run a server and all the tests. All of this can also be considered examples of pyws usage since much of pyws’s functionality is used here.

To proceed with the tests, first, ensure that pyws is installed, read more about Requirements and Installation, this tutorial is based on WSGI adapter, so check up its requirements too. You should also note that tests and examples can be found only in a source distribution archive or in your own clone of the GIT repo.

Directory structure

In pyws distribution, there is a directory called examples, let’s have a look at its contents:

examples/
    _django/                # not for now ...
    _twisted/               # not for now either ...
    _wsgi/
        __init__.py         # a simple WSGI application for testing
        Makefile            # a make file to automate running and stopping
                            # a server
    __init__.py
    api_settings.py         # common pyws server settings
    authenticate.py         # authentication related stuff
    functions.py            # example functions
    server.py               # server definition

Server & settings

First, let’s consider server.py:

import api_settings

server = SoapServer(api_settings, *api_settings.SOAP_PROTOCOL_PARAMS)

Here we create a server using class SoapServer with module object api_settings as settings and with api_settings.SOAP_PROTOCOL_PARAMS as SOAP protocol parameters.

Now, let’s turn to api_settings.py:

DEBUG = True

That’s easy, turns debug mode on, read more in DEBUG.

PROTOCOLS = (
    RestProtocol(),
    JsonProtocol(),
)

Additional protocols for the server, read more in PROTOCOLS.

SOAP_PROTOCOL_PARAMS = (
    'Test',
    'http://example.com/',
    'http://localhost:8000/api/soap',
    soap_headers_schema
)

These parameters are passed directly to SOAP protocol constructor, read more in Concerning SOAP protocol.

CREATE_CONTEXT = authenticate

The function creating a context , read more in CREATE_CONTEXT.

Authentication

In the previous paragraph, there were mentioned two variables: soap_headers_schema and authenticate, they are defined in authenticate.py, read more in Authentcation.

Functions

All the functions used in tests are defined in file functions.py. They are an example of how functions can be registered to a server and how their parameters are described, so you might want to cast a glance at it. Most use cases are described in this paragraph, but the file contains a little bit more examples.

Simple examples

from pyws.functions.register import register

@register()
@register('add_integers', return_type=int, args=((int, 0), (int, 0)))
@register('add_floats', return_type=float, args=((float, 0), (float, 0)))
def add_simple(a, b):
    return a + b

Decorators created by function register only register a function to a server without changing it at all, so we can safely use several decorators at once. As one function can work with different types of variables we can register one function with different names and argument specification.

args describes function arguments, each element of the tuple represents one argument, argument names are infered from the function. (int, 0) means that an argument has type int and that instead of None you’ll get 0. We could have just use int instead, but then we had to check argument values on not being None in function itself, which might not be convenient.

The first decorator register() will register a function with its own name and all arguments and return values will be treated as strings.

Read more in chapters Registering a function, Arguments & fields specification, Type specification.

Context example

@register('say_hello', needs_context=True)
def say_hello(context=None):
    return 'hello ' + context

This function requires a context, so we register it specifying needs_context as True. If a server fails to create a context, then this function is not called and an exception is raised instead. The context created by a server is passed to the function as keyword argument context.

Read more in chapter Context.

Date and datetime example

@register(return_type=date, args=(date, ))
@register('next_month_dt', return_type=datetime, args=(datetime, ))
def next_month(d):
    if not d:
        return None
    month = d.month + 1
    year = d.year
    if month > 12:
        month = (d.month - 1) % 12 + 1
        year += 1
    return d.replace(year=year, month=month)

Well, it looks pretty straight forward.

Read more in chapters Arguments & fields specification, Type specification.

Dict example

ABIntegerDict = {0: 'ABIntegerDict', 'a': (int, 0), 'b': (int, 0)}

Value with key 0 is the name of a dictionary type, other keys and values represent field names and their types.

ab_integer_dict_none = lambda: {'a': 0, 'b': 0}

Callables can be used as none values.

@register(
    'add_integer_dicts',
    return_type=ABIntegerDict,
    args=(
        (ABIntegerDict, ab_integer_dict_none),
        (ABIntegerDict, ab_integer_dict_none),
    ),
)
def add_dicts(p, q):
    return {
        'a': p['a'] + q['a'],
        'b': p['b'] + q['b'],
    }

List examples

StringList = [str]

@register(
    'add_string_lists',
    return_type=StringList,
    args=(StringList, StringList),
)
def add_string_lists(p, q):
    if len(p) < len(q):
        p.extend([''] * (len(q) - len(p)))
    if len(q) < len(p):
        q.extend([''] * (len(p) - len(q)))
    return list(p[i] + q[i] for i in xrange(len(p)))

List type definition is pretty simple, isn’t it?

IntegerList = [int, 0]

@register(
    'add_integer_lists',
    return_type=IntegerList,
    args=(IntegerList, IntegerList),
)
def add_integer_lists(p, q):
    if len(p) < len(q):
        p.extend([0] * (len(q) - len(p)))
    if len(q) < len(p):
        q.extend([0] * (len(p) - len(q)))
    return list(p[i] + q[i] for i in xrange(len(p)))

IntegerList‘s second element is an element none value.

Recursive type example

Tree = DictOf('Tree',
    ('value', int, 0),
)
Tree.add_fields(
    ('left', Tree),
    ('right', Tree),
)

@register(return_type=int, args=(Tree,))
def sum_tree(p):
    return p and (p['value'] +
        (p['left'] and sum_tree(p['left']) or 0) +
        (p['right'] and sum_tree(p['right']) or 0)) or 0

So far, the only way to create a recursive type is monkey pathcing.

Exception raising example

class HelloError(Exception):
    def __init__(self, name):
        super(HelloError, self).__init__(u'%s error' % name)

@register()
def raises_exception(name):
    """
    this function will always fail
    """
    raise HelloError(name)

It’s easy, just raise any exception you want.