Merge lp:~leonardr/lazr.restful/double-your-enjoyment-2 into lp:lazr.restful

Proposed by Leonard Richardson
Status: Merged
Approved by: Eleanor Berger
Approved revision: not available
Merged at revision: not available
Proposed branch: lp:~leonardr/lazr.restful/double-your-enjoyment-2
Merge into: lp:lazr.restful
Diff against target: 349 lines (+168/-66)
5 files modified
src/lazr/restful/directives/__init__.py (+6/-2)
src/lazr/restful/docs/multiversion.txt (+99/-38)
src/lazr/restful/docs/webservice.txt (+6/-25)
src/lazr/restful/publisher.py (+20/-1)
src/lazr/restful/testing/webservice.py (+37/-0)
To merge this branch: bzr merge lp:~leonardr/lazr.restful/double-your-enjoyment-2
Reviewer Review Type Date Requested Status
Eleanor Berger (community) Approve
Review via email: mp+15045@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Leonard Richardson (leonardr) wrote :

This branch adds the first bit of multi-versioned web service code, by making it possible to register different objects as the IServiceRootResource implementation for different versions. I illustrate in multiversion.txt by registering different root objects for the 'beta', '1.0', and 'dev' versions of that web service.

If you don't have multiple web service versions, you can still register your root resource object as the IServiceRootResource utility (without providing a name), and it will always be picked up. You don't have to register the same object separately for every version.

Previously, the 'publication' object was always the IServiceRootResource utility. But now there can be more than one IServiceRootResource utility. So we start off by setting the publication to None, and after doing a little hard-coded traversal to determine which version the client requested, we set the publication to the _appropriate_ IServiceRootResource utility and traverse from there.

I refactored the create_web_service_request() function from webservice.txt so that I could also use it in multiversion.txt.

