~cjwatson/lazr.restful/double-closing-brace

24.1.2 by Gary Poster
a broken but "complete" commit of the first draft. names are all changed, setup.py and buildout.cfg are on the right track hopefully, and we have all the files we need, to the best of mu knowledge. Only a couple of docs have been converted to ReST, and there is no intro doc.
1
# Copyright 2009 Canonical Ltd.  All rights reserved.
2
3
"""Publisher mixins for the webservice.
4
5
This module defines classes that are usually needed for integration
6
with the Zope publisher.
7
"""
8
9
__metaclass__ = type
10
__all__ = [
11
    'browser_request_to_web_service_request',
12
    'WebServicePublicationMixin',
13
    'WebServiceRequestTraversal',
14
    ]
15
46 by Barry Warsaw
Merge upstream r45. Add NEWS.txt entry.
16
184.2.10 by Ian Booth
Move notifications processing to WebServicePublicationMixin and driveby import cleanup
17
import simplejson
24.1.2 by Gary Poster
a broken but "complete" commit of the first draft. names are all changed, setup.py and buildout.cfg are on the right track hopefully, and we have all the files we need, to the best of mu knowledge. Only a couple of docs have been converted to ReST, and there is no intro doc.
18
import urllib
147.1.1 by Leonard Richardson
Initial implementation.
19
import urlparse
24.1.2 by Gary Poster
a broken but "complete" commit of the first draft. names are all changed, setup.py and buildout.cfg are on the right track hopefully, and we have all the files we need, to the best of mu knowledge. Only a couple of docs have been converted to ReST, and there is no intro doc.
20
21
from zope.component import (
184.2.10 by Ian Booth
Move notifications processing to WebServicePublicationMixin and driveby import cleanup
22
    adapter,
23
    getMultiAdapter,
24
    getUtility,
25
    queryAdapter,
184.2.11 by Ian Booth
Add testtools dependency and other drive by fixes
26
    queryMultiAdapter,
184.2.10 by Ian Booth
Move notifications processing to WebServicePublicationMixin and driveby import cleanup
27
    )
92.1.10 by Leonard Richardson
Got tests to pass.
28
from zope.component.interfaces import ComponentLookupError
184.2.10 by Ian Booth
Move notifications processing to WebServicePublicationMixin and driveby import cleanup
29
from zope.interface import (
30
    alsoProvides,
31
    implementer,
32
    )
47.1.2 by Leonard Richardson
OK, I had to move all the Simple* classes into lazr.restful.simple, but I was able to remove a lot of code from SimpleWebServiceRootResource subclasses.
33
from zope.publisher.interfaces import NotFound
24.1.2 by Gary Poster
a broken but "complete" commit of the first draft. names are all changed, setup.py and buildout.cfg are on the right track hopefully, and we have all the files we need, to the best of mu knowledge. Only a couple of docs have been converted to ReST, and there is no intro doc.
34
from zope.publisher.interfaces.browser import IBrowserRequest
163.1.1 by Leonard Richardson
Stop using IObject as an acceptable substitute for IReference.
35
from zope.schema.interfaces import IBytes
24.1.2 by Gary Poster
a broken but "complete" commit of the first draft. names are all changed, setup.py and buildout.cfg are on the right track hopefully, and we have all the files we need, to the best of mu knowledge. Only a couple of docs have been converted to ReST, and there is no intro doc.
36
from zope.security.checker import ProxyFactory
37
43 by Barry Warsaw
ADdress bug 387487 by extending traversal to subentries.
38
from lazr.restful import (
184.2.10 by Ian Booth
Move notifications processing to WebServicePublicationMixin and driveby import cleanup
39
    CollectionResource,
40
    EntryField,
41
    EntryFieldResource,
42
    EntryResource,
43
    ScopedCollection,
44
    )
24.1.2 by Gary Poster
a broken but "complete" commit of the first draft. names are all changed, setup.py and buildout.cfg are on the right track hopefully, and we have all the files we need, to the best of mu knowledge. Only a couple of docs have been converted to ReST, and there is no intro doc.
45
from lazr.restful.interfaces import (
184.2.10 by Ian Booth
Move notifications processing to WebServicePublicationMixin and driveby import cleanup
46
    IByteStorage,
47
    ICollection,
48
    ICollectionField,
49
    IEntry,
50
    IEntryField,
51
    IHTTPResource,
52
    INotificationsProvider,
53
    IReference,
54
    IServiceRootResource,
55
    IWebBrowserInitiatedRequest,
56
    IWebServiceClientRequest,
57
    IWebServiceConfiguration,
58
    )
