Merge lp:~flo-fuchs/mailman/restclient into lp:mailman
- restclient
- Merge into 3.0
Status: | Merged |
---|---|
Merge reported by: | Barry Warsaw |
Merged at revision: | not available |
Proposed branch: | lp:~flo-fuchs/mailman/restclient |
Merge into: | lp:mailman |
Diff against target: |
396 lines (+385/-0) 2 files modified
src/mailman/rest/docs/restclient.txt (+124/-0) src/mailmanclient/rest.py (+261/-0) |
To merge this branch: | bzr merge lp:~flo-fuchs/mailman/restclient |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Barry Warsaw | Approve | ||
Florian Fuchs | Needs Resubmitting | ||
Mailman Coders | Pending | ||
Review via email: mp+28522@code.launchpad.net |
Commit message
Description of the change
I added a rest client in src/mailmanclient as well as a doctest in src/mailman/
Barry Warsaw (barry) wrote : | # |
- 6916. By Florian Fuchs
-
added _Domain and _List classes, fixed code style issues
Florian Fuchs (flo-fuchs) wrote : | # |
First of all: Thanks a lot for that extensive review - I really, really appreciate getting this kind of detailed comment!
Exceptions
==========
MailmanRESTClie
If there are no mailing lists yet, an empty list is returned instead of an Exception.
Proxy Objects
=============
> What do you think about having proxy objects for Domains, Lists, etc.?
> Launchpadlib works like this and it provides more of a natural, object
> oriented API to clients, rather than an XMLRPC style seen here.
Great idea! I've added two sub-classes two return as List and Domain objects like:
domain = client.
list = domain.
and so on...
No user object yet: What do you think: Are we talking about a User with n email addresses and n memberships here or more like User = Membership?
Generally I'd say "get_singularterm" and "create_
Style issues
============
I've fixed all the issues according to the style guide and pep8. At least I hope so... Lesson learned... ;-)
Func name issue
===============
> I'm not sure 'reading' a list is the right phrase here. "Reading" a list
> implies to me reading its archive. Maybe .get_list() here?
I was thinking in CRUD terms where I understand "reading" more like getting a db record. But I agree, it's a little strange here. So I changed it to get_list().
List order
==========
> This is why I added the dump_json() helper in
> src/mailman/
> appropriate for your tests because you don't explicitly pass in a url; that's
> basically embedded in .get_lists and the client. But there's enough
> commonality that dump_json() should be refactored into something that can be
> shared.
I'm not sure if a func name like dump_json is a little confusing in that context since the client doesn't return any json. Maybe we could rename the function into dump_rest_output() (or similar) and make url, method optional? So if url is set, the rest server is called; if not, only data is sorted. `data` would then either server as a parm for POST/PUT content or as a dict to sort...
For now I've added the sort logic to the test (only a couple of lines).
So, thanks again for reviewing. On to the next round...? :-)
Florian
- 6917. By Florian Fuchs
-
added some line breaks in the restclient doctest file; changed confusing function names (http helper functions) in rest client
- 6918. By Florian Fuchs
-
fixed email validation in restclient to work with email addresses containing subdomains
Barry Warsaw (barry) wrote : | # |
Thanks for all the great updates. Things look pretty good, and I have only a
few minor issues to comment on. I think we're almost ready to merge it!
Great work.
On the dump_json() issue, i just thought of something: since you're only
displaying dictionaries, perhaps pprint will do the trick. In Python 2.6,
pprint.pprint() sorts the dictionary elements I believe.
>>> import os
>>> from pprint import pprint
>>> pprint(
{'COLUMNS': '79',
...
'DISPLAY': ':0.0',
'EMACS': 't',
...
You asked:
>No user object yet: What do you think: Are we talking about a User with n
>email addresses and n memberships here or more like User = Membership?
I think we should stick fairly close to the internal model here. So 'Users'
represent people, 'Addresses' represent their email addresses, which are
usually associated with a user, and 'Member' joins an address to a mailing
list with a given role. Only indirectly can you get at the user for a member
(i.e. through its address).
I agree about singular/plural terms.
-B
=== added file 'src/mailman/
--- src/mailman/
+++ src/mailman/
> @@ -0,0 +1,129 @@
> +======
> +Mailman REST Client
> +======
> +
> + # The test framework starts out with an example domain, so let's delete
> + # that first.
> + >>> from mailman.
> + >>> from zope.component import getUtility
> + >>> domain_manager = getUtility(
> +
> + >>> domain_
> + <Domain example.com...>
> + >>> transaction.
> +
> +First let's get an instance of MailmanRESTClient.
> +
> + >>> from mailmanclient.rest import MailmanRESTClient, MailmanRESTClie
> + >>> client = MailmanRESTClie
> +
> +So far there are no lists.
> +
> + >>> lists = client.get_lists()
> + >>> print lists
> + []
I think you can just do
>>> client.get_lists()
[]
> +
> +
> +Domains
> +=======
> +
> +In order to add new lists first a new domain has to be added.
> +
> + >>> new_domain = client.
> + >>> new_domaininfo = new_domain.
> + >>> for key in sorted(
> + ... print '{0}: {1}'.format(key, new_domaininfo[
> + base_url: http://
> + ...
> +
> +Later the domain object can be instanciated using get_domain()
> +
> + >>> my_domain = client.
> +
> +
> +Mailing lists
> +=============
> +
> +Now let's add some mailing lists.
> +
> + >>> new_list = my_domain.
> +
> +Lets add another list and get some information on the list.
s/Lets/let's/
> +
> + >>> another_list = my_domain.
> + >>> another_listinfo = another_
> + >>> for key in sorted(
> + ... print '{0}: {1}'.format(key, another_
> + fqdn_listname: <email address hidden>
> + ...
> +
> +Later the new list can be instanciated using get_list():
s/i...
- 6919. By Florian Fuchs
-
changes to the restclient lib:
* use httplib2 instead of httplib
* some style, typo and best-practice fixes
* use pprint in test file
* refactored http helper methods (only one used now instead of one for each HTTP method)
* removed host and email address validation (not necessary)
* use operator.itemgetter instead of lambdas to sort dicts
Florian Fuchs (flo-fuchs) wrote : | # |
I did some fixes and improvements like suggested in the last review...
> > + entry 0:
> > + ...
> > + self_link: http://
> <email address hidden><email address hidden>
> > + entry 1:
> > + ...
> > + self_link: http://
> <email address hidden><email address hidden>
> > + entry 2:
> > + ...
> > + self_link: http://
> <email address hidden><email address hidden>
>
> The client is returning json here, right?
Nope, the client never returns json. Either HTTP status codes or lists/dicts are returned.
> Should we be using httplib2 and urllib2 here? See the implementation of
> dump_json().
Done.
> > + def _delete_
> > + """Send an HTTP DELETE request.
> > +
> > + :param path: the URL to send the DELETE request to
> > + :type path: string
> > + :return: request status code
> > + :rtype: string
> > + """
> > + try:
> > + self.c.
> > + r = self.c.
> > + return r.status
> > + finally:
> > + self.c.close()
>
> I wonder if this duplication can be refactored?
There's only one http request method now.
> > + def _validate_
> > + """Validates a domain name.
> > +
> > + :param email_host: the domain str to validate
> > + :type email_host: string
> > + """
> > + pat = re.compile(
> > + if not pat.match(
> > + raise MailmanRESTClie
> email_host)
>
> Won't the Mailman core refuse to create a domain if it's not valid? It might
> still be worth doing client-side validation, but I would expect that more in
> some webui JavaScript. What's the advantage of doing this extra check (which
> might be different than what happens in the core)?
I didn't know if the core does email validation. Also, the django app does some validation. So I removed it.
> I wonder if this method is necessary. In general, attributes are preferred
> over accessors, and you've already got a public one right here! So clients
> can do:
>
> >>> my_domain = client.
> >>> my_domain.
> ...
>
> directly. In fact, for polymorphism, maybe the attribute should just be
> called 'info'?
Done.
Barry Warsaw (barry) wrote : | # |
I finally managed to figure out how to deploy this. See http://
Preview Diff
1 | === added file 'src/mailman/rest/docs/restclient.txt' |
2 | --- src/mailman/rest/docs/restclient.txt 1970-01-01 00:00:00 +0000 |
3 | +++ src/mailman/rest/docs/restclient.txt 2010-07-20 21:14:46 +0000 |
4 | @@ -0,0 +1,124 @@ |
5 | +=================== |
6 | +Mailman REST Client |
7 | +=================== |
8 | + |
9 | + >>> from pprint import pprint |
10 | + |
11 | + # The test framework starts out with an example domain, so let's delete |
12 | + # that first. |
13 | + >>> from mailman.interfaces.domain import IDomainManager |
14 | + >>> from zope.component import getUtility |
15 | + >>> domain_manager = getUtility(IDomainManager) |
16 | + |
17 | + >>> domain_manager.remove('example.com') |
18 | + <Domain example.com...> |
19 | + >>> transaction.commit() |
20 | + |
21 | +First let's get an instance of MailmanRESTClient. |
22 | + |
23 | + >>> from mailmanclient.rest import MailmanRESTClient, MailmanRESTClientError |
24 | + >>> client = MailmanRESTClient('localhost:8001') |
25 | + |
26 | +So far there are no lists. |
27 | + |
28 | + >>> client.get_lists() |
29 | + [] |
30 | + |
31 | + |
32 | +Domains |
33 | +======= |
34 | + |
35 | +In order to add new lists first a new domain has to be added. |
36 | + |
37 | + >>> new_domain = client.create_domain('example.com') |
38 | + >>> pprint(new_domain.info) |
39 | + {u'base_url': u'http://example.com', |
40 | + u'contact_address': u'postmaster@example.com', |
41 | + u'description': None, |
42 | + u'email_host': u'example.com', |
43 | + u'http_etag': u'"6b1ccf042e8f76138a0bd37e8509f364da92a5c5"', |
44 | + u'self_link': u'http://localhost:8001/3.0/domains/example.com', |
45 | + u'url_host': u'example.com'} |
46 | + |
47 | +Later the domain object can be instantiated using get_domain() |
48 | + |
49 | + >>> my_domain = client.get_domain('example.com') |
50 | + |
51 | + |
52 | +Mailing lists |
53 | +============= |
54 | + |
55 | +Now let's add s mailing list called 'test-one'. |
56 | + |
57 | + >>> new_list = my_domain.create_list('test-one') |
58 | + |
59 | +Let's add another list and get some information on the list. |
60 | + |
61 | + >>> another_list = my_domain.create_list('test-two') |
62 | + >>> pprint(another_list.info) |
63 | + {u'fqdn_listname': u'test-two@example.com', |
64 | + u'host_name': u'example.com', |
65 | + u'http_etag': u'"a05542c9faa07cbe2b8fdf8a1655a2361ab365f2"', |
66 | + u'list_name': u'test-two', |
67 | + u'real_name': u'Test-two', |
68 | + u'self_link': u'http://localhost:8001/3.0/lists/test-two@example.com'} |
69 | + |
70 | +Later the new list can be instantiated using get_list(): |
71 | + |
72 | + >>> some_list = client.get_list('test-one@example.com') |
73 | + |
74 | +The lists have been added and get_lists() returns a list of dicts, sorted |
75 | +by fqdn_listname. |
76 | + |
77 | + >>> pprint(client.get_lists()) |
78 | + [{u'fqdn_listname': u'test-one@example.com', |
79 | + u'host_name': u'example.com', |
80 | + u'http_etag': u'"5e99519ef1b823a52254b77e89bec54fbd17bef0"', |
81 | + u'list_name': u'test-one', |
82 | + u'real_name': u'Test-one', |
83 | + u'self_link': u'http://localhost:8001/3.0/lists/test-one@example.com'}, |
84 | + {u'fqdn_listname': u'test-two@example.com', |
85 | + u'host_name': u'example.com', |
86 | + u'http_etag': u'"a05542c9faa07cbe2b8fdf8a1655a2361ab365f2"', |
87 | + u'list_name': u'test-two', |
88 | + u'real_name': u'Test-two', |
89 | + u'self_link': u'http://localhost:8001/3.0/lists/test-two@example.com'}] |
90 | + |
91 | + |
92 | +Membership |
93 | +========== |
94 | + |
95 | +Since we now have a list we should add some members to it (.subscribe() |
96 | +returns an HTTP status code, ideally 201) |
97 | + |
98 | + >>> new_list.subscribe('jack@example.com', 'Jack') |
99 | + 201 |
100 | + >>> new_list.subscribe('meg@example.com', 'Meg') |
101 | + 201 |
102 | + >>> another_list.subscribe('jack@example.com', 'Jack') |
103 | + 201 |
104 | + |
105 | +We can get a list of all members: |
106 | + |
107 | + >>> pprint(client.get_members()) |
108 | + [{u'http_etag': u'"320f9e380322cafbbf531c11eab1ec9d38b3bb99"', |
109 | + u'self_link': u'http://localhost:8001/3.0/lists/test-one@example.com/member/jack@example.com'}, |
110 | + {u'http_etag': u'"cd75b7e93216a022573534d948511edfbfea06cd"', |
111 | + u'self_link': u'http://localhost:8001/3.0/lists/test-one@example.com/member/meg@example.com'}, |
112 | + {u'http_etag': u'"13399f5ebbab8c474926a7ad0ccfda28d717e398"', |
113 | + u'self_link': u'http://localhost:8001/3.0/lists/test-two@example.com/member/jack@example.com'}] |
114 | + |
115 | +Or just the members of a specific list: |
116 | + |
117 | + >>> pprint(new_list.get_members()) |
118 | + [{u'http_etag': u'"320f9e380322cafbbf531c11eab1ec9d38b3bb99"', |
119 | + u'self_link': u'http://localhost:8001/3.0/lists/test-one@example.com/member/jack@example.com'}, |
120 | + {u'http_etag': u'"cd75b7e93216a022573534d948511edfbfea06cd"', |
121 | + u'self_link': u'http://localhost:8001/3.0/lists/test-one@example.com/member/meg@example.com'}] |
122 | + |
123 | +After a while Meg decides to unsubscribe from the mailing list (like |
124 | +.subscribe() .unsubscribe() returns an HTTP status code, ideally 200). |
125 | + |
126 | + >>> new_list.unsubscribe('meg@example.com') |
127 | + 200 |
128 | + |
129 | |
130 | === added directory 'src/mailmanclient' |
131 | === added file 'src/mailmanclient/__init__.py' |
132 | === added file 'src/mailmanclient/rest.py' |
133 | --- src/mailmanclient/rest.py 1970-01-01 00:00:00 +0000 |
134 | +++ src/mailmanclient/rest.py 2010-07-20 21:14:46 +0000 |
135 | @@ -0,0 +1,261 @@ |
136 | +# Copyright (C) 2010 by the Free Software Foundation, Inc. |
137 | +# |
138 | +# This file is part of GNU Mailman. |
139 | +# |
140 | +# GNU Mailman is free software: you can redistribute it and/or modify it under |
141 | +# the terms of the GNU General Public License as published by the Free |
142 | +# Software Foundation, either version 3 of the License, or (at your option) |
143 | +# any later version. |
144 | +# |
145 | +# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT |
146 | +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or |
147 | +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for |
148 | +# more details. |
149 | +# |
150 | +# You should have received a copy of the GNU General Public License along with |
151 | +# GNU Mailman. If not, see <http://www.gnu.org/licenses/>. |
152 | + |
153 | +"""A client library for the Mailman REST API.""" |
154 | + |
155 | + |
156 | +from __future__ import absolute_import, unicode_literals |
157 | + |
158 | +__metaclass__ = type |
159 | +__all__ = [ |
160 | + 'MailmanRESTClient', |
161 | + 'MailmanRESTClientError', |
162 | + ] |
163 | + |
164 | + |
165 | +import re |
166 | +import json |
167 | + |
168 | +from httplib2 import Http |
169 | +from operator import itemgetter |
170 | +from urllib import urlencode |
171 | +from urllib2 import HTTPError |
172 | + |
173 | + |
174 | +class MailmanRESTClientError(Exception): |
175 | + """An exception thrown by the Mailman REST API client.""" |
176 | + |
177 | + |
178 | +class MailmanRESTClient(): |
179 | + """A wrapper for the Mailman REST API.""" |
180 | + |
181 | + def __init__(self, host): |
182 | + """Check and modify the host name. |
183 | + |
184 | + :param host: the host name of the REST API |
185 | + :type host: string |
186 | + :return: a MailmanRESTClient object |
187 | + :rtype: objectFirst line should |
188 | + """ |
189 | + self.host = host |
190 | + # If there is a trailing slash remove it |
191 | + if self.host[-1] == '/': |
192 | + self.host = self.host[:-1] |
193 | + # If there is no protocol, fall back to http:// |
194 | + if self.host[0:4] != 'http': |
195 | + self.host = 'http://' + self.host |
196 | + |
197 | + def __repr__(self): |
198 | + return '<MailmanRESTClient: %s>' % self.host |
199 | + |
200 | + def _http_request(self, path, data=None, method=None): |
201 | + """Send an HTTP request. |
202 | + |
203 | + :param path: the path to send the request to |
204 | + :type path: string |
205 | + :param data: POST oder PUT data to send |
206 | + :type data: dict |
207 | + :param method: the HTTP method; defaults to GET or POST (if |
208 | + data is not None) |
209 | + :type method: string |
210 | + :return: the request content or a status code, depending on the |
211 | + method and if the request was successful |
212 | + :rtype: int, list or dict |
213 | + """ |
214 | + url = self.host + path |
215 | + # Include general header information |
216 | + headers = { |
217 | + 'User-Agent': 'MailmanRESTClient', |
218 | + 'Accept': 'text/plain', |
219 | + } |
220 | + if data is not None: |
221 | + data = urlencode(data) |
222 | + if method is None: |
223 | + if data is None: |
224 | + method = 'GET' |
225 | + else: |
226 | + method = 'POST' |
227 | + method = method.upper() |
228 | + if method == 'POST': |
229 | + headers['Content-type'] = "application/x-www-form-urlencoded" |
230 | + response, content = Http().request(url, method, data, headers) |
231 | + if method == 'GET': |
232 | + if response.status // 100 != 2: |
233 | + return response.status |
234 | + else: |
235 | + return json.loads(content) |
236 | + else: |
237 | + return response.status |
238 | + |
239 | + def create_domain(self, email_host): |
240 | + """Create a domain and return a domain object. |
241 | + |
242 | + :param email_host: The host domain to create |
243 | + :type email_host: string |
244 | + :return: A domain object or a status code (if the create |
245 | + request failed) |
246 | + :rtype int or object |
247 | + """ |
248 | + data = { |
249 | + 'email_host': email_host, |
250 | + } |
251 | + response = self._http_request('/3.0/domains', data, 'POST') |
252 | + if response == 201: |
253 | + return _Domain(self.host, email_host) |
254 | + else: |
255 | + return response |
256 | + |
257 | + def get_domain(self, email_host): |
258 | + """Return a domain object. |
259 | + |
260 | + :param email_host: host domain |
261 | + :type email_host: string |
262 | + :rtype object |
263 | + """ |
264 | + return _Domain(self.host, email_host) |
265 | + |
266 | + def get_lists(self): |
267 | + """Get a list of all mailing list. |
268 | + |
269 | + :return: a list of dicts with all mailing lists |
270 | + :rtype: list |
271 | + """ |
272 | + response = self._http_request('/3.0/lists') |
273 | + if 'entries' not in response: |
274 | + return [] |
275 | + else: |
276 | + # Return a dict with entries sorted by fqdn_listname |
277 | + return sorted(response['entries'], |
278 | + key=itemgetter('fqdn_listname')) |
279 | + |
280 | + def get_list(self, fqdn_listname): |
281 | + """Find and return a list object. |
282 | + |
283 | + :param fqdn_listname: the mailing list address |
284 | + :type fqdn_listname: string |
285 | + :rtype: object |
286 | + """ |
287 | + return _List(self.host, fqdn_listname) |
288 | + |
289 | + def get_members(self): |
290 | + """Get a list of all list members. |
291 | + |
292 | + :return: a list of dicts with the members of all lists |
293 | + :rtype: list |
294 | + """ |
295 | + response = self._http_request('/3.0/members') |
296 | + if 'entries' not in response: |
297 | + return [] |
298 | + else: |
299 | + return sorted(response['entries'], |
300 | + key=itemgetter('self_link')) |
301 | + |
302 | + |
303 | +class _Domain(MailmanRESTClient): |
304 | + """A domain wrapper for the MailmanRESTClient.""" |
305 | + |
306 | + def __init__(self, host, email_host): |
307 | + """Connect to host and get list information. |
308 | + |
309 | + :param host: the host name of the REST API |
310 | + :type host: string |
311 | + :param email_host: host domain |
312 | + :type email_host: string |
313 | + :rtype: object |
314 | + """ |
315 | + super(_Domain, self).__init__(host) |
316 | + self.info = self._http_request('/3.0/domains/' + email_host) |
317 | + |
318 | + def create_list(self, list_name): |
319 | + """Create a mailing list and return a list object. |
320 | + |
321 | + :param list_name: the name of the list to be created |
322 | + :type list_name: string |
323 | + :rtype: object |
324 | + """ |
325 | + fqdn_listname = list_name + '@' + self.info['email_host'] |
326 | + data = { |
327 | + 'fqdn_listname': fqdn_listname |
328 | + } |
329 | + response = self._http_request('/3.0/lists', data, 'POST') |
330 | + return _List(self.host, fqdn_listname) |
331 | + |
332 | + def delete_list(self, list_name): |
333 | + fqdn_listname = list_name + '@' + self.info['email_host'] |
334 | + return self._http_request('/3.0/lists/' + fqdn_listname, None, 'DELETE') |
335 | + |
336 | + |
337 | +class _List(MailmanRESTClient): |
338 | + """A mailing list wrapper for the MailmanRESTClient.""" |
339 | + |
340 | + def __init__(self, host, fqdn_listname): |
341 | + """Connect to host and get list information. |
342 | + |
343 | + :param host: the host name of the REST API |
344 | + :type host: string |
345 | + :param fqdn_listname: the mailing list address |
346 | + :type fqdn_listname: string |
347 | + :rtype: object |
348 | + """ |
349 | + super(_List, self).__init__(host) |
350 | + self.info = self._http_request('/3.0/lists/' + fqdn_listname) |
351 | + |
352 | + def subscribe(self, address, real_name=None): |
353 | + """Add an address to a list. |
354 | + |
355 | + :param address: email address to add to the list. |
356 | + :type address: string |
357 | + :param real_name: the real name of the new member |
358 | + :type real_name: string |
359 | + """ |
360 | + data = { |
361 | + 'fqdn_listname': self.info['fqdn_listname'], |
362 | + 'address': address, |
363 | + 'real_name': real_name |
364 | + } |
365 | + return self._http_request('/3.0/members', data, 'POST') |
366 | + |
367 | + def unsubscribe(self, address): |
368 | + """Unsubscribe an address to a list. |
369 | + |
370 | + :param address: email address to add to the list. |
371 | + :type address: string |
372 | + :param real_name: the real name of the new member |
373 | + :type real_name: string |
374 | + """ |
375 | + return self._http_request('/3.0/lists/' + |
376 | + self.info['fqdn_listname'] + |
377 | + '/member/' + |
378 | + address, |
379 | + None, |
380 | + 'DELETE') |
381 | + |
382 | + def get_members(self): |
383 | + """Get a list of all list members. |
384 | + |
385 | + :return: a list of dicts with all members |
386 | + :rtype: list |
387 | + """ |
388 | + response = self._http_request('/3.0/lists/' + |
389 | + self.info['fqdn_listname'] + |
390 | + '/roster/members') |
391 | + if 'entries' not in response: |
392 | + return [] |
393 | + else: |
394 | + return sorted(response['entries'], |
395 | + key=itemgetter('self_link')) |
396 | + |
Thanks for getting this client library started, Florian! Here are some
thoughts on your merge proposal.
As we discussed on IRC, I now think the best place for this code is in
src/mailmanclient. Don't worry about that though; I'll fix that up when I
merge to trunk. I know for now it's difficult getting the test suite to run
with that layout, so I'll take that on. I don't think it will be too
difficult, but there will also be some other useful helpers to share.
Omitting the non-controversial stuff.
=== added file 'src/mailman/ rest/docs/ restclient. txt' rest/docs/ restclient. txt 1970-01-01 00:00:00 +0000 rest/docs/ restclient. txt 2010-06-25 16:50:42 +0000 ======= ====== ======= ====== interfaces. domain import IDomainManager IDomainManager) manager. remove( 'example. com') commit( )
--- src/mailman/
+++ src/mailman/
> @@ -0,0 +1,89 @@
> +======
> +Mailman REST Client
> +======
> +
> +Domains
> +=======
> +
> + # The test framework starts out with an example domain, so let's delete
> + # that first.
> + >>> from mailman.
> + >>> from zope.component import getUtility
> + >>> domain_manager = getUtility(
> +
> + >>> domain_
> + <Domain example.com...>
> + >>> transaction.
The test infrastructure currently sets up this sample domain. I wonder
whether the REST client tests will more often want to start from a clean
slate, or want this example domain. I think perhaps the former, so I should
probably add a layer that is just like the ConfigLayer, but has an empty
testSetUp(). (You don't have to worry about that for now. :)
> +In order to add new lists first a new domain has to be added. ntError nt('localhost: 8001') domain( 'example. com')
> +
> + >>> from mailmanclient.rest import MailmanRESTClient, MailmanRESTClie
> + >>> c = MailmanRESTClie
> + >>> c.create_
> + True
What do you think about having proxy objects for Domains, Lists, etc.?
Launchpadlib works like this and it provides more of a natural, object
oriented API to clients, rather than an XMLRPC style seen here.
So for example, .create_domain() would return a Domain surrogate, and you'd
call things like .get_lists() and .create_list() on that object.
How much harder do you think it would be to do something like that, and do you
think it would be worth it?
> + ntError is raised.
> +
> +Mailing lists
> +=============
> +
> +You can get a lists of all lists by calling get_lists(). If no lists have been created yet, MailmanRESTClie
Please wrap narrative to 78 characters.
> + ntError: No mailing lists found
> + >>> lists = c.get_lists()
> + Traceback (most recent call last):
> + ...
> + MailmanRESTClie
I think it might be better to return an empty list instead of raising an
exception here. Think about application code like so:
# Display all the current lists. ntError:
try:
lists = c.get_lists()
except MailmanRESTClie
# Are we sure we got "No mailing lists found" or did some other
# generic client error occur?
lists = []
for mailing_list in lists:
# ...
The thing is, having no mailing lists isn't an error condition, so this should
probably return an empty (Python) list rather than raise an exception.
...