wadllib

An Application object represents a web service described by a WADL file.

>>> import os
>>> import sys
>>> import pkg_resources
>>> from wadllib.application import Application

The first argument to the Application constructor is the URL at which the WADL file was found. The second argument may be raw WADL markup.

>>> wadl_string = pkg_resources.resource_string(
...     'wadllib.tests.data', 'launchpad-wadl.xml')
>>> wadl = Application("http://api.launchpad.dev/beta/", wadl_string)

Or the second argument may be an open filehandle containing the markup.

>>> cleanups = []
>>> def application_for(filename, url="http://www.example.com/"):
...    wadl_stream = pkg_resources.resource_stream(
...    'wadllib.tests.data', filename)
...    cleanups.append(wadl_stream)
...    return Application(url, wadl_stream)
>>> wadl = application_for("launchpad-wadl.xml",
...                        "http://api.launchpad.dev/beta/")

Creating a Resource from a representation definition

Although every representation is a representation of some HTTP resource, an HTTP resource doesn’t necessarily correspond directly to a WADL <resource> or <resource_type> tag. Sometimes a representation is defined within a WADL <method> tag.

>>> find_method = personset_resource.get_method(
...     query_params={'ws.op' : 'find'})
>>> find_method.id
'people-find'
>>> representation_definition = (
...     find_method.response.get_representation_definition(
...     'application/json'))

There may be no WADL <resource> or <resource_type> tag for the representation defined here. That’s why wadllib makes it possible to instantiate an anonymous Resource object using only the representation definition.

>>> from wadllib.application import Resource
>>> anonymous_resource = Resource(
...     wadl, "http://foo/", representation_definition.tag)

We can bind this resource to a representation, as long as we explicitly pass in the representation definition.

>>> anonymous_resource = anonymous_resource.bind(
...     get_testdata('personset'), 'application/json',
...     representation_definition=representation_definition)

Once the resource is bound to a representation, we can get its parameter values.

>>> print(anonymous_resource.get_parameter(
...     'total_size', 'application/json').get_value())
63

Resource instantiation

If you happen to have the URL to an object lying around, and you know its type, you can construct a Resource object directly instead of by following links.

>>> from wadllib.application import Resource
>>> limi_person = Resource(wadl, "http://api.launchpad.dev/beta/~limi",
...     "http://api.launchpad.dev/beta/#person")
>>> sorted([method.id for method in limi_person.method_iter])[:3]
['person-acceptInvitationToBeMemberOf', 'person-addMember', 'person-declineInvitationToBeMemberOf']
>>> bound_limi = bind_to_testdata(limi_person, 'person-limi')
>>> sorted(bound_limi.parameter_names())[:3]
['admins_collection_link', 'confirmed_email_addresses_collection_link',
 'date_created']
>>> languages_link = bound_limi.get_parameter("languages_collection_link")
>>> print(languages_link.get_value())
http://api.launchpad.dev/beta/~limi/languages

You can bind a Resource to a representation when you create it.

>>> limi_data = get_testdata('person-limi')
>>> bound_limi = Resource(
...     wadl, "http://api.launchpad.dev/beta/~limi",
...     "http://api.launchpad.dev/beta/#person", limi_data,
...     "application/json")
>>> print(bound_limi.get_parameter(
...     "languages_collection_link").get_value())
http://api.launchpad.dev/beta/~limi/languages

By default the representation is treated as a string and processed according to the media type you pass into the Resource constructor. If you’ve already processed the representation, pass in False for the ‘representation_needs_processing’ argument.

>>> from wadllib import _make_unicode
>>> processed_limi_data = json.loads(_make_unicode(limi_data))
>>> bound_limi = Resource(wadl, "http://api.launchpad.dev/beta/~limi",
...     "http://api.launchpad.dev/beta/#person", processed_limi_data,
...     "application/json", False)
>>> print(bound_limi.get_parameter(
...     "languages_collection_link").get_value())
http://api.launchpad.dev/beta/~limi/languages

Most of the time, the representation of a resource is of the type you’d get by sending a standard GET to that resource. If that’s not the case, you can specify a RepresentationDefinition as the ‘representation_definition’ argument to bind() or the Resource constructor, to show what the representation really looks like. Here’s an example.

There’s a method on a person resource such as bound_limi that’s identified by a distinctive query argument: ws.op=getMembersByStatus.

>>> method = bound_limi.get_method(
...     query_params={'ws.op' : 'findPathToTeam'})

Invoke this method with a GET request and you’ll get back a page from a list of people.