117.1.1 by Leonard Richardson
Refactored the code for tagging a request object with version information, and used it consistently.
59
from lazr.restful.utils import tag_request_with_version_name
24.1.2 by Gary Poster
a broken but "complete" commit of the first draft. names are all changed, setup.py and buildout.cfg are on the right track hopefully, and we have all the files we need, to the best of mu knowledge. Only a couple of docs have been converted to ReST, and there is no intro doc.
60
61
62
class WebServicePublicationMixin:
63
    """A mixin for webservice publication.
64
65
    This should usually be mixed-in with ZopePublication, or Browser,
43 by Barry Warsaw
ADdress bug 387487 by extending traversal to subentries.
66
    or HTTPPublication.
67
    """
24.1.2 by Gary Poster
a broken but "complete" commit of the first draft. names are all changed, setup.py and buildout.cfg are on the right track hopefully, and we have all the files we need, to the best of mu knowledge. Only a couple of docs have been converted to ReST, and there is no intro doc.
68
69
    def traverseName(self, request, ob, name):
70
        """See `zope.publisher.interfaces.IPublication`.
71
72
        In addition to the default traversal implementation, this publication
73
        also handles traversal to collection scoped into an entry.
74
        """
75
        # If this is the last traversal step, then look first for a scoped
76
        # collection. This is done because although Navigation handles
77
        # traversal to entries in a scoped collection, they don't usually
78
        # handle traversing to the scoped collection itself.
79
        if len(request.getTraversalStack()) == 0:
80
            try:
93.1.9 by Leonard Richardson
I'm not sure why, but getMultiAdapter works where interface constructors fail.
81
                entry = getMultiAdapter((ob, request), IEntry)
82
            except ComponentLookupError:
93.1.41 by Leonard Richardson
Added cleanup.
83
                # This doesn't look like a lazr.restful object. Let
84
                # the superclass handle traversal.
24.1.2 by Gary Poster
a broken but "complete" commit of the first draft. names are all changed, setup.py and buildout.cfg are on the right track hopefully, and we have all the files we need, to the best of mu knowledge. Only a couple of docs have been converted to ReST, and there is no intro doc.
85
                pass
86
            else:
75.1.1 by Leonard Richardson
Initial implementation.
87
                if name.endswith("_link"):
88
                    # The user wants to access the link resource itself,
89
                    # rather than the object on the other side of the link.
90
                    if name.endswith("_collection_link"):
91
                        schema_name = name[:-16]
92
                    else:
93
                        schema_name = name[:-5]
94
                    field = entry.schema.get(schema_name)
95
                    return EntryField(entry, field, name)
24.1.2 by Gary Poster
a broken but "complete" commit of the first draft. names are all changed, setup.py and buildout.cfg are on the right track hopefully, and we have all the files we need, to the best of mu knowledge. Only a couple of docs have been converted to ReST, and there is no intro doc.
96
                field = entry.schema.get(name)
97
                if ICollectionField.providedBy(field):
98
                    result = self._traverseToScopedCollection(
99
                        request, entry, field, name)
100
                    if result is not None:
101
                        return result
102
                elif IBytes.providedBy(field):
103
                    return self._traverseToByteStorage(
104
                        request, entry, field, name)
163.1.1 by Leonard Richardson
Stop using IObject as an acceptable substitute for IReference.
105
                elif IReference.providedBy(field):
43 by Barry Warsaw
ADdress bug 387487 by extending traversal to subentries.
106
                    sub_entry = getattr(entry, name, None)
107
                    if sub_entry is None:
108
                        raise NotFound(ob, name, request)
109
                    else:
110
                        return sub_entry
24.1.2 by Gary Poster
a broken but "complete" commit of the first draft. names are all changed, setup.py and buildout.cfg are on the right track hopefully, and we have all the files we need, to the best of mu knowledge. Only a couple of docs have been converted to ReST, and there is no intro doc.
111
                elif field is not None:
