Merge lp:~wallyworld/lazr.restful/propogate-notifications into lp:lazr.restful
- propogate-notifications
- Merge into trunk
Status: | Merged |
---|---|
Merge reported by: | Ian Booth |
Merged at revision: | not available |
Proposed branch: | lp:~wallyworld/lazr.restful/propogate-notifications |
Merge into: | lp:lazr.restful |
Diff against target: |
329 lines (+149/-18) 7 files modified
setup.py (+1/-0) src/lazr/restful/NEWS.txt (+7/-1) src/lazr/restful/_resource.py (+0/-2) src/lazr/restful/interfaces/_rest.py (+34/-3) src/lazr/restful/publisher.py (+49/-9) src/lazr/restful/tests/test_webservice.py (+57/-3) versions.cfg (+1/-0) |
To merge this branch: | bzr merge lp:~wallyworld/lazr.restful/propogate-notifications |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Benji York (community) | code | Approve | |
Review via email: mp+54690@code.launchpad.net |
Commit message
Description of the change
Add support for propagating notifications resulting from a server call back to the client. The notifications are json encoded in a response header attribute (XHR-Notificati
Leonard Richardson (leonardr) wrote : | # |
Ian Booth (wallyworld) wrote : | # |
> 1. Custom HTTP headers need to start with X-, and by convention our custom
> HTTP headers start with X-Lazr-. So XHR-Notifications should be X-Lazr-XHR-
> Notifications. But there's nothing XHR-specific about these notifications.
> That's how you're using them now, but other clients could use them for other
> purposes. So I recommend just X-Lazr-
>
Done
> 2. The more complicated the value of the HTTP header, the less comfortable I
> feel. My ideal is for HTTP header values to be plain text. If client support
> allows it, I would prefer to see each notification sent in a separate copy of
> the header:
>
> X-Lazr-
> X-Lazr-
>
<snip>
I may be wrong - RFC2616 says
"Multiple message-header fields with the same field-name MAY be present in a message if and only if the entire field-value for that header field is defined as a comma-separated list [i.e., #(values)]. It MUST be possible to combine the multiple header fields into one "field-name: field-value" pair, without changing the semantics of the message, by appending each subsequent field-value to the first, each separated by a comma"
Since the notification text values may be escaped html and totally free form text, it's not possible I think to do what is suggested above. The only way I can see at this point is to encode the notification text values as a json string and send as one header value.
>
> 3. I don't think you need an INotificationsP
> simple.py. It's a really simple class and any real implementation needs a
> source for the notifications anyway. If you want to use it in the example/base
> doctests (see below), you could
>
> 4. You register a notification adapter in example/base, but you don't use it
> in any of the example/base doctests. There's no reason to put something in the
> example web service if you don't demonstrate the functionality. You can
> either take out the notification adapter (along with the simple.py
> implementation), or you can add a demonstration to the example/base doctests.
> For an optional feature like this, I think a unit test is sufficient.
Removed from examples etc and just retained the unit test.
Benji York (benji) wrote : | # |
On Fri, Mar 25, 2011 at 9:25 AM, Ian Booth <email address hidden> wrote:
>> 1. Custom HTTP headers need to start with X-, and by convention our custom
>> HTTP headers start with X-Lazr-. So XHR-Notifications should be X-Lazr-XHR-
>> Notifications. But there's nothing XHR-specific about these notifications.
>> That's how you're using them now, but other clients could use them for other
>> purposes. So I recommend just X-Lazr-
>>
>
> Done
>
>> 2. The more complicated the value of the HTTP header, the less comfortable I
>> feel. My ideal is for HTTP header values to be plain text. If client support
>> allows it, I would prefer to see each notification sent in a separate copy of
>> the header:
>>
>> X-Lazr-
>> X-Lazr-
>>
> <snip>
>
> I may be wrong - RFC2616 says
> "Multiple message-header fields with the same field-name MAY be
> present in a message if and only if the entire field-value for that
> header field is defined as a comma-separated list [i.e., #(values)].
> It MUST be possible to combine the multiple header fields into one
> "field-name: field-value" pair, without changing the semantics of the
> message, by appending each subsequent field-value to the first, each
> separated by a comma"
Yep. Unfortunately multiple headers aren't really meant to be "true"
sequences just shorthand for a comma-separated list. We could escape
any commas in the headers but we're starting to get hacky.
> Since the notification text values may be escaped html and totally
> free form text, it's not possible I think to do what is suggested
> above. The only way I can see at this point is to encode the
> notification text values as a json string and send as one header
> value.
Do we really want to allow arbitrary HTML? For example, what if I have
a GUI client and want to display these messages? I would have to use an
HTML renderer to be able to show them sanely. We could allow a small
subset of HTML like just anchor tags, but even that places a pretty big
burden on non-browser clients.
I propose that we start with a type (DEBUG, WARNING, etc.) and a
plain-text string. That may mean that we have to audit/rework the
existing notifications to make them compatible.
--
Benji York
Ian Booth (wallyworld) wrote : | # |
>
>> Since the notification text values may be escaped html and totally
>> free form text, it's not possible I think to do what is suggested
>> above. The only way I can see at this point is to encode the
>> notification text values as a json string and send as one header
>> value.
>
> Do we really want to allow arbitrary HTML? For example, what if I have
> a GUI client and want to display these messages? I would have to use an
> HTML renderer to be able to show them sanely. We could allow a small
> subset of HTML like just anchor tags, but even that places a pretty big
> burden on non-browser clients.
>
> I propose that we start with a type (DEBUG, WARNING, etc.) and a
> plain-text string. That may mean that we have to audit/rework the
> existing notifications to make them compatible.
Trouble is, all the launchpad usages of notifications - well very many -
stuff html in there using structured(). The assumption on launchpad's
part is that these notifications are being displayed on an html client.
The lazr restful stuff is making no assumptions about the content of the
notifications. It just runs the content through json and sends it along.
It's up to client and server to ensure that stuff sent is acceptable. In
launchpad's case, client.js grabs the html and displays it. If launchpad
currently assumes it's to send notifications as html, that's ok. It can
be changed later and lazr restful will continue to function unchanged.
Non browser clients won't be supported right off the bat but they will
just ignore the notification header value anyway. The initial goal is to
fix the issue in launchpad where inline editing of stuff works different
to non ajax editing in that it doesn't display the same messages to the
user.
Leonard Richardson (leonardr) wrote : | # |
OK, I'm convinced that there's no simpler solution right now than JSON-encoding the notifications and putting them in a single header. The implementation looks good as well.
The only suggestion I can make is that you should be able to put a single _processNotific
Ian Booth (wallyworld) wrote : | # |
> OK, I'm convinced that there's no simpler solution right now than JSON-
> encoding the notifications and putting them in a single header. The
> implementation looks good as well.
>
> The only suggestion I can make is that you should be able to put a single
> _processNotific
> instead of calling it from within every implementation of __call__. This would
> centralize the notification code so it's only called once, and it would also
> put it 'outside' of the resource processing, in the request processing. This
> fits nicely with the way we moved the notifications themselves 'outside' of
> the resource representations and into the HTTP response.
I've moved the notifications processing code as suggested to WebServicePubli
Benji York (benji) wrote : | # |
There are a couple of things that are still needed on this branch.
First, I can't get the tests to pass. First there was an undeclared
dependency on testtool, so I removed references to it but then I got
many, many other failures. If you're not seeing these, then I suspect
you're running the tests with a dirty Python (e.g., a system Python).
Running with a clean Python (or virtualenv) should result in the same
failures.
The INotificationsP
attributes or methods). It looks like it should have a "notifications"
attribute. Something like this should do:
=== modified file 'src/lazr/
--- src/lazr/
+++ src/lazr/
@@ -56,6 +56,8 @@
'IWebServi
]
+from textwrap import dedent
+
from zope.schema import Bool, Dict, Int, List, Text, TextLine
from zope.interface import Attribute, Interface
# These two should really be imported from zope.interface, but
@@ -281,16 +283,20 @@
"""A response object which contains notifications.
A web framework may define an adapter for this interface, allowing
- the web service to provide an implementation having a 'notifications'
- property, providing a list of namedtuples (level, message) which can be
- used by the caller to display extra information about the completed
- request. 'level' matches the standard logging levels:
- DEBUG = logging.DEBUG # A debugging message
- INFO = logging.INFO # simple confirmation of a change
- WARNING = logging.WARNING # action will not be successful unless you ...
- ERROR = logging.ERROR # the previous action did not succeed, and why
+ the web service to provide notifications to be sent to the client.
"""
+ notifications = Attribute(
+ A list of namedtuples (level, message) which can be used by the
+ caller to display extra information about the completed request.
+ 'level' matches the standard logging levels:
+
+ DEBUG = logging.DEBUG # a debugging message
+ INFO = logging.INFO # simple confirmation of a change
+ WARNING = logging.WARNING # action will not be successful unless...
+ ERROR = logging.ERROR # the previous action did not succeed
+ """))
+
Now for lesser things:
A couple of the multi-line imports in src/lazr/
trailing commas.
Here's a small whitespace fix:
=== modified file 'src/lazr/
--- src/lazr/
+++ src/lazr/
@@ -201,7 +201,7 @@
and notifications_
- json_notifications =simplejson.
+ json_notifications = simplejson.
Ian Booth (wallyworld) wrote : | # |
> There are a couple of things that are still needed on this branch.
>
> First, I can't get the tests to pass. First there was an undeclared
> dependency on testtool, so I removed references to it but then I got
> many, many other failures. If you're not seeing these, then I suspect
> you're running the tests with a dirty Python (e.g., a system Python).
> Running with a clean Python (or virtualenv) should result in the same
> failures.
>
testtool is used in tests other than the new one created for this mp. The issue is that it had not been added to the project dependencies in setup.py etc. So I've fixed that and run the tests using virtualenv and they all pass. I had to also remove as a drive-by a bad import that was causing lots of noise in the test run.
Other requested fixes also done.
Preview Diff
1 | === modified file 'setup.py' |
2 | --- setup.py 2011-03-31 03:34:49 +0000 |
3 | +++ setup.py 2011-03-31 22:44:29 +0000 |
4 | @@ -66,6 +66,7 @@ |
5 | 'pytz', |
6 | 'setuptools', |
7 | 'simplejson>=2.1.0', |
8 | + 'testtools', |
9 | 'van.testing', |
10 | 'wsgiref', |
11 | 'zope.app.pagetemplate', |
12 | |
13 | === modified file 'src/lazr/restful/NEWS.txt' |
14 | --- src/lazr/restful/NEWS.txt 2011-03-31 04:30:56 +0000 |
15 | +++ src/lazr/restful/NEWS.txt 2011-03-31 22:44:29 +0000 |
16 | @@ -2,7 +2,7 @@ |
17 | NEWS for lazr.restful |
18 | ===================== |
19 | |
20 | -0.18.1 (2011-03-25) |
21 | +0.18.1 (2011-04-01) |
22 | =================== |
23 | |
24 | Fixed minor test failures. |
25 | @@ -10,6 +10,12 @@ |
26 | The object modification event will not be fired if a client sends an |
27 | empty changeset via PATCH. |
28 | |
29 | +The webservice may define an adapter which is used, after an operation on a |
30 | +resource, to provide notifications consisting of namedtuples (level, message). |
31 | +Any notifications are json encoded and inserted into the response header using |
32 | +the 'X-Lazr-Notification' key. They may then be used by the caller to provide |
33 | +extra information to the user about the completed request. |
34 | + |
35 | The webservice:json TALES function now returns JSON that will survive |
36 | HTML escaping. |
37 | |
38 | |
39 | === modified file 'src/lazr/restful/_resource.py' |
40 | --- src/lazr/restful/_resource.py 2011-03-31 03:36:23 +0000 |
41 | +++ src/lazr/restful/_resource.py 2011-03-31 22:44:29 +0000 |
42 | @@ -102,7 +102,6 @@ |
43 | IHTTPResource, |
44 | IJSONPublishable, |
45 | IReference, |
46 | - IReferenceChoice, |
47 | IRepresentationCache, |
48 | IResourceDELETEOperation, |
49 | IResourceGETOperation, |
50 | @@ -121,7 +120,6 @@ |
51 | ) |
52 | from lazr.restful.utils import ( |
53 | extract_write_portion, |
54 | - get_current_browser_request, |
55 | get_current_web_service_request, |
56 | parse_accept_style_header, |
57 | sorted_named_things, |
58 | |
59 | === modified file 'src/lazr/restful/interfaces/_rest.py' |
60 | --- src/lazr/restful/interfaces/_rest.py 2011-03-15 14:13:10 +0000 |
61 | +++ src/lazr/restful/interfaces/_rest.py 2011-03-31 22:44:29 +0000 |
62 | @@ -32,6 +32,7 @@ |
63 | 'IFieldMarshaller', |
64 | 'IFileLibrarian', |
65 | 'IHTTPResource', |
66 | + 'INotificationsProvider', |
67 | 'IJSONPublishable', |
68 | 'IJSONRequestCache', |
69 | 'IRepresentationCache', |
70 | @@ -55,15 +56,28 @@ |
71 | 'IWebServiceVersion', |
72 | ] |
73 | |
74 | -from zope.schema import Bool, Dict, Int, List, Text, TextLine |
75 | -from zope.interface import Attribute, Interface |
76 | +from textwrap import dedent |
77 | +from zope.schema import ( |
78 | + Bool, |
79 | + Dict, |
80 | + Int, |
81 | + List, |
82 | + Text, |
83 | + TextLine, |
84 | + ) |
85 | +from zope.interface import ( |
86 | + Attribute, |
87 | + Interface, |
88 | + ) |
89 | # These two should really be imported from zope.interface, but |
90 | # the import fascist complains because they are not in __all__ there. |
91 | from zope.interface.interface import invariant |
92 | from zope.interface.exceptions import Invalid |
93 | from zope.publisher.interfaces import IPublishTraverse |
94 | from zope.publisher.interfaces.browser import ( |
95 | - IBrowserRequest, IDefaultBrowserLayer) |
96 | + IBrowserRequest, |
97 | + IDefaultBrowserLayer, |
98 | + ) |
99 | from lazr.batchnavigator.interfaces import InvalidBatchSizeError |
100 | |
101 | # Constants for periods of time |
102 | @@ -276,6 +290,23 @@ |
103 | """ |
104 | |
105 | |
106 | +class INotificationsProvider(Interface): |
107 | + """A response object which contains notifications. |
108 | + |
109 | + A web framework may define an adapter for this interface, allowing |
110 | + the web service to provide notifications to be sent to the client. |
111 | + """ |
112 | + notifications = Attribute(dedent("""\ |
113 | + A list of namedtuples (level, message) which can be used by the |
114 | + caller to display extra information about the completed request. |
115 | + 'level' matches the standard logging levels: |
116 | + |
117 | + DEBUG = logging.DEBUG # a debugging message |
118 | + INFO = logging.INFO # simple confirmation of a change |
119 | + WARNING = logging.WARNING # action will not be successful unless... |
120 | + ERROR = logging.ERROR # the previous action did not succeed |
121 | + """)) |
122 | + |
123 | class IWebServiceClientRequest(IBrowserRequest): |
124 | """Interface for requests to the web service.""" |
125 | version = Attribute("The version of the web service that the client " |
126 | |
127 | === modified file 'src/lazr/restful/publisher.py' |
128 | --- src/lazr/restful/publisher.py 2011-03-17 14:39:17 +0000 |
129 | +++ src/lazr/restful/publisher.py 2011-03-31 22:44:29 +0000 |
130 | @@ -14,14 +14,23 @@ |
131 | ] |
132 | |
133 | |
134 | -import traceback |
135 | +import simplejson |
136 | import urllib |
137 | import urlparse |
138 | |
139 | from zope.component import ( |
140 | - adapter, getMultiAdapter, getUtility, queryAdapter, queryMultiAdapter) |
141 | + adapter, |
142 | + getMultiAdapter, |
143 | + getUtility, |
144 | + queryAdapter, |
145 | + queryMultiAdapter, |
146 | + ) |
147 | from zope.component.interfaces import ComponentLookupError |
148 | -from zope.interface import alsoProvides, implementer, implements |
149 | +from zope.interface import ( |
150 | + alsoProvides, |
151 | + implementer, |
152 | + implements, |
153 | + ) |
154 | from zope.publisher.interfaces import NotFound |
155 | from zope.publisher.interfaces.browser import IBrowserRequest |
156 | from zope.schema.interfaces import IBytes |
157 | @@ -30,13 +39,26 @@ |
158 | from lazr.uri import URI |
159 | |
160 | from lazr.restful import ( |
161 | - CollectionResource, EntryField, EntryFieldResource, |
162 | - EntryResource, ScopedCollection, ServiceRootResource) |
163 | + CollectionResource, |
164 | + EntryField, |
165 | + EntryFieldResource, |
166 | + EntryResource, |
167 | + ScopedCollection, |
168 | + ) |
169 | from lazr.restful.interfaces import ( |
170 | - IByteStorage, ICollection, ICollectionField, IEntry, IEntryField, |
171 | - IHTTPResource, IReference, IServiceRootResource, |
172 | - IWebBrowserInitiatedRequest, IWebServiceClientRequest, |
173 | - IWebServiceConfiguration, IWebServiceVersion) |
174 | + IByteStorage, |
175 | + ICollection, |
176 | + ICollectionField, |
177 | + IEntry, |
178 | + IEntryField, |
179 | + IHTTPResource, |
180 | + INotificationsProvider, |
181 | + IReference, |
182 | + IServiceRootResource, |
183 | + IWebBrowserInitiatedRequest, |
184 | + IWebServiceClientRequest, |
185 | + IWebServiceConfiguration, |
186 | + ) |
187 | from lazr.restful.utils import tag_request_with_version_name |
188 | |
189 | |
190 | @@ -166,10 +188,28 @@ |
191 | # Wrap the resource in a security proxy. |
192 | return ProxyFactory(resource) |
193 | |
194 | + def _processNotifications(self, request): |
195 | + """Add any notification messages to the response headers. |
196 | + |
197 | + If the webservice has defined an INotificationsProvider adaptor, use |
198 | + it to include with the response the relevant notification messages |
199 | + and their severity levels. |
200 | + """ |
201 | + notifications_provider = INotificationsProvider(request, None) |
202 | + notifications = [] |
203 | + if (notifications_provider is not None |
204 | + and notifications_provider.notifications): |
205 | + notifications = ([(notification.level, notification.message) |
206 | + for notification in notifications_provider.notifications]) |
207 | + json_notifications = simplejson.dumps(notifications) |
208 | + request.response.setHeader( |
209 | + 'X-Lazr-Notifications', json_notifications) |
210 | + |
211 | def callObject(self, request, object): |
212 | """Help web browsers handle redirects correctly.""" |
213 | value = super( |
214 | WebServicePublicationMixin, self).callObject(request, object) |
215 | + self._processNotifications(request) |
216 | if request.response.getStatus() / 100 == 3: |
217 | vhost = URI(request.getApplicationURL()).host |
218 | if IWebBrowserInitiatedRequest.providedBy(request): |
219 | |
220 | === modified file 'src/lazr/restful/tests/test_webservice.py' |
221 | --- src/lazr/restful/tests/test_webservice.py 2011-03-25 11:33:14 +0000 |
222 | +++ src/lazr/restful/tests/test_webservice.py 2011-03-31 22:44:29 +0000 |
223 | @@ -9,6 +9,8 @@ |
224 | from lxml import etree |
225 | from operator import attrgetter |
226 | from textwrap import dedent |
227 | +import collections |
228 | +import logging |
229 | import random |
230 | import re |
231 | import simplejson |
232 | @@ -20,7 +22,6 @@ |
233 | getUtility, |
234 | ) |
235 | from zope.interface import implements, Interface |
236 | -from zope.interface.interface import InterfaceClass |
237 | from zope.publisher.browser import TestRequest |
238 | from zope.schema import Choice, Date, Datetime, TextLine |
239 | from zope.schema.interfaces import ITextLine |
240 | @@ -31,8 +32,6 @@ |
241 | ) |
242 | from zope.traversing.browser.interfaces import IAbsoluteURL |
243 | |
244 | -from wadllib.application import Application |
245 | - |
246 | from lazr.enum import EnumeratedType, Item |
247 | from lazr.lifecycle.interfaces import IObjectModifiedEvent |
248 | from lazr.restful import ( |
249 | @@ -44,6 +43,7 @@ |
250 | ICollection, |
251 | IEntry, |
252 | IFieldHTMLRenderer, |
253 | + INotificationsProvider, |
254 | IResourceGETOperation, |
255 | IServiceRootResource, |
256 | IWebBrowserOriginatingRequest, |
257 | @@ -923,6 +923,60 @@ |
258 | return ResourceOperation(None, request) |
259 | |
260 | |
261 | +Notification = collections.namedtuple('Notification', ['level', 'message']) |
262 | + |
263 | + |
264 | +class NotificationsProviderTest(EntryTestCase): |
265 | + """Test that notifcations are included in the response headers.""" |
266 | + |
267 | + testmodule_objects = [HasOneField, IHasOneField] |
268 | + |
269 | + class DummyWebsiteRequestWithNotifications: |
270 | + """A request to the website, as opposed to the web service.""" |
271 | + implements(INotificationsProvider) |
272 | + |
273 | + @property |
274 | + def notifications(self): |
275 | + return [Notification(logging.INFO, "Informational"), |
276 | + Notification(logging.WARNING, "Warning") |
277 | + ] |
278 | + def setUp(self): |
279 | + super(NotificationsProviderTest, self).setUp() |
280 | + self.default_media_type = "application/json;include=lp_html" |
281 | + self._register_website_url_space(IHasOneField) |
282 | + self._register_notification_adapter() |
283 | + |
284 | + def _register_notification_adapter(self): |
285 | + """Simulates a service where an entry corresponds to a web page.""" |
286 | + |
287 | + # First, create a converter from web service requests to |
288 | + # web service requests with notifications. |
289 | + def web_service_request_to_notification_request(service_request): |
290 | + """Create a corresponding request to the website.""" |
291 | + return self.DummyWebsiteRequestWithNotifications() |
292 | + |
293 | + getGlobalSiteManager().registerAdapter( |
294 | + web_service_request_to_notification_request, |
295 | + [IWebServiceClientRequest], INotificationsProvider) |
296 | + |
297 | + @contextmanager |
298 | + def resource(self): |
299 | + """Simplify the entry_resource call.""" |
300 | + with self.entry_resource(IHasOneField, HasOneField, "") as resource: |
301 | + yield resource |
302 | + |
303 | + def test_response_notifications(self): |
304 | + with self.resource() as resource: |
305 | + resource.request.publication.callObject( |
306 | + resource.request, resource) |
307 | + notifications = resource.request.response.getHeader( |
308 | + "X-Lazr-Notifications") |
309 | + self.assertFalse(notifications is None) |
310 | + notifications = simplejson.loads(notifications) |
311 | + expected_notifications = [ |
312 | + [logging.INFO, "Informational"], [logging.WARNING, "Warning"]] |
313 | + self.assertEquals(notifications, expected_notifications) |
314 | + |
315 | class EventTestCase(EntryTestCase): |
316 | |
317 | testmodule_objects = [IHasOneField] |
318 | |
319 | === modified file 'versions.cfg' |
320 | --- versions.cfg 2011-03-31 03:34:49 +0000 |
321 | +++ versions.cfg 2011-03-31 22:44:29 +0000 |
322 | @@ -26,6 +26,7 @@ |
323 | pytz = 2010h |
324 | setuptools = 0.6c11 |
325 | simplejson = 2.1.3 |
326 | +testtools = 0.9.8 |
327 | transaction = 1.0.0 |
328 | van.testing = 2.0.1 |
329 | wsgi-intercept = 0.4 |
I like how this is shaping up. The test in test_webservice.py is great. Here are my comments on the design:
1. Custom HTTP headers need to start with X-, and by convention our custom HTTP headers start with X-Lazr-. So XHR-Notifications should be X-Lazr- XHR-Notificatio ns. But there's nothing XHR-specific about these notifications. That's how you're using them now, but other clients could use them for other purposes. So I recommend just X-Lazr- Notification.
2. The more complicated the value of the HTTP header, the less comfortable I feel. My ideal is for HTTP header values to be plain text. If client support allows it, I would prefer to see each notification sent in a separate copy of the header:
X-Lazr- Notification: ['DEBUG', 'Notification 1'] Notification: ['WARN', 'Notification 2']
X-Lazr-
Or, even better:
X-Lazr- Notification: DEBUG: Notification 1 Notification: WARN: Notification 2
X-Lazr-
Or:
X-Lazr- Notification- DEBUG: Notification 1 Notification- WARN: Notification 2
X-Lazr-
It's up to you--just keep in mind that non-Ajax clients might want this information as well.
3. I don't think you need an INotificationsP rovider implementation in simple.py. It's a really simple class and any real implementation needs a source for the notifications anyway. If you want to use it in the example/base doctests (see below), you could
4. You register a notification adapter in example/base, but you don't use it in any of the example/base doctests. There's no reason to put something in the example web service if you don't demonstrate the functionality. You can either take out the notification adapter (along with the simple.py implementation), or you can add a demonstration to the example/base doctests. For an optional feature like this, I think a unit test is sufficient.