REST API helpers

There are a number of helpers that make building out the REST API easier.

Etags

HTTP etags are a way for clients to decide whether their copy of a resource has changed or not. Mailman’s REST API calculates this in a cheap and dirty way. Pass in the dictionary representing the resource and that dictionary gets modified to contain the etag under the http_etag key.

>>> from mailman.rest.helpers import etag
>>> resource = dict(geddy='bass', alex='guitar', neil='drums')
>>> json_data = etag(resource)
>>> print(resource['http_etag'])
"6929ecfbda2282980a4818fb75f82e812077f77a"

For convenience, the etag function also returns the JSON representation of the dictionary after tagging, since that’s almost always what you want.

>>> import json
>>> data = json.loads(json_data)

# This is pretty close to what we want, so it's convenient to use.
>>> dump_msgdata(data)
alex     : guitar
geddy    : bass
http_etag: "6929ecfbda2282980a4818fb75f82e812077f77a"
neil     : drums

POST and PUT unpacking

Another helper unpacks POST and PUT request variables, validating and converting their values.

>>> from mailman.rest.validator import Validator
>>> validator = Validator(one=int, two=str, three=bool)

>>> class FakeRequest:
...     params = None
>>> FakeRequest.params = dict(one='1', two='two', three='yes')

On valid input, the validator can be used as a **keyword argument.

>>> def print_request(one, two, three):
...     print(repr(one), repr(two), repr(three))
>>> print_request(**validator(FakeRequest))
1 'two' True

On invalid input, an exception is raised.

>>> FakeRequest.params['one'] = 'hello'
>>> print_request(**validator(FakeRequest))
Traceback (most recent call last):
...
ValueError: Cannot convert parameters: one

On missing input, an exception is raised.

>>> del FakeRequest.params['one']
>>> print_request(**validator(FakeRequest))
Traceback (most recent call last):
...
ValueError: Missing parameters: one

If more than one key is missing, it will be reflected in the error message.

>>> del FakeRequest.params['two']
>>> print_request(**validator(FakeRequest))
Traceback (most recent call last):
...
ValueError: Missing parameters: one, two

Extra keys are also not allowed.

>>> FakeRequest.params = dict(one='1', two='two', three='yes',
...                           four='', five='')
>>> print_request(**validator(FakeRequest))
Traceback (most recent call last):
...
ValueError: Unexpected parameters: five, four

However, if optional keys are missing, it’s okay.

>>> validator = Validator(one=int, two=str, three=bool,
...                       four=int, five=int,
...                       _optional=('four', 'five'))

>>> FakeRequest.params = dict(one='1', two='two', three='yes',
...                           four='4', five='5')
>>> def print_request(one, two, three, four=None, five=None):
...     print(repr(one), repr(two), repr(three), repr(four), repr(five))
>>> print_request(**validator(FakeRequest))
1 'two' True 4 5

>>> del FakeRequest.params['four']
>>> print_request(**validator(FakeRequest))
1 'two' True None 5

>>> del FakeRequest.params['five']
>>> print_request(**validator(FakeRequest))
1 'two' True None None

But if the optional values are present, they must of course also be valid.

>>> FakeRequest.params = dict(one='1', two='two', three='yes',
...                           four='no', five='maybe')
>>> print_request(**validator(FakeRequest))
Traceback (most recent call last):
...
ValueError: Cannot convert parameters: five, four

Arrays

Some POST forms include more than one value for a particular key. This is how lists and arrays are modeled. The validator does the right thing with such form data. Specifically, when a key shows up multiple times in the form data, a list is given to the validator.

# We can't use a normal dictionary because we'll have multiple keys, but
# the validator only wants to call .items() on the object.
>>> class MultiDict:
...     def __init__(self, *params): self.values = list(params)
...     def items(self): return iter(self.values)
>>> form_data = MultiDict(
...     ('one', '1'),
...     ('many', '3'),
...     ('many', '4'),
...     ('many', '5'),
...     )

This is a validation function that ensures the value is a list.

>>> def must_be_list(value):
...     if not isinstance(value, list):
...         raise ValueError('not a list')
...     return [int(item) for item in value]

This is a validation function that ensure the value is not a list.

>>> def must_be_scalar(value):
...     if isinstance(value, list):
...         raise ValueError('is a list')
...     return int(value)

And a validator to pull it all together.

>>> validator = Validator(one=must_be_scalar, many=must_be_list)
>>> FakeRequest.params = form_data
>>> values = validator(FakeRequest)
>>> print(values['one'])
1
>>> print(values['many'])
[3, 4, 5]

The list values are guaranteed to be in the same order they show up in the form data.

>>> FakeRequest.params = MultiDict(
...     ('one', '1'),
...     ('many', '3'),
...     ('many', '5'),
...     ('many', '4'),
...     )
>>> values = validator(FakeRequest)
>>> print(values['one'])
1
>>> print(values['many'])
[3, 5, 4]