
florianfuchs has proposed merging lp:~flo-fuchs/mailman/restclient into lp:mailman. Requested reviews: Mailman Coders (mailman-coders) I added a rest client in src/mailmanclient as well as a doctest in src/mailman/rest/docs/restclient.txt. -- https://code.launchpad.net/~flo-fuchs/mailman/restclient/+merge/28522 Your team Mailman Coders is requested to review the proposed merge of lp:~flo-fuchs/mailman/restclient into lp:mailman.

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' --- src/mailman/rest/docs/restclient.txt 1970-01-01 00:00:00 +0000 +++ src/mailman/rest/docs/restclient.txt 2010-06-25 16:50:42 +0000
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. :)
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?
Please wrap narrative to 78 characters.
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. try: lists = c.get_lists() except MailmanRESTClientError: # 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.
Unfortunately, this is too fragile because it depends on dictionary sort order, which isn't defined in Python, and has definitely changed between Python versions! This is why I added the dump_json() helper in src/mailman/tests/test_documentation.py. That helper isn't entirely 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. Do you want to take a crack at that?
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?
Here's a place where a more object-oriented client might work better. For example, if we had both a List and User object, then depending on which one you had, you could do a simpler subscription. E.g. if .read_list/.get_list returned a List object, you'd do something like this: >>> test_one = c.get_list('test-one@example.com') >>> member = test_one.subscribe('aperson@example.com', 'Garp') Conversely, if you had a User object, you could do: >>> aperson = c.get_user('aperson@example.com') >>> aperson.subscribe('test-one@example.com')
Here's that dict ordering problem again.
The same API principles apply here too. === added directory 'src/mailmanclient' === added file 'src/mailmanclient/__init__.py' === added file 'src/mailmanclient/rest.py' --- src/mailmanclient/rest.py 1970-01-01 00:00:00 +0000 +++ src/mailmanclient/rest.py 2010-06-25 16:50:42 +0000
I hate to be so nitpicky on style, but I'm gonna anyway. :) A few things will help consistency with other code, so I'll be kind of anal about it for now. template.py in the top level directory is a good place to start with a new Python file. It contains the license header, a template module docstring, and some important __future__ imports. It also forces all unadorned classes to be new style (through the __metaclass__ assignment), and adds an empty __all__ for publicly exported symbols.
See src/mailman/docs/STYLEGUIDE.txt -----snip snip----- import json from httplib import HTTPConnection, HTTPException from urllib import urlencode -----snip snip----- (two blank lines between top level constructs)
(blank line needed)
This should be a complete sentence, e.g.: # Include general header information.
I usually prefer single quotes when possible (one less shift key to hold down :), and the closing brace should be indented 4 spaces.
The bare except is problematic; it's generally not a good idea to use them. In this case, it's probably better to just let the HTTPConnection exception flow up uncaught.
Similarly here. I don't think we need to turn all exceptions into MailmanRESTClientErrors. Let's reserve that exception hierarchy for exceptions specifically in our library.
You're actually not allowed to raise None (I had to try it myself to see :). This might be another case of just letting the original exception percolate up:
I'm a little less sure about this one, but that probably calls for just letting the json.loads() fail until there's an overriding reason to mutate it.
Again, probably don't need to try/except this.
Interesting. Why not always just return the status code? It seems like it would be better to always return an integer and let the client decide what to do with the status code, rather than transform just 201 into True.
Similar.
You can use an XXX comment like so: """XXX 2010-06-29 florian: to be implemented.""" That slightly suggestive XXX is an old Guido tradition, but there are lots of good tools that search for them and remind you about them. E.g. pylint. Including the date and your Launchpad id is a good idea to identify who wrote the comment and how old it is, without having to reconstruct it from the vcs history.
You can use epydoc rest markup for describing the parameters and return values for a method. http://epydoc.sourceforge.net/manual-fields.html#epydoc-fields There are lots of examples sprinkled about in the current Mailman code.
Yep, like that!
This needs to be line wrapped to 78 characters. You can indent continuation lines.
Dedent 4 spaces and end in a comma.
+ }
Indent 4 spaces.
Similar, no need to catch this exception.
Dedent all lines 4 spaces and end the last one in a comma.
+ }
Indent 4 spaces.
Don't catch.
Line length. You can probably just write this like: return self._delete_url( '/3.0/lists/' + fqdn_listname + '/member/' + address) (Note, no space before the close paren.)
No spaces around the = sign.
Maybe just return the empty list here?
And here, an empty list would be fine.
Similar comments here. This is a great first draft. Please let me know if any of my comments don't make sense or you have questions about them. Thanks! -Barry -- https://code.launchpad.net/~flo-fuchs/mailman/restclient/+merge/28522 Your team Mailman Coders is requested to review the proposed merge of lp:~flo-fuchs/mailman/restclient into lp:mailman.