>>> people_page_repr_definition = (
...     method.response.get_representation_definition('application/json'))
>>> people_page_repr_definition.tag.attrib['href']
'http://api.launchpad.dev/beta/#person-page'

As it happens, we have a page from a list of people to use as test data.

>>> people_page_repr = get_testdata('personset')

If we bind the resource to the result of the method invocation as happened above, we don’t be able to access any of the parameters we’d expect. wadllib will think the representation is of type ‘person-full’, the default GET type for bound_limi.

>>> bad_people_page = bound_limi.bind(people_page_repr)
>>> print(bad_people_page.get_parameter('total_size'))
None

Since we don’t actually have a ‘person-full’ representation, we won’t be able to get values for the parameters of that kind of representation.

>>> bad_people_page.get_parameter('name').get_value()
Traceback (most recent call last):
...
KeyError: 'name'

So that’s a dead end. But, if we pass the correct representation type into bind(), we can access the parameters associated with a ‘person-page’ representation.

>>> people_page = bound_limi.bind(
...     people_page_repr,
...     representation_definition=people_page_repr_definition)
>>> people_page.get_parameter('total_size').get_value()
63

If you invoke the method and ask for a media type other than JSON, you won’t get anything.

>>> print(method.response.get_representation_definition('text/html'))
None

Data type conversion

The values of date and dateTime parameters are automatically converted to Python datetime objects.

>>> data_type_wadl = application_for('data-types-wadl.xml')
>>> service_root = data_type_wadl.get_resource_by_path('')
>>> representation = json.dumps(
...     {'a_date': '2007-10-20',
...      'a_datetime': '2005-06-06T08:59:51.619713+00:00'})
>>> bound_root = service_root.bind(representation, 'application/json')
>>> bound_root.get_parameter('a_date').get_value()
datetime.datetime(2007, 10, 20, 0, 0)
>>> bound_root.get_parameter('a_datetime').get_value()
datetime.datetime(2005, 6, 6, 8, ...)

A ‘date’ field can include a timestamp, and a ‘datetime’ field can omit one. wadllib will turn both into datetime objects.

>>> representation = json.dumps(
...     {'a_date': '2005-06-06T08:59:51.619713+00:00',
...      'a_datetime': '2007-10-20'})
>>> bound_root = service_root.bind(representation, 'application/json')
>>> bound_root.get_parameter('a_datetime').get_value()
datetime.datetime(2007, 10, 20, 0, 0)
>>> bound_root.get_parameter('a_date').get_value()
datetime.datetime(2005, 6, 6, 8, ...)

If a date or dateTime parameter has a null value, you get None. If the value is a string that can’t be parsed to a datetime object, you get a ValueError.

>>> representation = json.dumps(
...     {'a_date': 'foo', 'a_datetime': None})
>>> bound_root = service_root.bind(representation, 'application/json')
>>> bound_root.get_parameter('a_date').get_value()
Traceback (most recent call last):
...
ValueError: foo
>>> print(bound_root.get_parameter('a_datetime').get_value())
None

Representation creation

You must provide a representation when invoking certain methods. The representation() method helps you build one without knowing the details of how a representation is put together.

>>> create_team_method.build_representation(
...     display_name='Joe Bloggs', name='joebloggs')
('application/x-www-form-urlencoded', 'display_name=Joe+Bloggs&name=joebloggs&ws.op=newTeam')

The return value of build_representation is a 2-tuple containing the media type of the built representation, and the string representation itself. Along with the resource’s URL, this is all you need to send the representation to a web server.

>>> bound_limi.get_method('patch').build_representation(name='limi2')
('application/json', '{"name": "limi2"}')

Representations may require values for certain parameters.

>>> create_team_method.build_representation()
Traceback (most recent call last):
...
ValueError: No value for required parameter 'display_name'
>>> bound_limi.get_method('put').build_representation(name='limi2')
Traceback (most recent call last):
...
ValueError: No value for required parameter 'mugshot_link'

Some representations may safely include binary data.

>>> binary_stream = pkg_resources.resource_stream(
...     'wadllib.tests.data', 'multipart-binary-wadl.xml')
>>> cleanups.append(binary_stream)
>>> binary_wadl = Application(
...     "http://www.example.com/", binary_stream)
>>> service_root = binary_wadl.get_resource_by_path('')

Define a helper that processes the representation the same way zope.publisher would.

