diff options
| author | J08nY | 2017-06-01 20:53:53 +0200 |
|---|---|---|
| committer | J08nY | 2017-08-07 17:39:07 +0200 |
| commit | a665dccf9404d6f95d8a4587f05d748b504e1f9d (patch) | |
| tree | a2bc4f3e4cbb443224d66a5cae4b78131b4e4213 | |
| parent | 1f1a35e7ccde1cfe239286a9a6333ebe8882d8f3 (diff) | |
| download | mailman-a665dccf9404d6f95d8a4587f05d748b504e1f9d.tar.gz mailman-a665dccf9404d6f95d8a4587f05d748b504e1f9d.tar.zst mailman-a665dccf9404d6f95d8a4587f05d748b504e1f9d.zip | |
| -rw-r--r-- | src/mailman/app/docs/plugins.rst | 3 | ||||
| -rw-r--r-- | src/mailman/interfaces/plugin.py | 6 | ||||
| -rw-r--r-- | src/mailman/rest/docs/plugins.rst | 59 | ||||
| -rw-r--r-- | src/mailman/rest/plugins.py | 68 | ||||
| -rw-r--r-- | src/mailman/rest/root.py | 15 | ||||
| -rw-r--r-- | src/mailman/rest/tests/test_plugins.py | 104 | ||||
| -rw-r--r-- | src/mailman/testing/plugin.py | 69 |
7 files changed, 322 insertions, 2 deletions
diff --git a/src/mailman/app/docs/plugins.rst b/src/mailman/app/docs/plugins.rst index 42038b654..a66e4413d 100644 --- a/src/mailman/app/docs/plugins.rst +++ b/src/mailman/app/docs/plugins.rst @@ -113,6 +113,9 @@ initialization process. ... global counter ... print('post-hook:', counter) ... counter += 1 + ... + ... def rest_object(self): + ... pass ... """, file=fp) >>> fp.close() diff --git a/src/mailman/interfaces/plugin.py b/src/mailman/interfaces/plugin.py index 9e226824f..f1b7212f0 100644 --- a/src/mailman/interfaces/plugin.py +++ b/src/mailman/interfaces/plugin.py @@ -30,3 +30,9 @@ class IPlugin(Interface): def post_hook(): """A plugin hook called in the second initialization step. After DB.""" + + def rest_object(): + """Return the REST object which will be hooked up to the REST api. + + This object will be served at the /<api>/plugins/<plugin.name>/ root. + """ diff --git a/src/mailman/rest/docs/plugins.rst b/src/mailman/rest/docs/plugins.rst new file mode 100644 index 000000000..787726e55 --- /dev/null +++ b/src/mailman/rest/docs/plugins.rst @@ -0,0 +1,59 @@ +========= + Plugins +========= + +Plugins can supply REST routes. +:: + + >>> dump_json('http://localhost:9001/3.1/plugins') + entry 0: + class: mailman.testing.plugin.ExamplePlugin + enable: True + http_etag: "..." + name: example + path: + http_etag: "..." + start: 0 + total_size: 1 + + >>> dump_json('http://localhost:9001/3.1/plugins/example') + example-plugin-reply: yes + http_etag: "..." + + >>> dump_json('http://localhost:9001/3.1/plugins/example/good') + good: True + http_etag: "..." + + >>> dump_json('http://localhost:9001/3.1/plugins/example/bad') + Traceback (most recent call last): + ... + urllib.error.HTTPError: HTTP Error 400: ... + +For example this route that presents a counter. +:: + + >>> dump_json('http://localhost:9001/3.1/plugins/example/count') + count: 0 + http_etag: "..." + + >>> call_http('http://localhost:9001/3.1/plugins/example/count', + ... {'count':3}, method='POST') + content-length: 0 + date: ... + server: ... + status: 204 + + >>> dump_json('http://localhost:9001/3.1/plugins/example/count') + count: 3 + http_etag: "..." + + >>> call_http('http://localhost:9001/3.1/plugins/example/count', + ... method='DELETE') + content-length: 0 + date: ... + server: ... + status: 204 + + >>> dump_json('http://localhost:9001/3.1/plugins/example/count') + count: 0 + http_etag: "..." diff --git a/src/mailman/rest/plugins.py b/src/mailman/rest/plugins.py new file mode 100644 index 000000000..ac8fc0ace --- /dev/null +++ b/src/mailman/rest/plugins.py @@ -0,0 +1,68 @@ +# Copyright (C) 2010-2017 by the Free Software Foundation, Inc. +# +# This file is part of GNU Mailman. +# +# GNU Mailman is free software: you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) +# any later version. +# +# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along with +# GNU Mailman. If not, see <http://www.gnu.org/licenses/>. + +"""REST for plugins, dynamically proxies requests to plugin's rest_object.""" + +from lazr.config import as_boolean +from mailman.config import config +from mailman.rest.helpers import CollectionMixin, NotFound, etag, okay +from public import public + + +@public +class AllPlugins(CollectionMixin): + """Read-only list of all plugin configs.""" + + def _resource_as_dict(self, plugin_config): + """See `CollectionMixin`.""" + resource = { + 'name': plugin_config.name.split('.')[-1], + 'class': plugin_config['class'], + 'enable': as_boolean(plugin_config['enable']), + 'path': plugin_config['path'] + } + return resource + + def _get_collection(self, request): + """See `CollectionMixin`.""" + return config.plugin_configs + + def on_get(self, request, response): + """/plugins""" + resource = self._make_collection(request) + okay(response, etag(resource)) + + +@public +class APlugin: + """REST proxy to the plugins rest_object.""" + + def __init__(self, plugin_name): + self.__rest_object = None + if plugin_name in config.plugins.keys(): + plugin = config.plugins[plugin_name] + self.__rest_object = plugin.rest_object() + # If the plugin doesn't exist or doesn't provide a rest_object, + # just proxy to NotFound + if self.__rest_object is None: + self.__rest_object = NotFound() + + def __getattr__(self, attrib): + return getattr(self.__rest_object, attrib) + + def __dir__(self): + return dir(self.__rest_object) diff --git a/src/mailman/rest/root.py b/src/mailman/rest/root.py index 76289078b..46116aba6 100644 --- a/src/mailman/rest/root.py +++ b/src/mailman/rest/root.py @@ -30,6 +30,7 @@ from mailman.rest.helpers import ( BadRequest, NotFound, child, etag, no_content, not_found, okay) from mailman.rest.lists import AList, AllLists, Styles from mailman.rest.members import AMember, AllMembers, FindMembers +from mailman.rest.plugins import APlugin, AllPlugins from mailman.rest.preferences import ReadOnlyPreferences from mailman.rest.queues import AQueue, AQueueFile, AllQueues from mailman.rest.templates import TemplateFinder @@ -308,6 +309,20 @@ class TopLevel: return BadRequest(), [] @child() + def plugins(self, context, segments): + """/<api>/plugins + /<api>/plugins/<plugin_name> + /<api>/plugins/<plugin_name>/... + """ + if self.api.version_info < (3, 1): + return NotFound(), [] + if len(segments) == 0: + return AllPlugins(), [] + else: + plugin_name = segments.pop(0) + return APlugin(plugin_name), segments + + @child() def bans(self, context, segments): """/<api>/bans /<api>/bans/<email> diff --git a/src/mailman/rest/tests/test_plugins.py b/src/mailman/rest/tests/test_plugins.py new file mode 100644 index 000000000..f6d7afb76 --- /dev/null +++ b/src/mailman/rest/tests/test_plugins.py @@ -0,0 +1,104 @@ +# Copyright (C) 2010-2017 by the Free Software Foundation, Inc. +# +# This file is part of GNU Mailman. +# +# GNU Mailman is free software: you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) +# any later version. +# +# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along with +# GNU Mailman. If not, see <http://www.gnu.org/licenses/>. + +"""Test RESTability of plugins.""" + +import unittest + +from mailman.testing.helpers import call_api +from mailman.testing.layers import RESTLayer +from urllib.error import HTTPError + + +class TestAPI31Plugins(unittest.TestCase): + layer = RESTLayer + + def test_list_plugins(self): + json, response = call_api('http://localhost:9001/3.1/plugins') + self.assertEqual(response.status_code, 200) + json.pop('http_etag') + self.assertEqual(len(json.keys()), 3) + self.assertIn('entries', json.keys()) + entries = json['entries'] + self.assertEqual(len(entries), 1) + plugin_conf = entries[0] + self.assertEqual(plugin_conf['name'], 'example') + self.assertEqual(plugin_conf['enable'], True) + self.assertEqual(plugin_conf['class'], + 'mailman.testing.plugin.ExamplePlugin') + self.assertEqual(plugin_conf['path'], '') + + def test_non_existent_plugin(self): + with self.assertRaises(HTTPError) as cm: + call_api('http://localhost:9001/3.1/plugins/does-not-exist') + self.assertEqual(cm.exception.code, 404) + + def test_example_plugin(self): + json, response = call_api('http://localhost:9001/3.1/plugins/example') + self.assertEqual(response.status_code, 200) + json.pop('http_etag') + self.assertEqual(len(json.keys()), 1) + self.assertEqual(json['example-plugin-reply'], 'yes') + + with self.assertRaises(HTTPError) as cm: + call_api('http://localhost:9001/3.1/plugins/example', + method='POST') + self.assertEqual(cm.exception.code, 400) + + def test_example_plugin_child(self): + json, response = \ + call_api('http://localhost:9001/3.1/plugins/example/good') + self.assertEqual(response.status_code, 200) + json.pop('http_etag') + self.assertEqual(len(json.keys()), 1) + self.assertEqual(json['good'], True) + + with self.assertRaises(HTTPError) as cm: + call_api('http://localhost:9001/3.1/plugins/example/bad') + self.assertEqual(cm.exception.code, 400) + + def test_example_plugin_counter(self): + json, response = \ + call_api('http://localhost:9001/3.1/plugins/example/count') + self.assertEqual(response.status_code, 200) + json.pop('http_etag') + self.assertEqual(len(json.keys()), 1) + self.assertEqual(json['count'], 0) + + json, response = \ + call_api('http://localhost:9001/3.1/plugins/example/count', + method='POST', data={'count': 5}) + self.assertEqual(response.status_code, 204) + + json, response = \ + call_api('http://localhost:9001/3.1/plugins/example/count') + self.assertEqual(response.status_code, 200) + json.pop('http_etag') + self.assertEqual(len(json.keys()), 1) + self.assertEqual(json['count'], 5) + + json, response = \ + call_api('http://localhost:9001/3.1/plugins/example/count', + method='DELETE') + self.assertEqual(response.status_code, 204) + + json, response = \ + call_api('http://localhost:9001/3.1/plugins/example/count') + self.assertEqual(response.status_code, 200) + json.pop('http_etag') + self.assertEqual(len(json.keys()), 1) + self.assertEqual(json['count'], 0) diff --git a/src/mailman/testing/plugin.py b/src/mailman/testing/plugin.py index 75667844a..a8b001406 100644 --- a/src/mailman/testing/plugin.py +++ b/src/mailman/testing/plugin.py @@ -15,22 +15,87 @@ # You should have received a copy of the GNU General Public License along with # GNU Mailman. If not, see <http://www.gnu.org/licenses/>. -"""Example plugin with hooks for testing purposes.""" +"""Example plugin with hooks and REST for testing purposes.""" +from mailman.config import config from mailman.interfaces.plugin import IPlugin +from mailman.rest.helpers import bad_request, child, etag, no_content, okay +from mailman.rest.validator import Validator from public import public from zope.interface import implementer @public +class GoodREST: + + def on_get(self, request, response): + okay(response, etag({'good': True})) + + +@public +class BadREST: + + def on_get(self, request, response): + bad_request(response, etag({'good': False})) + + +@public +class CountingREST: + + def __init__(self): + self._plugin = config.plugins['example'] + + def on_get(self, request, response): + okay(response, etag({'count': self._plugin.counter})) + + def on_post(self, request, response): + try: + counter = Validator(count=int)(request) + self._plugin.counter = counter['count'] + except ValueError as error: + bad_request(response, str(error)) + else: + no_content(response) + + def on_delete(self, request, response): + self._plugin.counter = 0 + no_content(response) + + +@public +class RESTExample: + + def on_get(self, request, response): + okay(response, etag({'example-plugin-reply': 'yes'})) + + def on_post(self, request, response): + bad_request(response) + + @child() + def good(self, context, segments): + return GoodREST(), [] + + @child() + def bad(self, context, segments): + return BadREST(), [] + + @child() + def count(self, context, segments): + return CountingREST(), [] + + +@public @implementer(IPlugin) class ExamplePlugin: def __init__(self): - pass + self.counter = 0 def pre_hook(self): pass def post_hook(self): pass + + def rest_object(self): + return RESTExample() |