112
                    return EntryField(entry, field, name)
113
                else:
114
                    # Falls through to our parent version.
115
                    pass
116
        return super(WebServicePublicationMixin, self).traverseName(
117
            request, ob, name)
118
119
    def _traverseToByteStorage(self, request, entry, field, name):
120
        """Try to traverse to a byte storage resource in entry."""
121
        # Even if the library file is None, we want to allow
122
        # traversal, because the request might be a PUT request
123
        # creating a file here.
124
        return getMultiAdapter((entry, field.bind(entry)), IByteStorage)
125
126
    def _traverseToScopedCollection(self, request, entry, field, name):
127
        """Try to traverse to a collection in entry.
128
129
        This is done because we don't usually traverse to attributes
130
        representing a collection in our regular Navigation.
131
132
        This method returns None if a scoped collection cannot be found.
133
        """
134
        collection = getattr(entry, name, None)
135
        if collection is None:
136
            return None
93.1.22 by Leonard Richardson
Changed another requestless adapter lookup and consturctor to employ the request. webservice.txt tests now all pass.
137
        scoped_collection = ScopedCollection(entry.context, entry, request)
24.1.2 by Gary Poster
a broken but "complete" commit of the first draft. names are all changed, setup.py and buildout.cfg are on the right track hopefully, and we have all the files we need, to the best of mu knowledge. Only a couple of docs have been converted to ReST, and there is no intro doc.
138
        # Tell the IScopedCollection object what collection it's managing,
139
        # and what the collection's relationship is to the entry it's
140
        # scoped to.
141
        scoped_collection.collection = collection
142
        scoped_collection.relationship = field
143
        return scoped_collection
144
145
    def getDefaultTraversal(self, request, ob):
146
        """See `zope.publisher.interfaces.browser.IBrowserPublication`.
147
148
        The WebService doesn't use the getDefaultTraversal() extension
149
        mechanism, because it only applies to GET, HEAD, and POST methods.
150
151
        See getResource() for the alternate mechanism.
152
        """
153
        # Don't traverse to anything else.
154
        return ob, None
155
156
    def getResource(self, request, ob):
157
        """Return the resource that can publish the object ob.
158
159
        This is done at the end of traversal.  If the published object
160
        supports the ICollection, or IEntry interface we wrap it into the
161
        appropriate resource.
162
        """
203.1.2 by Curtis Hovey
Convert ComponentLookupError to NotFound when getResource() cannot
163
        try:
164
            if (ICollection.providedBy(ob) or
165
                queryMultiAdapter((ob, request), ICollection) is not None):
166
                # Object supports ICollection protocol.
167
                resource = CollectionResource(ob, request)
168
            elif (IEntry.providedBy(ob) or
169
                  queryMultiAdapter((ob, request), IEntry) is not None):
170
                # Object supports IEntry protocol.
171
                resource = EntryResource(ob, request)
172
            elif (IEntryField.providedBy(ob) or
173
                  queryAdapter(ob, IEntryField) is not None):
174
                # Object supports IEntryField protocol.
175
                resource = EntryFieldResource(ob, request)
176
            elif queryMultiAdapter((ob, request), IHTTPResource) is not None:
177
                # Object can be adapted to a resource.
178
                resource = queryMultiAdapter((ob, request), IHTTPResource)
179
            elif IHTTPResource.providedBy(ob):
180
                # A resource knows how to take care of itself.
181
                return ob
182
            else:
183
                # This object should not be published on the web service.
184
                raise NotFound(ob, '')
185
        except ComponentLookupError:
24.1.2 by Gary Poster
a broken but "complete" commit of the first draft. names are all changed, setup.py and buildout.cfg are on the right track hopefully, and we have all the files we need, to the best of mu knowledge. Only a couple of docs have been converted to ReST, and there is no intro doc.
186
            raise NotFound(ob, '')
187
188
        # Wrap the resource in a security proxy.
189
        return ProxyFactory(resource)
190
184.2.10 by Ian Booth
Move notifications processing to WebServicePublicationMixin and driveby import cleanup
191
    def _processNotifications(self, request):
192
        """Add any notification messages to the response headers.
193
194
        If the webservice has defined an INotificationsProvider adaptor, use
195
        it to include with the response the relevant notification messages
196
        and their severity levels.
197
        """
