1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
|
================
REST API helpers
================
There are a number of helpers that make building out the REST API easier.
Resource paths
==============
For example, most resources don't have to worry about where they are rooted.
They only need to know where they are relative to the root URI, and this
function can return them the full path to the resource.
>>> from mailman.rest.helpers import path_to
>>> print(path_to('system'))
http://localhost:9001/3.0/system
Parameters like the ``scheme``, ``host``, ``port``, and API version number can
be set in the configuration file.
::
>>> config.push('helpers', """
... [webservice]
... hostname: geddy
... port: 2112
... use_https: yes
... api_version: 4.2
... """)
>>> cleanups.append((config.pop, 'helpers'))
>>> print(path_to('system'))
https://geddy:2112/4.2/system
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'])
"96e036d66248cab746b7d97047e08896fcfb2493"
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: "96e036d66248cab746b7d97047e08896fcfb2493"
neil : drums
POST and PUT unpacking
======================
Another helper unpacks ``POST`` and ``PUT`` request variables, validating and
converting their values.
::
>>> import six
>>> from mailman.rest.validator import Validator
>>> validator = Validator(one=int, two=six.text_type, 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 u'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=six.text_type, 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 u'two' True 4 5
>>> del FakeRequest.params['four']
>>> print_request(**validator(FakeRequest))
1 u'two' True None 5
>>> del FakeRequest.params['five']
>>> print_request(**validator(FakeRequest))
1 u'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]
|