Revision history for this message
Eleanor Berger (intellectronica) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'src/lazr/restful/directives/__init__.py'
--- src/lazr/restful/directives/__init__.py 2009-09-03 17:31:27 +0000
+++ src/lazr/restful/directives/__init__.py 2009-11-19 16:50:21 +0000
@@ -69,8 +69,12 @@
69 def createRequest(self, body_instream, environ):69 def createRequest(self, body_instream, environ):
70 """See `IWebServiceConfiguration`."""70 """See `IWebServiceConfiguration`."""
71 request = request_class(body_instream, environ)71 request = request_class(body_instream, environ)
72 service_root_object = getUtility(IServiceRootResource)72 # Once we traverse the URL a bit and find the
73 request.setPublication(publication_class(service_root_object))73 # requested web service version, we'll be able to set
74 # the application to the appropriate service root
75 # resource. This happens in
76 # WebServiceRequestTraversal._removeVirtualHostTraversals().
77 request.setPublication(publication_class(None))
74 return request78 return request
75 cls.createRequest = createRequest79 cls.createRequest = createRequest
7680
7781
=== modified file 'src/lazr/restful/docs/multiversion.txt'
--- src/lazr/restful/docs/multiversion.txt 2009-11-18 17:41:31 +0000
+++ src/lazr/restful/docs/multiversion.txt 2009-11-19 16:50:21 +0000
@@ -100,44 +100,6 @@
100 >>> C2 = Contact("Oliver Bluth", "10-1000000", "22-2222222")100 >>> C2 = Contact("Oliver Bluth", "10-1000000", "22-2222222")
101 >>> CONTACTS = [C1, C2]101 >>> CONTACTS = [C1, C2]
102102
103Web service infrastructure initialization
104=========================================
105
106The lazr.restful package contains a set of default adapters and
107definitions to implement the web service.
108
109 >>> from zope.configuration import xmlconfig
110 >>> zcmlcontext = xmlconfig.string("""
111 ... <configure xmlns="http://namespaces.zope.org/zope">
112 ... <include package="lazr.restful" file="basic-site.zcml"/>
113 ... <utility
114 ... factory="lazr.restful.example.base.filemanager.FileManager" />
115 ... </configure>
116 ... """)
117
118A IWebServiceConfiguration utility is also expected to be defined which
119defines common configuration option for the webservice.
120
121 >>> from lazr.restful import directives
122 >>> from lazr.restful.interfaces import IWebServiceConfiguration
123 >>> from lazr.restful.simple import BaseWebServiceConfiguration
124 >>> from lazr.restful.testing.webservice import WebServiceTestPublication
125
126 >>> class WebServiceConfiguration(BaseWebServiceConfiguration):
127 ... use_https = False
128 ... active_versions = ['beta', '1.0']
129 ... latest_version_uri_prefix = 'dev'
130 ... code_revision = 'test'
131 ... max_batch_size = 100
132 ... directives.publication_class(WebServiceTestPublication)
133
134 >>> from grokcore.component.testing import grok_component
135 >>> ignore = grok_component(
136 ... 'WebServiceConfiguration', WebServiceConfiguration)
137
138 >>> from zope.component import getUtility
139 >>> webservice_configuration = getUtility(IWebServiceConfiguration)
140
141Defining the web service data model103Defining the web service data model
142===================================104===================================
143105
@@ -336,3 +298,102 @@
336 >>> dev_collection = getAdapter(contact_set, ICollection, name="dev")298 >>> dev_collection = getAdapter(contact_set, ICollection, name="dev")
337 >>> len(dev_collection.find())299 >>> len(dev_collection.find())
338 2300 2
301
302Web service infrastructure initialization
303=========================================
304
305Now that we've defined the data model, it's time to set up the web
306service infrastructure.
307
308 >>> from zope.configuration import xmlconfig
309 >>> zcmlcontext = xmlconfig.string("""
310 ... <configure xmlns="http://namespaces.zope.org/zope">
311 ... <include package="lazr.restful" file="basic-site.zcml"/>
312 ... <utility
313 ... factory="lazr.restful.example.base.filemanager.FileManager" />
314 ... </configure>
315 ... """)
316
317Here's the configuration, which defines the three versions: 'beta',
318'1.0', and 'dev'.
319
320 >>> from lazr.restful import directives
321 >>> from lazr.restful.interfaces import IWebServiceConfiguration
322 >>> from lazr.restful.simple import BaseWebServiceConfiguration
323 >>> from lazr.restful.testing.webservice import WebServiceTestPublication
324
325 >>> class WebServiceConfiguration(BaseWebServiceConfiguration):
326 ... hostname = 'api.multiversion.dev'
327 ... use_https = False
328 ... active_versions = ['beta', '1.0']
329 ... latest_version_uri_prefix = 'dev'
330 ... code_revision = 'test'
331 ... max_batch_size = 100
332 ... directives.publication_class(WebServiceTestPublication)
333
334 >>> from grokcore.component.testing import grok_component
335 >>> ignore = grok_component(
336 ... 'WebServiceConfiguration', WebServiceConfiguration)
337
338 >>> from zope.component import getUtility
339 >>> config = getUtility(IWebServiceConfiguration)
340
341
342The service root resource
343=========================
344
345To make things more interesting we'll define two distinct service
346roots. The 'beta' web service will publish the contact set as
347'contact_list', and subsequent versions will publish it as 'contacts'.
348
349 >>> from lazr.restful.interfaces import IServiceRootResource
350 >>> from lazr.restful.simple import RootResource
351 >>> from zope.traversing.browser.interfaces import IAbsoluteURL
352
353 >>> class BetaServiceRootResource(RootResource):
354 ... implements(IAbsoluteURL)
355 ...
356 ... top_level_objects = { 'contact_list': ContactSet() }
357
358 >>> class PostBetaServiceRootResource(RootResource):
359 ... implements(IAbsoluteURL)
360 ...
361 ... top_level_objects = { 'contacts': ContactSet() }
362
363 >>> for version, cls in (('beta', BetaServiceRootResource),
364 ... ('1.0', PostBetaServiceRootResource),
365 ... ('dev', PostBetaServiceRootResource)):
366 ... app = cls()
367 ... sm.registerUtility(app, IServiceRootResource, name=version)
368
369 >>> beta_app = getUtility(IServiceRootResource, 'beta')
370 >>> dev_app = getUtility(IServiceRootResource, 'dev')
371
372 >>> beta_app.top_level_names
373 ['contact_list']
374
375 >>> dev_app.top_level_names
376 ['contacts']
377
378Both classes will use the default lazr.restful code to generate their
379URLs.
380
381 >>> from lazr.restful.simple import RootResourceAbsoluteURL
382 >>> for cls in (BetaServiceRootResource, PostBetaServiceRootResource):
383 ... sm.registerAdapter(
384 ... RootResourceAbsoluteURL, [cls, IBrowserRequest])
385
386 >>> from zope.traversing.browser import absoluteURL
387 >>> from lazr.restful.testing.webservice import (
388 ... create_web_service_request)
389
390 >>> beta_request = create_web_service_request('/beta/')
391 >>> ignore = beta_request.traverse(None)
392 >>> print absoluteURL(beta_app, beta_request)
393 http://api.multiversion.dev/beta/
394
395 >>> dev_request = create_web_service_request('/dev/')
396 >>> ignore = dev_request.traverse(None)
397 >>> print absoluteURL(dev_app, dev_request)
398 http://api.multiversion.dev/dev/
399
339400
=== modified file 'src/lazr/restful/docs/webservice.txt'
--- src/lazr/restful/docs/webservice.txt 2009-11-18 17:41:31 +0000
+++ src/lazr/restful/docs/webservice.txt 2009-11-19 16:50:21 +0000
@@ -505,6 +505,7 @@
505 >>> from lazr.restful.testing.webservice import WebServiceTestPublication505 >>> from lazr.restful.testing.webservice import WebServiceTestPublication
506506
507 >>> class WebServiceConfiguration(BaseWebServiceConfiguration):507 >>> class WebServiceConfiguration(BaseWebServiceConfiguration):
508 ... hostname = 'api.cookbooks.dev'
508 ... use_https = False509 ... use_https = False
509 ... active_versions = ['beta']510 ... active_versions = ['beta']
510 ... latest_version_uri_prefix = 'devel'511 ... latest_version_uri_prefix = 'devel'
@@ -957,30 +958,9 @@
957will act as though you had performed a GET on the URL958will act as though you had performed a GET on the URL
958'http://api.cookbooks.dev/beta/'.959'http://api.cookbooks.dev/beta/'.
959960
960 >>> from cStringIO import StringIO
961 >>> webservice_configuration.root = app961 >>> webservice_configuration.root = app
962 >>> def create_web_service_request(962 >>> from lazr.restful.testing.webservice import (
963 ... path_info, method='GET', body=None, environ=None,963 ... create_web_service_request)
964 ... http_host='api.cookbooks.dev'):
965 ... test_environ = {
966 ... 'SERVER_URL': 'http://%s' % http_host,
967 ... 'HTTP_HOST': http_host,
968 ... 'PATH_INFO': path_info,
969 ... 'REQUEST_METHOD': method,
970 ... }
971 ... if environ is not None:
972 ... test_environ.update(environ)
973 ... if body is None:
974 ... body_instream = StringIO('')
975 ... else:
976 ... test_environ['CONTENT_LENGTH'] = len(body)
977 ... body_instream = StringIO(body)
978 ... request = webservice_configuration.createRequest(
979 ... body_instream, test_environ)
980 ... request.processInputs()
981 ... # This sets the request as the current interaction.
982 ... request.publication.beforeTraversal(request)
983 ... return request
984964
985 >>> request = create_web_service_request('/beta/')965 >>> request = create_web_service_request('/beta/')
986 >>> ignore = request.traverse(app)966 >>> ignore = request.traverse(app)
@@ -1397,7 +1377,8 @@
1397 ... return self.result1377 ... return self.result
13981378
1399 >>> def make_dummy_operation_request(result):1379 >>> def make_dummy_operation_request(result):
1400 ... request = create_web_service_request('/')1380 ... request = create_web_service_request('/beta/')
1381 ... ignore = request.traverse(app)
1401 ... operation = DummyOperation(None, request)1382 ... operation = DummyOperation(None, request)
1402 ... operation.result = result1383 ... operation.result = result
1403 ... return request, operation1384 ... return request, operation
@@ -1880,7 +1861,7 @@
1880 ... resource = create_web_service_request(1861 ... resource = create_web_service_request(
1881 ... '/beta/cookbooks/The%20Joy%20of%20Cooking',1862 ... '/beta/cookbooks/The%20Joy%20of%20Cooking',
1882 ... body=simplejson.dumps(representation), environ=headers,1863 ... body=simplejson.dumps(representation), environ=headers,
1883 ... method='PATCH', http_host=host).traverse(app)1864 ... method='PATCH', hostname=host).traverse(app)
1884 ... return resource()1865 ... return resource()
1885 >>> path = '/beta/authors/Julia%20Child'1866 >>> path = '/beta/authors/Julia%20Child'
18861867
18871868
=== modified file 'src/lazr/restful/publisher.py'
--- src/lazr/restful/publisher.py 2009-11-12 19:08:10 +0000
+++ src/lazr/restful/publisher.py 2009-11-19 16:50:21 +0000
@@ -20,6 +20,7 @@
2020
21from zope.component import (21from zope.component import (
22 adapter, getMultiAdapter, getUtility, queryAdapter, queryMultiAdapter)22 adapter, getMultiAdapter, getUtility, queryAdapter, queryMultiAdapter)
23from zope.component.interfaces import ComponentLookupError
23from zope.interface import alsoProvides, implementer, implements24from zope.interface import alsoProvides, implementer, implements
24from zope.publisher.interfaces import NotFound25from zope.publisher.interfaces import NotFound
25from zope.publisher.interfaces.browser import IBrowserRequest26from zope.publisher.interfaces.browser import IBrowserRequest
@@ -33,7 +34,7 @@
33 EntryResource, ScopedCollection, ServiceRootResource)34 EntryResource, ScopedCollection, ServiceRootResource)
34from lazr.restful.interfaces import (35from lazr.restful.interfaces import (
35 IByteStorage, ICollection, ICollectionField, IEntry, IEntryField,36 IByteStorage, ICollection, ICollectionField, IEntry, IEntryField,
36 IHTTPResource, IWebBrowserInitiatedRequest,37 IHTTPResource, IServiceRootResource, IWebBrowserInitiatedRequest,
37 IWebServiceClientRequest, IWebServiceConfiguration)38 IWebServiceClientRequest, IWebServiceConfiguration)
3839
3940
@@ -212,6 +213,14 @@
212 on the result of the base class's traversal.213 on the result of the base class's traversal.
213 """214 """
214 self._removeVirtualHostTraversals()215 self._removeVirtualHostTraversals()
216
217 # We don't trust the value of 'ob' passed in (it's probably
218 # None) because the publication depends on which version of
219 # the web service was requested.
220 # _removeVirtualHostTraversals() has determined which version
221 # was requested and has set the application appropriately, so
222 # now we can get a good value for 'ob' and traverse it.
223 ob = self.publication.getApplication(self)
215 result = super(WebServiceRequestTraversal, self).traverse(ob)224 result = super(WebServiceRequestTraversal, self).traverse(ob)
216 return self.publication.getResource(self, result)225 return self.publication.getResource(self, result)
217226
@@ -246,6 +255,16 @@
246 raise NotFound(self, '', self)255 raise NotFound(self, '', self)
247 self.annotations[self.VERSION_ANNOTATION] = version256 self.annotations[self.VERSION_ANNOTATION] = version
248257
258 # Find the appropriate service root for this version and set
259 # the publication's application appropriately.
260 try:
261 # First, try to find a version-specific service root.
262 service_root = getUtility(IServiceRootResource, name=version)
263 except ComponentLookupError:
264 # Next, try a version-independent service root.
265 service_root = getUtility(IServiceRootResource)
266 self.publication.application = service_root
267
249 def _popTraversal(self, name=None):268 def _popTraversal(self, name=None):
250 """Remove a name from the traversal stack, if it is present.269 """Remove a name from the traversal stack, if it is present.
251270
252271
=== modified file 'src/lazr/restful/testing/webservice.py'
--- src/lazr/restful/testing/webservice.py 2009-11-12 19:08:10 +0000
+++ src/lazr/restful/testing/webservice.py 2009-11-19 16:50:21 +0000
@@ -4,6 +4,7 @@
44
5__metaclass__ = type5__metaclass__ = type
6__all__ = [6__all__ = [
7 'create_web_service_request',
7 'ExampleWebServiceTestCaller',8 'ExampleWebServiceTestCaller',
8 'ExampleWebServicePublication',9 'ExampleWebServicePublication',
9 'FakeRequest',10 'FakeRequest',
@@ -14,6 +15,7 @@
14 'TestPublication',15 'TestPublication',
15 ]16 ]
1617
18from cStringIO import StringIO
17import os19import os
18import traceback20import traceback
19import simplejson21import simplejson
@@ -40,6 +42,41 @@
40from lazr.uri import URI42from lazr.uri import URI
4143
4244
45def create_web_service_request(path_info, method='GET', body=None,
46 environ=None, hostname=None):
47 """Create a web service request object with the given parameters.
48
49 :param path_info: The path portion of the requested URL.
50 :hostname: The hostname portion of the requested URL. Defaults to
51 the value specified in the configuration object.
52 :param method: The HTTP method to use.
53 :body: The HTTP entity-body to submit.
54 :environ: The environment of the created request.
55 """
56 config = getUtility(IWebServiceConfiguration)
57 if hostname is None:
58 hostname = config.hostname
59 test_environ = {
60 'SERVER_URL': 'http://%s' % hostname,
61 'HTTP_HOST': hostname,
62 'PATH_INFO': path_info,
63 'REQUEST_METHOD': method,
64 }
65 if environ is not None:
66 test_environ.update(environ)
67 if body is None:
68 body_instream = StringIO('')
69 else:
70 test_environ['CONTENT_LENGTH'] = len(body)
71 body_instream = StringIO(body)
72 request = config.createRequest(body_instream, test_environ)
73
74 request.processInputs()
75 # This sets the request as the current interaction.
76 request.publication.beforeTraversal(request)
77 return request
78
79
43class FakeResponse:80class FakeResponse:
44 """Simple response wrapper object."""81 """Simple response wrapper object."""
45 def __init__(self):82 def __init__(self):

Subscribers

People subscribed via source and target branches