198
        notifications_provider = INotificationsProvider(request, None)
199
        notifications = []
200
        if (notifications_provider is not None
201
            and notifications_provider.notifications):
202
            notifications = ([(notification.level, notification.message)
203
                 for notification in notifications_provider.notifications])
184.2.11 by Ian Booth
Add testtools dependency and other drive by fixes
204
        json_notifications = simplejson.dumps(notifications)
184.2.10 by Ian Booth
Move notifications processing to WebServicePublicationMixin and driveby import cleanup
205
        request.response.setHeader(
206
            'X-Lazr-Notifications', json_notifications)
207
24.1.2 by Gary Poster
a broken but "complete" commit of the first draft. names are all changed, setup.py and buildout.cfg are on the right track hopefully, and we have all the files we need, to the best of mu knowledge. Only a couple of docs have been converted to ReST, and there is no intro doc.
208
    def callObject(self, request, object):
209
        """Help web browsers handle redirects correctly."""
210
        value = super(
211
            WebServicePublicationMixin, self).callObject(request, object)
184.2.10 by Ian Booth
Move notifications processing to WebServicePublicationMixin and driveby import cleanup
212
        self._processNotifications(request)
24.1.2 by Gary Poster
a broken but "complete" commit of the first draft. names are all changed, setup.py and buildout.cfg are on the right track hopefully, and we have all the files we need, to the best of mu knowledge. Only a couple of docs have been converted to ReST, and there is no intro doc.
213
        if request.response.getStatus() / 100 == 3:
214
            if IWebBrowserInitiatedRequest.providedBy(request):
215
                # This request was (probably) sent by a web
216
                # browser. Because web browsers, content negotiation,
217
                # and redirects are a deadly combination, we're going
218
                # to help the browser out a little.
219
                #
220
                # We're going to take the current request's "Accept"
221
                # header and put it into the URL specified in the
222
                # Location header. When the web browser makes its
223
                # request, it will munge the original 'Accept' header,
224
                # but because the URL it's accessing will include the
225
                # old header in the "ws.accept" header, we'll still be
226
                # able to serve the right document.
227
                location = request.response.getHeader("Location", None)
228
                if location is not None:
157.1.1 by Leonard Richardson
Implementation and test.
229
                    accept = request.getHeader("Accept", "application/json")
24.1.2 by Gary Poster
a broken but "complete" commit of the first draft. names are all changed, setup.py and buildout.cfg are on the right track hopefully, and we have all the files we need, to the best of mu knowledge. Only a couple of docs have been converted to ReST, and there is no intro doc.
230
                    qs_append = "ws.accept=" + urllib.quote(accept)
147.1.1 by Leonard Richardson
Initial implementation.
231
                    # We don't use the URI class because it will raise
232
                    # an exception if the Location contains invalid
233
                    # characters. Invalid characters may indeed be a
234
                    # problem, but let the problem be handled
235
                    # somewhere else.
236
                    (scheme, netloc, path, query, fragment) = (
237
                        urlparse.urlsplit(location))
238
                    if query == '':
203.1.1 by Curtis Hovey
Hush lint.
239
                        query = qs_append
24.1.2 by Gary Poster
a broken but "complete" commit of the first draft. names are all changed, setup.py and buildout.cfg are on the right track hopefully, and we have all the files we need, to the best of mu knowledge. Only a couple of docs have been converted to ReST, and there is no intro doc.
240
                    else:
203.1.1 by Curtis Hovey
Hush lint.
241
                        query += '&' + qs_append
147.1.1 by Leonard Richardson
Initial implementation.
242
                    uri = urlparse.urlunsplit(
243
                        (scheme, netloc, path, query, fragment))
24.1.2 by Gary Poster
a broken but "complete" commit of the first draft. names are all changed, setup.py and buildout.cfg are on the right track hopefully, and we have all the files we need, to the best of mu knowledge. Only a couple of docs have been converted to ReST, and there is no intro doc.
244
                    request.response.setHeader("Location", str(uri))
245
        return value