>>> import cgi
>>> import io
>>> def assert_message_parts(media_type, doc, expected):
...     environ = {
...         'REQUEST_METHOD': 'POST',
...         'CONTENT_TYPE': media_type,
...         'CONTENT_LENGTH': str(len(doc)),
...         }
...     kwargs = (
...         {'encoding': 'UTF-8'} if sys.version_info[0] >= 3 else {})
...     fs = cgi.FieldStorage(
...         fp=io.BytesIO(doc), environ=environ, keep_blank_values=1,
...         **kwargs)
...     values = []
...     def append_values(fields):
...         for field in fields:
...             if field.list:
...                 append_values(field.list)
...             else:
...                 values.append(field.value)
...     append_values(fs.list)
...     assert values == expected, (
...         'Expected %s, got %s' % (expected, values))
>>> method = service_root.get_method('post', 'multipart/form-data')
>>> media_type, doc = method.build_representation(
...     text_field="text", binary_field=b"\x01\x02\r\x81\r")
>>> print(media_type)
multipart/form-data; boundary=...
>>> assert_message_parts(media_type, doc, ['text', b'\x01\x02\r\x81\r'])
>>> method = service_root.get_method('post', 'multipart/form-data')
>>> media_type, doc = method.build_representation(
...     text_field=u"text", binary_field=b"\x01\x02\r\x81\r")
>>> print(media_type)
multipart/form-data; boundary=...
>>> assert_message_parts(media_type, doc, ['text', b'\x01\x02\r\x81\r'])
>>> method = service_root.get_method('post', 'multipart/form-data')
>>> media_type, doc = method.build_representation(
...     text_field="text\n", binary_field=b"\x01\x02\r\x81\n\r")
>>> print(media_type)
multipart/form-data; boundary=...
>>> assert_message_parts(
...     media_type, doc, ['text\r\n', b'\x01\x02\r\x81\n\r'])
>>> method = service_root.get_method('post', 'multipart/form-data')
>>> media_type, doc = method.build_representation(
...     text_field=u"text\n", binary_field=b"\x01\x02\r\x81\n\r")
>>> print(media_type)
multipart/form-data; boundary=...
>>> assert_message_parts(
...     media_type, doc, ['text\r\n', b'\x01\x02\r\x81\n\r'])
>>> method = service_root.get_method('post', 'multipart/form-data')
>>> media_type, doc = method.build_representation(
...     text_field="text\r\nmore\r\n",
...     binary_field=b"\x01\x02\r\n\x81\r\x82\n")
>>> print(media_type)
multipart/form-data; boundary=...
>>> assert_message_parts(
...     media_type, doc, ['text\r\nmore\r\n', b'\x01\x02\r\n\x81\r\x82\n'])
>>> method = service_root.get_method('post', 'multipart/form-data')
>>> media_type, doc = method.build_representation(
...     text_field=u"text\r\nmore\r\n",
...     binary_field=b"\x01\x02\r\n\x81\r\x82\n")
>>> print(media_type)
multipart/form-data; boundary=...
>>> assert_message_parts(
...     media_type, doc, ['text\r\nmore\r\n', b'\x01\x02\r\n\x81\r\x82\n'])
>>> method = service_root.get_method('post', 'text/unknown')
>>> method.build_representation(field="value")
Traceback (most recent call last):
...
ValueError: Unsupported media type: 'text/unknown'

Options

Some parameters take values from a predefined list of options.

>>> option_wadl = application_for('options-wadl.xml')
>>> definitions = option_wadl.representation_definitions
>>> service_root = option_wadl.get_resource_by_path('')
>>> definition = definitions['service-root-json']
>>> param = definition.params(service_root)[0]
>>> print(param.name)
has_options
>>> sorted([option.value for option in param.options])
['Value 1', 'Value 2']

Such parameters cannot take values that are not in the list.

>>> definition.validate_param_values(
...     [param], {'has_options': 'Value 1'})
{'has_options': 'Value 1'}
>>> definition.validate_param_values(
...     [param], {'has_options': 'Invalid value'})
Traceback (most recent call last):
...
ValueError: Invalid value 'Invalid value' for parameter
'has_options': valid values are: "Value 1", "Value 2"

Error conditions

You’ll get None if you try to look up a nonexistent resource.

>>> print(wadl.get_resource_by_path('nosuchresource'))
None

You’ll get an exception if you try to look up a nonexistent resource type.

>>> print(wadl.get_resource_type('#nosuchtype'))
Traceback (most recent call last):
KeyError: 'No such XML ID: "#nosuchtype"'

You’ll get None if you try to look up a method whose parameters don’t match any defined method.

>>> print(bound_limi.get_method(
...     'post', representation_params={ 'foo' : 'bar' }))
None