REST API helpers
There are a number of helpers that make building out the REST API easier.
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
... content_type = 'application/x-www-form-urlencoded'
>>> 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: Invalid Parameter "one": invalid literal for int() with base 10: 'hello'.
On missing input, an exception is raised.
>>> del FakeRequest.params['one']
>>> print_request(**validator(FakeRequest))
Traceback (most recent call last):
...
ValueError: Missing Parameter: 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 Parameter: 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: Invalid Parameter "five": invalid literal for int() with base 10: 'maybe'. Invalid Parameter "four": invalid literal for int() with base 10: 'no'.
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]
PATCH Unpacking
PATCH
requests are different from PUT
and POST
because with the
latter, you’re changing the entire resource, so all expected attributes must
exist. With the former, you’re only changing a subset of the attributes, so
you only validate the ones that exist in the request.
>>> from mailman.rest.validator import PatchValidator
>>> from mailman.rest.helpers import GetterSetter
>>> values = dict(one=GetterSetter(int),
... two=GetterSetter(str),
... three=GetterSetter(bool))
>>> FakeRequest.params = dict(one=1)
>>> validator = PatchValidator(FakeRequest, values)
PatchValidator
can be used to update the attributes of an object directly:
>>> class FakeObject:
... one = 2
>>> fakeobj = FakeObject()
>>> validator.update(fakeobj, FakeRequest)
>>> print(fakeobj.one)
1
JSON Unpacking
Request can optionally consist of JSON body as parameters. If the
Content-Type
header is set to application/json
, request’s body is parsed
to set request.media
as a dict object
>>> validator = Validator(one=int, two=str, three=bool)
>>> class FakeRequest:
... params = None
... content_type = 'application/json'
... media = None
>>> FakeRequest.media = 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