246
247
212 by Colin Watson
Switch zope.interface, zope.component, and lazr.delegates users from class advice to class decorators.
248
@implementer(IWebServiceClientRequest)
40.2.7 by Leonard Richardson
Moved the wsgi example TraverseWithGet to core lazr.restful.
249
class WebServiceRequestTraversal(object):
24.1.2 by Gary Poster
a broken but "complete" commit of the first draft. names are all changed, setup.py and buildout.cfg are on the right track hopefully, and we have all the files we need, to the best of mu knowledge. Only a couple of docs have been converted to ReST, and there is no intro doc.
250
    """Mixin providing web-service resource wrapping in traversal.
251
252
    This should be mixed in the request using to the base publication used.
253
    """
254
90.1.4 by Leonard Richardson
Refactored string into a constant.
255
    VERSION_ANNOTATION = 'lazr.restful.version'
256
24.1.2 by Gary Poster
a broken but "complete" commit of the first draft. names are all changed, setup.py and buildout.cfg are on the right track hopefully, and we have all the files we need, to the best of mu knowledge. Only a couple of docs have been converted to ReST, and there is no intro doc.
257
    def traverse(self, ob):
258
        """See `zope.publisher.interfaces.IPublisherRequest`.
259
260
        This is called once at the beginning of the traversal process.
261
262
        WebService requests call the `WebServicePublication.getResource()`
263
        on the result of the base class's traversal.
264
        """
265
        self._removeVirtualHostTraversals()
92.1.11 by Leonard Richardson
OK, it makes sense to always re-fetch the publication.
266
267
        # We don't trust the value of 'ob' passed in (it's probably
268
        # None) because the publication depends on which version of
269
        # the web service was requested.
270
        # _removeVirtualHostTraversals() has determined which version
271
        # was requested and has set the application appropriately, so
272
        # now we can get a good value for 'ob' and traverse it.
273
        ob = self.publication.getApplication(self)
24.1.2 by Gary Poster
a broken but "complete" commit of the first draft. names are all changed, setup.py and buildout.cfg are on the right track hopefully, and we have all the files we need, to the best of mu knowledge. Only a couple of docs have been converted to ReST, and there is no intro doc.
274
        result = super(WebServiceRequestTraversal, self).traverse(ob)
275
        return self.publication.getResource(self, result)
276
277
    def _removeVirtualHostTraversals(self):
278
        """Remove the /[path_override] and /[version] traversal names."""
279
        names = list()
280
        config = getUtility(IWebServiceConfiguration)
48.1.1 by Leonard Richardson
Initial implementation.
281
        if config.path_override is not None:
282
            api = self._popTraversal(config.path_override)
283
            if api is not None:
284
                names.append(api)
285
                # Requests that use the webservice path override are
286
                # usually made by web browsers. Mark this request as one
287
                # initiated by a web browser, for the sake of
288
                # optimizations later in the request lifecycle.
289
                alsoProvides(self, IWebBrowserInitiatedRequest)
24.1.2 by Gary Poster
a broken but "complete" commit of the first draft. names are all changed, setup.py and buildout.cfg are on the right track hopefully, and we have all the files we need, to the best of mu knowledge. Only a couple of docs have been converted to ReST, and there is no intro doc.
290
110.1.1 by Leonard Richardson
Removed the latest_version_uri_prefix. If you want a floating dev version, put it at the end of active_versions.
291
        # Only accept versioned URLs. Any of the active_versions is
90.1.5 by Leonard Richardson
Initial implementation.
292
        # acceptable.
90.1.1 by Leonard Richardson
Initial implementation.
293
        version = None
110.1.1 by Leonard Richardson
Removed the latest_version_uri_prefix. If you want a floating dev version, put it at the end of active_versions.
294
        for version_string in config.active_versions:
90.1.1 by Leonard Richardson
Initial implementation.
295
            if version_string is not None:
296
                version = self._popTraversal(version_string)
297
                if version is not None:
298
                    names.append(version)
299
                    self.setVirtualHostRoot(names=names)
300
                    break
301
        if version is None:
302
            raise NotFound(self, '', self)
117.1.1 by Leonard Richardson
Refactored the code for tagging a request object with version information, and used it consistently.
303
        tag_request_with_version_name(self, version)
93.1.3 by Leonard Richardson
Initial implementation.
304
92.1.10 by Leonard Richardson
Got tests to pass.
305
        # Find the appropriate service root for this version and set
