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 | 1 | =================== | ||
6 | 2 | Mailman REST Client | ||
7 | 3 | =================== | ||
8 | 4 | |||
9 | 5 | >>> from pprint import pprint | ||
10 | 6 | |||
11 | 7 | # The test framework starts out with an example domain, so let's delete | ||
12 | 8 | # that first. | ||
13 | 9 | >>> from mailman.interfaces.domain import IDomainManager | ||
14 | 10 | >>> from zope.component import getUtility | ||
15 | 11 | >>> domain_manager = getUtility(IDomainManager) | ||
16 | 12 | |||
17 | 13 | >>> domain_manager.remove('example.com') | ||
18 | 14 | <Domain example.com...> | ||
19 | 15 | >>> transaction.commit() | ||
20 | 16 | |||
21 | 17 | First let's get an instance of MailmanRESTClient. | ||
22 | 18 | |||
23 | 19 | >>> from mailmanclient.rest import MailmanRESTClient, MailmanRESTClientError | ||
24 | 20 | >>> client = MailmanRESTClient('localhost:8001') | ||
25 | 21 | |||
26 | 22 | So far there are no lists. | ||
27 | 23 | |||
28 | 24 | >>> client.get_lists() | ||
29 | 25 | [] | ||
30 | 26 | |||
31 | 27 | |||
32 | 28 | Domains | ||
33 | 29 | ======= | ||
34 | 30 | |||
35 | 31 | In order to add new lists first a new domain has to be added. | ||
36 | 32 | |||
37 | 33 | >>> new_domain = client.create_domain('example.com') | ||
38 | 34 | >>> pprint(new_domain.info) | ||
39 | 35 | {u'base_url': u'http://example.com', | ||
40 | 36 | u'contact_address': u'postmaster@example.com', | ||
41 | 37 | u'description': None, | ||
42 | 38 | u'email_host': u'example.com', | ||
43 | 39 | u'http_etag': u'"6b1ccf042e8f76138a0bd37e8509f364da92a5c5"', | ||
44 | 40 | u'self_link': u'http://localhost:8001/3.0/domains/example.com', | ||
45 | 41 | u'url_host': u'example.com'} | ||
46 | 42 | |||
47 | 43 | Later the domain object can be instantiated using get_domain() | ||
48 | 44 | |||
49 | 45 | >>> my_domain = client.get_domain('example.com') | ||
50 | 46 | |||
51 | 47 | |||
52 | 48 | Mailing lists | ||
53 | 49 | ============= | ||
54 | 50 | |||
55 | 51 | Now let's add s mailing list called 'test-one'. | ||
56 | 52 | |||
57 | 53 | >>> new_list = my_domain.create_list('test-one') | ||
58 | 54 | |||
59 | 55 | Let's add another list and get some information on the list. | ||
60 | 56 | |||
61 | 57 | >>> another_list = my_domain.create_list('test-two') | ||
62 | 58 | >>> pprint(another_list.info) | ||
63 | 59 | {u'fqdn_listname': u'test-two@example.com', | ||
64 | 60 | u'host_name': u'example.com', | ||
65 | 61 | u'http_etag': u'"a05542c9faa07cbe2b8fdf8a1655a2361ab365f2"', | ||
66 | 62 | u'list_name': u'test-two', | ||
67 | 63 | u'real_name': u'Test-two', | ||
68 | 64 | u'self_link': u'http://localhost:8001/3.0/lists/test-two@example.com'} | ||
69 | 65 | |||
70 | 66 | Later the new list can be instantiated using get_list(): | ||
71 | 67 | |||
72 | 68 | >>> some_list = client.get_list('test-one@example.com') | ||
73 | 69 | |||
74 | 70 | The lists have been added and get_lists() returns a list of dicts, sorted | ||
75 | 71 | by fqdn_listname. | ||
76 | 72 | |||
77 | 73 | >>> pprint(client.get_lists()) | ||
78 | 74 | [{u'fqdn_listname': u'test-one@example.com', | ||
79 | 75 | u'host_name': u'example.com', | ||
80 | 76 | u'http_etag': u'"5e99519ef1b823a52254b77e89bec54fbd17bef0"', | ||
81 | 77 | u'list_name': u'test-one', | ||
82 | 78 | u'real_name': u'Test-one', | ||
83 | 79 | u'self_link': u'http://localhost:8001/3.0/lists/test-one@example.com'}, | ||
84 | 80 | {u'fqdn_listname': u'test-two@example.com', | ||
85 | 81 | u'host_name': u'example.com', | ||
86 | 82 | u'http_etag': u'"a05542c9faa07cbe2b8fdf8a1655a2361ab365f2"', | ||
87 | 83 | u'list_name': u'test-two', | ||
88 | 84 | u'real_name': u'Test-two', | ||
89 | 85 | u'self_link': u'http://localhost:8001/3.0/lists/test-two@example.com'}] | ||
90 | 86 | |||
91 | 87 | |||
92 | 88 | Membership | ||
93 | 89 | ========== | ||
94 | 90 | |||
95 | 91 | Since we now have a list we should add some members to it (.subscribe() | ||
96 | 92 | returns an HTTP status code, ideally 201) | ||
97 | 93 | |||
98 | 94 | >>> new_list.subscribe('jack@example.com', 'Jack') | ||
99 | 95 | 201 | ||
100 | 96 | >>> new_list.subscribe('meg@example.com', 'Meg') | ||
101 | 97 | 201 | ||
102 | 98 | >>> another_list.subscribe('jack@example.com', 'Jack') | ||
103 | 99 | 201 | ||
104 | 100 | |||
105 | 101 | We can get a list of all members: | ||
106 | 102 | |||
107 | 103 | >>> pprint(client.get_members()) | ||
108 | 104 | [{u'http_etag': u'"320f9e380322cafbbf531c11eab1ec9d38b3bb99"', | ||
109 | 105 | u'self_link': u'http://localhost:8001/3.0/lists/test-one@example.com/member/jack@example.com'}, | ||
110 | 106 | {u'http_etag': u'"cd75b7e93216a022573534d948511edfbfea06cd"', | ||
111 | 107 | u'self_link': u'http://localhost:8001/3.0/lists/test-one@example.com/member/meg@example.com'}, | ||
112 | 108 | {u'http_etag': u'"13399f5ebbab8c474926a7ad0ccfda28d717e398"', | ||
113 | 109 | u'self_link': u'http://localhost:8001/3.0/lists/test-two@example.com/member/jack@example.com'}] | ||
114 | 110 | |||
115 | 111 | Or just the members of a specific list: | ||
116 | 112 | |||
117 | 113 | >>> pprint(new_list.get_members()) | ||
118 | 114 | [{u'http_etag': u'"320f9e380322cafbbf531c11eab1ec9d38b3bb99"', | ||
119 | 115 | u'self_link': u'http://localhost:8001/3.0/lists/test-one@example.com/member/jack@example.com'}, | ||
120 | 116 | {u'http_etag': u'"cd75b7e93216a022573534d948511edfbfea06cd"', | ||
121 | 117 | u'self_link': u'http://localhost:8001/3.0/lists/test-one@example.com/member/meg@example.com'}] | ||
122 | 118 | |||
123 | 119 | After a while Meg decides to unsubscribe from the mailing list (like | ||
124 | 120 | .subscribe() .unsubscribe() returns an HTTP status code, ideally 200). | ||
125 | 121 | |||
126 | 122 | >>> new_list.unsubscribe('meg@example.com') | ||
127 | 123 | 200 | ||
128 | 124 | |||
129 | 0 | 125 | ||
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 | 1 | # Copyright (C) 2010 by the Free Software Foundation, Inc. | ||
137 | 2 | # | ||
138 | 3 | # This file is part of GNU Mailman. | ||
139 | 4 | # | ||
140 | 5 | # GNU Mailman is free software: you can redistribute it and/or modify it under | ||
141 | 6 | # the terms of the GNU General Public License as published by the Free | ||
142 | 7 | # Software Foundation, either version 3 of the License, or (at your option) | ||
143 | 8 | # any later version. | ||
144 | 9 | # | ||
145 | 10 | # GNU Mailman is distributed in the hope that it will be useful, but WITHOUT | ||
146 | 11 | # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or | ||
147 | 12 | # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for | ||
148 | 13 | # more details. | ||
149 | 14 | # | ||
150 | 15 | # You should have received a copy of the GNU General Public License along with | ||
151 | 16 | # GNU Mailman. If not, see <http://www.gnu.org/licenses/>. | ||
152 | 17 | |||
153 | 18 | """A client library for the Mailman REST API.""" | ||
154 | 19 | |||
155 | 20 | |||
156 | 21 | from __future__ import absolute_import, unicode_literals | ||
157 | 22 | |||
158 | 23 | __metaclass__ = type | ||
159 | 24 | __all__ = [ | ||
160 | 25 | 'MailmanRESTClient', | ||
161 | 26 | 'MailmanRESTClientError', | ||
162 | 27 | ] | ||
163 | 28 | |||
164 | 29 | |||
165 | 30 | import re | ||
166 | 31 | import json | ||
167 | 32 | |||
168 | 33 | from httplib2 import Http | ||
169 | 34 | from operator import itemgetter | ||
170 | 35 | from urllib import urlencode | ||
171 | 36 | from urllib2 import HTTPError | ||
172 | 37 | |||
173 | 38 | |||
174 | 39 | class MailmanRESTClientError(Exception): | ||
175 | 40 | """An exception thrown by the Mailman REST API client.""" | ||
176 | 41 | |||
177 | 42 | |||
178 | 43 | class MailmanRESTClient(): | ||
179 | 44 | """A wrapper for the Mailman REST API.""" | ||
180 | 45 | |||
181 | 46 | def __init__(self, host): | ||
182 | 47 | """Check and modify the host name. | ||
183 | 48 | |||
184 | 49 | :param host: the host name of the REST API | ||
185 | 50 | :type host: string | ||
186 | 51 | :return: a MailmanRESTClient object | ||
187 | 52 | :rtype: objectFirst line should | ||
188 | 53 | """ | ||
189 | 54 | self.host = host | ||
190 | 55 | # If there is a trailing slash remove it | ||
191 | 56 | if self.host[-1] == '/': | ||
192 | 57 | self.host = self.host[:-1] | ||
193 | 58 | # If there is no protocol, fall back to http:// | ||
194 | 59 | if self.host[0:4] != 'http': | ||
195 | 60 | self.host = 'http://' + self.host | ||
196 | 61 | |||
197 | 62 | def __repr__(self): | ||
198 | 63 | return '<MailmanRESTClient: %s>' % self.host | ||
199 | 64 | |||
200 | 65 | def _http_request(self, path, data=None, method=None): | ||
201 | 66 | """Send an HTTP request. | ||
202 | 67 | |||
203 | 68 | :param path: the path to send the request to | ||
204 | 69 | :type path: string | ||
205 | 70 | :param data: POST oder PUT data to send | ||
206 | 71 | :type data: dict | ||
207 | 72 | :param method: the HTTP method; defaults to GET or POST (if | ||
208 | 73 | data is not None) | ||
209 | 74 | :type method: string | ||
210 | 75 | :return: the request content or a status code, depending on the | ||
211 | 76 | method and if the request was successful | ||
212 | 77 | :rtype: int, list or dict | ||
213 | 78 | """ | ||
214 | 79 | url = self.host + path | ||
215 | 80 | # Include general header information | ||
216 | 81 | headers = { | ||
217 | 82 | 'User-Agent': 'MailmanRESTClient', | ||
218 | 83 | 'Accept': 'text/plain', | ||
219 | 84 | } | ||
220 | 85 | if data is not None: | ||
221 | 86 | data = urlencode(data) | ||
222 | 87 | if method is None: | ||
223 | 88 | if data is None: | ||
224 | 89 | method = 'GET' | ||
225 | 90 | else: | ||
226 | 91 | method = 'POST' | ||
227 | 92 | method = method.upper() | ||
228 | 93 | if method == 'POST': | ||
229 | 94 | headers['Content-type'] = "application/x-www-form-urlencoded" | ||
230 | 95 | response, content = Http().request(url, method, data, headers) | ||
231 | 96 | if method == 'GET': | ||
232 | 97 | if response.status // 100 != 2: | ||
233 | 98 | return response.status | ||
234 | 99 | else: | ||
235 | 100 | return json.loads(content) | ||
236 | 101 | else: | ||
237 | 102 | return response.status | ||
238 | 103 | |||
239 | 104 | def create_domain(self, email_host): | ||
240 | 105 | """Create a domain and return a domain object. | ||
241 | 106 | |||
242 | 107 | :param email_host: The host domain to create | ||
243 | 108 | :type email_host: string | ||
244 | 109 | :return: A domain object or a status code (if the create | ||
245 | 110 | request failed) | ||
246 | 111 | :rtype int or object | ||
247 | 112 | """ | ||
248 | 113 | data = { | ||
249 | 114 | 'email_host': email_host, | ||
250 | 115 | } | ||
251 | 116 | response = self._http_request('/3.0/domains', data, 'POST') | ||
252 | 117 | if response == 201: | ||
253 | 118 | return _Domain(self.host, email_host) | ||
254 | 119 | else: | ||
255 | 120 | return response | ||
256 | 121 | |||
257 | 122 | def get_domain(self, email_host): | ||
258 | 123 | """Return a domain object. | ||
259 | 124 | |||
260 | 125 | :param email_host: host domain | ||
261 | 126 | :type email_host: string | ||
262 | 127 | :rtype object | ||
263 | 128 | """ | ||
264 | 129 | return _Domain(self.host, email_host) | ||
265 | 130 | |||
266 | 131 | def get_lists(self): | ||
267 | 132 | """Get a list of all mailing list. | ||
268 | 133 | |||
269 | 134 | :return: a list of dicts with all mailing lists | ||
270 | 135 | :rtype: list | ||
271 | 136 | """ | ||
272 | 137 | response = self._http_request('/3.0/lists') | ||
273 | 138 | if 'entries' not in response: | ||
274 | 139 | return [] | ||
275 | 140 | else: | ||
276 | 141 | # Return a dict with entries sorted by fqdn_listname | ||
277 | 142 | return sorted(response['entries'], | ||
278 | 143 | key=itemgetter('fqdn_listname')) | ||
279 | 144 | |||
280 | 145 | def get_list(self, fqdn_listname): | ||
281 | 146 | """Find and return a list object. | ||
282 | 147 | |||
283 | 148 | :param fqdn_listname: the mailing list address | ||
284 | 149 | :type fqdn_listname: string | ||
285 | 150 | :rtype: object | ||
286 | 151 | """ | ||
287 | 152 | return _List(self.host, fqdn_listname) | ||
288 | 153 | |||
289 | 154 | def get_members(self): | ||
290 | 155 | """Get a list of all list members. | ||
291 | 156 | |||
292 | 157 | :return: a list of dicts with the members of all lists | ||
293 | 158 | :rtype: list | ||
294 | 159 | """ | ||
295 | 160 | response = self._http_request('/3.0/members') | ||
296 | 161 | if 'entries' not in response: | ||
297 | 162 | return [] | ||
298 | 163 | else: | ||
299 | 164 | return sorted(response['entries'], | ||
300 | 165 | key=itemgetter('self_link')) | ||
301 | 166 | |||
302 | 167 | |||
303 | 168 | class _Domain(MailmanRESTClient): | ||
304 | 169 | """A domain wrapper for the MailmanRESTClient.""" | ||
305 | 170 | |||
306 | 171 | def __init__(self, host, email_host): | ||
307 | 172 | """Connect to host and get list information. | ||
308 | 173 | |||
309 | 174 | :param host: the host name of the REST API | ||
310 | 175 | :type host: string | ||
311 | 176 | :param email_host: host domain | ||
312 | 177 | :type email_host: string | ||
313 | 178 | :rtype: object | ||
314 | 179 | """ | ||
315 | 180 | super(_Domain, self).__init__(host) | ||
316 | 181 | self.info = self._http_request('/3.0/domains/' + email_host) | ||
317 | 182 | |||
318 | 183 | def create_list(self, list_name): | ||
319 | 184 | """Create a mailing list and return a list object. | ||
320 | 185 | |||
321 | 186 | :param list_name: the name of the list to be created | ||
322 | 187 | :type list_name: string | ||
323 | 188 | :rtype: object | ||
324 | 189 | """ | ||
325 | 190 | fqdn_listname = list_name + '@' + self.info['email_host'] | ||
326 | 191 | data = { | ||
327 | 192 | 'fqdn_listname': fqdn_listname | ||
328 | 193 | } | ||
329 | 194 | response = self._http_request('/3.0/lists', data, 'POST') | ||
330 | 195 | return _List(self.host, fqdn_listname) | ||
331 | 196 | |||
332 | 197 | def delete_list(self, list_name): | ||
333 | 198 | fqdn_listname = list_name + '@' + self.info['email_host'] | ||
334 | 199 | return self._http_request('/3.0/lists/' + fqdn_listname, None, 'DELETE') | ||
335 | 200 | |||
336 | 201 | |||
337 | 202 | class _List(MailmanRESTClient): | ||
338 | 203 | """A mailing list wrapper for the MailmanRESTClient.""" | ||
339 | 204 | |||
340 | 205 | def __init__(self, host, fqdn_listname): | ||
341 | 206 | """Connect to host and get list information. | ||
342 | 207 | |||
343 | 208 | :param host: the host name of the REST API | ||
344 | 209 | :type host: string | ||
345 | 210 | :param fqdn_listname: the mailing list address | ||
346 | 211 | :type fqdn_listname: string | ||
347 | 212 | :rtype: object | ||
348 | 213 | """ | ||
349 | 214 | super(_List, self).__init__(host) | ||
350 | 215 | self.info = self._http_request('/3.0/lists/' + fqdn_listname) | ||
351 | 216 | |||
352 | 217 | def subscribe(self, address, real_name=None): | ||
353 | 218 | """Add an address to a list. | ||
354 | 219 | |||
355 | 220 | :param address: email address to add to the list. | ||
356 | 221 | :type address: string | ||
357 | 222 | :param real_name: the real name of the new member | ||
358 | 223 | :type real_name: string | ||
359 | 224 | """ | ||
360 | 225 | data = { | ||
361 | 226 | 'fqdn_listname': self.info['fqdn_listname'], | ||
362 | 227 | 'address': address, | ||
363 | 228 | 'real_name': real_name | ||
364 | 229 | } | ||
365 | 230 | return self._http_request('/3.0/members', data, 'POST') | ||
366 | 231 | |||
367 | 232 | def unsubscribe(self, address): | ||
368 | 233 | """Unsubscribe an address to a list. | ||
369 | 234 | |||
370 | 235 | :param address: email address to add to the list. | ||
371 | 236 | :type address: string | ||
372 | 237 | :param real_name: the real name of the new member | ||
373 | 238 | :type real_name: string | ||
374 | 239 | """ | ||
375 | 240 | return self._http_request('/3.0/lists/' + | ||
376 | 241 | self.info['fqdn_listname'] + | ||
377 | 242 | '/member/' + | ||
378 | 243 | address, | ||
379 | 244 | None, | ||
380 | 245 | 'DELETE') | ||
381 | 246 | |||
382 | 247 | def get_members(self): | ||
383 | 248 | """Get a list of all list members. | ||
384 | 249 | |||
385 | 250 | :return: a list of dicts with all members | ||
386 | 251 | :rtype: list | ||
387 | 252 | """ | ||
388 | 253 | response = self._http_request('/3.0/lists/' + | ||
389 | 254 | self.info['fqdn_listname'] + | ||
390 | 255 | '/roster/members') | ||
391 | 256 | if 'entries' not in response: | ||
392 | 257 | return [] | ||
393 | 258 | else: | ||
394 | 259 | return sorted(response['entries'], | ||
395 | 260 | key=itemgetter('self_link')) | ||
396 | 261 |
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.
...