First of all: Thanks a lot for that extensive review - I really, really appreciate getting this kind of detailed comment! Exceptions ========== MailmanRESTClientError is only used for email- and domain-validation. Apart from that the original Exceptions don't get catched any more. If there are no mailing lists yet, an empty list is returned instead of an Exception. Proxy Objects =============
Great idea! I've added two sub-classes two return as List and Domain objects like: domain = client.get_domain('example.com') list = domain.create_list('test') 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_singularterm" (get_list(), create_domain() etc.) should return an object with helpful methods and "get_pluralterm"/"create_pluralterm" (get_lists, get_members) should return lists oder dicts... 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 ==========
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 -- https://code.launchpad.net/~flo-fuchs/mailman/restclient/+merge/28522 Your team Mailman Coders is requested to review the proposed merge of lp:~flo-fuchs/mailman/restclient into lp:mailman.

Review: Needs Fixing 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(dict(os.environ)) {'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/rest/docs/restclient.txt' --- src/mailman/rest/docs/restclient.txt 1970-01-01 00:00:00 +0000 +++ src/mailman/rest/docs/restclient.txt 2010-07-15 14:02:55 +0000
I think you can just do >>> client.get_lists() []
s/Lets/let's/
s/instanciated/instantiated/
The other thing to think about is, if you repeat similar code in the doctest, you can always refactor them into a function defined in the doctest.
The client is returning json here, right?
=== added directory 'src/mailmanclient' === added file 'src/mailmanclient/__init__.py' === added file 'src/mailmanclient/rest.py' --- src/mailmanclient/rest.py 1970-01-01 00:00:00 +0000 +++ src/mailmanclient/rest.py 2010-07-15 14:02:55 +0000
Believe it or not, these lines should be reversed. I know it's weird but non from-import-* imports get sorted in increasing length. Just a little quirk I inherited from Guido. :) import re import json
Should we be using httplib2 and urllib2 here? See the implementation of dump_json().
You don't need that extra blank line. Strictly speaking, you don't need the following 'pass' line either!
The base class isn't necessary here, because the '__metaclass__ = type' at the top of the module already makes all unadorned classes default to new style.
First line should end in a period, and there should be a blank line following. Also, I try to make :param: descriptions full sentences too (capitalized and ending in a period). Not the :type: description though. Apply these rules to all the following docstrings.
Abbreviations == bad! :) Well, usually. ;) You can use self.connection or self.conn here to make it a little more readable. OTOH, in dump_json(), we create a new HTTP() instance for every request. I wonder if that wouldn't be a better way to go. IOW, what's the advantage of caching this HTTPConnection instance in this attribute?
Add an :return: to describe what gets returned.
I wonder if this duplication can be refactored?
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)?
Same question here.
Better to use 'response' rather than 'r'.
lambdas are kind of icky, how about: # Move this to the imports at the top of the file. from operator import itemgetter return sorted(response['entries'], key=itemgetter('fqdn_listname'))
See above.
For new-style classes: super(_Domain, self).__init__(host)
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.get_domain('example.com') >>> my_domain.domain_info ... directly. In fact, for polymorphism, maybe the attribute should just be called 'info'?
-- https://code.launchpad.net/~flo-fuchs/mailman/restclient/+merge/28522 Your team Mailman Coders is requested to review the proposed merge of lp:~flo-fuchs/mailman/restclient into lp:mailman.

Review: Resubmit I did some fixes and improvements like suggested in the last review...
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.
There's only one http request method now.
I didn't know if the core does email validation. Also, the django app does some validation. So I removed it.
Done. -- https://code.launchpad.net/~flo-fuchs/mailman/restclient/+merge/28522 Your team Mailman Coders is requested to review the proposed merge of lp:~flo-fuchs/mailman/restclient into lp:mailman.

Review: Approve I finally managed to figure out how to deploy this. See http://launchpad.net/mailman.client and the trunk branch of that. Thanks so much for the contribution! -- https://code.launchpad.net/~flo-fuchs/mailman/restclient/+merge/28522 Your team Mailman Coders is requested to review the proposed merge of lp:~flo-fuchs/mailman/restclient into lp:mailman.