306
        # the publication's application appropriately.
307
        try:
308
            # First, try to find a version-specific service root.
93.1.2 by Leonard Richardson
Got multiversion.txt to work with a version-specific request interface.
309
            service_root = getUtility(IServiceRootResource, name=self.version)
92.1.10 by Leonard Richardson
Got tests to pass.
310
        except ComponentLookupError:
311
            # Next, try a version-independent service root.
312
            service_root = getUtility(IServiceRootResource)
313
        self.publication.application = service_root
314
48.1.1 by Leonard Richardson
Initial implementation.
315
    def _popTraversal(self, name=None):
24.1.2 by Gary Poster
a broken but "complete" commit of the first draft. names are all changed, setup.py and buildout.cfg are on the right track hopefully, and we have all the files we need, to the best of mu knowledge. Only a couple of docs have been converted to ReST, and there is no intro doc.
316
        """Remove a name from the traversal stack, if it is present.
317
48.1.1 by Leonard Richardson
Initial implementation.
318
        :name: The string to look for in the stack, or None to accept
319
        any string.
320
24.1.2 by Gary Poster
a broken but "complete" commit of the first draft. names are all changed, setup.py and buildout.cfg are on the right track hopefully, and we have all the files we need, to the best of mu knowledge. Only a couple of docs have been converted to ReST, and there is no intro doc.
321
        :return: The name of the element removed, or None if the stack
322
            wasn't changed.
323
        """
324
        stack = self.getTraversalStack()
48.1.1 by Leonard Richardson
Initial implementation.
325
        if len(stack) > 0 and (name is None or stack[-1] == name):
24.1.2 by Gary Poster
a broken but "complete" commit of the first draft. names are all changed, setup.py and buildout.cfg are on the right track hopefully, and we have all the files we need, to the best of mu knowledge. Only a couple of docs have been converted to ReST, and there is no intro doc.
326
            item = stack.pop()
327
            self.setTraversalStack(stack)
328
            return item
329
        return None
330
331
332
@implementer(IWebServiceClientRequest)
333
@adapter(IBrowserRequest)
90.1.5 by Leonard Richardson
Initial implementation.
334
def browser_request_to_web_service_request(
335
    website_request, web_service_version=None):
24.1.2 by Gary Poster
a broken but "complete" commit of the first draft. names are all changed, setup.py and buildout.cfg are on the right track hopefully, and we have all the files we need, to the best of mu knowledge. Only a couple of docs have been converted to ReST, and there is no intro doc.
336
    """An adapter from a browser request to a web service request.
337
338
    Used to instantiate Resource objects when handling normal web
339
    browser requests.
340
    """
341
    config = getUtility(IWebServiceConfiguration)
90.1.5 by Leonard Richardson
Initial implementation.
342
    if web_service_version is None:
343
        web_service_version = config.active_versions[-1]
344
134.5.26 by Leonard Richardson
The 'body' of a request is a stream, not a string.
345
    body = website_request.bodyStream.getCacheStream()
24.1.2 by Gary Poster
a broken but "complete" commit of the first draft. names are all changed, setup.py and buildout.cfg are on the right track hopefully, and we have all the files we need, to the best of mu knowledge. Only a couple of docs have been converted to ReST, and there is no intro doc.
346
    environ = dict(website_request.environment)
347
    # Zope picks up on SERVER_URL when setting the _app_server attribute
348
    # of the new request.
349
    environ['SERVER_URL'] = website_request.getApplicationURL()
350
    web_service_request = config.createRequest(body, environ)
351
    web_service_request.setVirtualHostRoot(
90.1.5 by Leonard Richardson
Initial implementation.
352
        names=[config.path_override, web_service_version])
117.1.1 by Leonard Richardson
Refactored the code for tagging a request object with version information, and used it consistently.
353
    tag_request_with_version_name(web_service_request, web_service_version)
24.1.2 by Gary Poster
a broken but "complete" commit of the first draft. names are all changed, setup.py and buildout.cfg are on the right track hopefully, and we have all the files we need, to the best of mu knowledge. Only a couple of docs have been converted to ReST, and there is no intro doc.
354
    web_service_request._vh_root = website_request.getVirtualHostRoot()
355
    return web_service_request