The proposal to merge lp:~flo-fuchs/mailman/restclient into lp:mailman has been updated. Status: Needs review => Merged For more details, see: https://code.launchpad.net/~flo-fuchs/mailman/restclient/+merge/28522 -- https://code.launchpad.net/~flo-fuchs/mailman/restclient/+merge/28522 Your team Mailman Coders is requested to review the proposed merge of lp:~flo-fuchs/mailman/restclient into lp:mailman.

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' --- src/mailman/rest/docs/restclient.txt 1970-01-01 00:00:00 +0000 +++ src/mailman/rest/docs/restclient.txt 2010-06-25 16:50:42 +0000
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. :)
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?
Please wrap narrative to 78 characters.
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. try: lists = c.get_lists() except MailmanRESTClientError: # 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.
Unfortunately, this is too fragile because it depends on dictionary sort order, which isn't defined in Python, and has definitely changed between Python versions! This is why I added the dump_json() helper in src/mailman/tests/test_documentation.py. That helper isn't entirely 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. Do you want to take a crack at that?
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?
Here's a place where a more object-oriented client might work better. For example, if we had both a List and User object, then depending on which one you had, you could do a simpler subscription. E.g. if .read_list/.get_list returned a List object, you'd do something like this: >>> test_one = c.get_list('test-one@example.com') >>> member = test_one.subscribe('aperson@example.com', 'Garp') Conversely, if you had a User object, you could do: >>> aperson = c.get_user('aperson@example.com') >>> aperson.subscribe('test-one@example.com')
Here's that dict ordering problem again.
The same API principles apply here too. === added directory 'src/mailmanclient' === added file 'src/mailmanclient/__init__.py' === added file 'src/mailmanclient/rest.py' --- src/mailmanclient/rest.py 1970-01-01 00:00:00 +0000 +++ src/mailmanclient/rest.py 2010-06-25 16:50:42 +0000
I hate to be so nitpicky on style, but I'm gonna anyway. :) A few things will help consistency with other code, so I'll be kind of anal about it for now. template.py in the top level directory is a good place to start with a new Python file. It contains the license header, a template module docstring, and some important __future__ imports. It also forces all unadorned classes to be new style (through the __metaclass__ assignment), and adds an empty __all__ for publicly exported symbols.
See src/mailman/docs/STYLEGUIDE.txt -----snip snip----- import json from httplib import HTTPConnection, HTTPException from urllib import urlencode -----snip snip----- (two blank lines between top level constructs)
(blank line needed)
This should be a complete sentence, e.g.: # Include general header information.
I usually prefer single quotes when possible (one less shift key to hold down :), and the closing brace should be indented 4 spaces.
The bare except is problematic; it's generally not a good idea to use them. In this case, it's probably better to just let the HTTPConnection exception flow up uncaught.
Similarly here. I don't think we need to turn all exceptions into MailmanRESTClientErrors. Let's reserve that exception hierarchy for exceptions specifically in our library.
You're actually not allowed to raise None (I had to try it myself to see :). This might be another case of just letting the original exception percolate up:
I'm a little less sure about this one, but that probably calls for just letting the json.loads() fail until there's an overriding reason to mutate it.
Again, probably don't need to try/except this.
Interesting. Why not always just return the status code? It seems like it would be better to always return an integer and let the client decide what to do with the status code, rather than transform just 201 into True.
Similar.
You can use an XXX comment like so: """XXX 2010-06-29 florian: to be implemented.""" That slightly suggestive XXX is an old Guido tradition, but there are lots of good tools that search for them and remind you about them. E.g. pylint. Including the date and your Launchpad id is a good idea to identify who wrote the comment and how old it is, without having to reconstruct it from the vcs history.
You can use epydoc rest markup for describing the parameters and return values for a method. http://epydoc.sourceforge.net/manual-fields.html#epydoc-fields There are lots of examples sprinkled about in the current Mailman code.
Yep, like that!
This needs to be line wrapped to 78 characters. You can indent continuation lines.
Dedent 4 spaces and end in a comma.
+ }
Indent 4 spaces.
Similar, no need to catch this exception.
Dedent all lines 4 spaces and end the last one in a comma.
+ }
Indent 4 spaces.
Don't catch.
Line length. You can probably just write this like: return self._delete_url( '/3.0/lists/' + fqdn_listname + '/member/' + address) (Note, no space before the close paren.)
No spaces around the = sign.
Maybe just return the empty list here?
And here, an empty list would be fine.
Similar comments here. This is a great first draft. Please let me know if any of my comments don't make sense or you have questions about them. Thanks! -Barry -- https://code.launchpad.net/~flo-fuchs/mailman/restclient/+merge/28522 Your team Mailman Coders is requested to review the proposed merge of lp:~flo-fuchs/mailman/restclient into lp:mailman.

First of all: Thanks a lot for that extensive review - I really, really appreciate getting this kind of detailed comment! Exceptions ========== MailmanRESTClientError is only used for email- and domain-validation. Apart from that the original Exceptions don't get catched any more. If there are no mailing lists yet, an empty list is returned instead of an Exception. Proxy Objects =============
Great idea! I've added two sub-classes two return as List and Domain objects like: domain = client.get_domain('example.com') list = domain.create_list('test') 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_singularterm" (get_list(), create_domain() etc.) should return an object with helpful methods and "get_pluralterm"/"create_pluralterm" (get_lists, get_members) should return lists oder dicts... 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 ==========
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 -- https://code.launchpad.net/~flo-fuchs/mailman/restclient/+merge/28522 Your team Mailman Coders is requested to review the proposed merge of lp:~flo-fuchs/mailman/restclient into lp:mailman.

Review: Needs Fixing 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(dict(os.environ)) {'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/rest/docs/restclient.txt' --- src/mailman/rest/docs/restclient.txt 1970-01-01 00:00:00 +0000 +++ src/mailman/rest/docs/restclient.txt 2010-07-15 14:02:55 +0000
I think you can just do >>> client.get_lists() []
s/Lets/let's/
s/instanciated/instantiated/
The other thing to think about is, if you repeat similar code in the doctest, you can always refactor them into a function defined in the doctest.
The client is returning json here, right?
=== added directory 'src/mailmanclient' === added file 'src/mailmanclient/__init__.py' === added file 'src/mailmanclient/rest.py' --- src/mailmanclient/rest.py 1970-01-01 00:00:00 +0000 +++ src/mailmanclient/rest.py 2010-07-15 14:02:55 +0000
Believe it or not, these lines should be reversed. I know it's weird but non from-import-* imports get sorted in increasing length. Just a little quirk I inherited from Guido. :) import re import json
Should we be using httplib2 and urllib2 here? See the implementation of dump_json().
You don't need that extra blank line. Strictly speaking, you don't need the following 'pass' line either!
The base class isn't necessary here, because the '__metaclass__ = type' at the top of the module already makes all unadorned classes default to new style.
First line should end in a period, and there should be a blank line following. Also, I try to make :param: descriptions full sentences too (capitalized and ending in a period). Not the :type: description though. Apply these rules to all the following docstrings.
Abbreviations == bad! :) Well, usually. ;) You can use self.connection or self.conn here to make it a little more readable. OTOH, in dump_json(), we create a new HTTP() instance for every request. I wonder if that wouldn't be a better way to go. IOW, what's the advantage of caching this HTTPConnection instance in this attribute?
Add an :return: to describe what gets returned.
I wonder if this duplication can be refactored?
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)?
Same question here.
Better to use 'response' rather than 'r'.
lambdas are kind of icky, how about: # Move this to the imports at the top of the file. from operator import itemgetter return sorted(response['entries'], key=itemgetter('fqdn_listname'))
See above.
For new-style classes: super(_Domain, self).__init__(host)
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.get_domain('example.com') >>> my_domain.domain_info ... directly. In fact, for polymorphism, maybe the attribute should just be called 'info'?
-- https://code.launchpad.net/~flo-fuchs/mailman/restclient/+merge/28522 Your team Mailman Coders is requested to review the proposed merge of lp:~flo-fuchs/mailman/restclient into lp:mailman.

Review: Resubmit I did some fixes and improvements like suggested in the last review...
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.
There's only one http request method now.
I didn't know if the core does email validation. Also, the django app does some validation. So I removed it.
Done. -- https://code.launchpad.net/~flo-fuchs/mailman/restclient/+merge/28522 Your team Mailman Coders is requested to review the proposed merge of lp:~flo-fuchs/mailman/restclient into lp:mailman.

Review: Approve I finally managed to figure out how to deploy this. See http://launchpad.net/mailman.client and the trunk branch of that. Thanks so much for the contribution! -- https://code.launchpad.net/~flo-fuchs/mailman/restclient/+merge/28522 Your team Mailman Coders is requested to review the proposed merge of lp:~flo-fuchs/mailman/restclient into lp:mailman.

The proposal to merge lp:~flo-fuchs/mailman/restclient into lp:mailman has been updated. Status: Needs review => Merged For more details, see: https://code.launchpad.net/~flo-fuchs/mailman/restclient/+merge/28522 -- https://code.launchpad.net/~flo-fuchs/mailman/restclient/+merge/28522 Your team Mailman Coders is requested to review the proposed merge of lp:~flo-fuchs/mailman/restclient into lp:mailman.
participants (3)
-
Barry Warsaw
-
Florian Fuchs
-
florianfuchs