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
1=== modified file 'src/lazr/restful/directives/__init__.py'
2--- src/lazr/restful/directives/__init__.py 2009-09-03 17:31:27 +0000
3+++ src/lazr/restful/directives/__init__.py 2009-11-19 16:50:21 +0000
4@@ -69,8 +69,12 @@
5 def createRequest(self, body_instream, environ):
6 """See `IWebServiceConfiguration`."""
7 request = request_class(body_instream, environ)
8- service_root_object = getUtility(IServiceRootResource)
9- request.setPublication(publication_class(service_root_object))
10+ # Once we traverse the URL a bit and find the
11+ # requested web service version, we'll be able to set
12+ # the application to the appropriate service root
13+ # resource. This happens in
14+ # WebServiceRequestTraversal._removeVirtualHostTraversals().
15+ request.setPublication(publication_class(None))
16 return request
17 cls.createRequest = createRequest
18
19
20=== modified file 'src/lazr/restful/docs/multiversion.txt'
21--- src/lazr/restful/docs/multiversion.txt 2009-11-18 17:41:31 +0000
22+++ src/lazr/restful/docs/multiversion.txt 2009-11-19 16:50:21 +0000
23@@ -100,44 +100,6 @@
24 >>> C2 = Contact("Oliver Bluth", "10-1000000", "22-2222222")
25 >>> CONTACTS = [C1, C2]
26
27-Web service infrastructure initialization
28-=========================================
29-
30-The lazr.restful package contains a set of default adapters and
31-definitions to implement the web service.
32-
33- >>> from zope.configuration import xmlconfig
34- >>> zcmlcontext = xmlconfig.string("""
35- ... <configure xmlns="http://namespaces.zope.org/zope">
36- ... <include package="lazr.restful" file="basic-site.zcml"/>
37- ... <utility
38- ... factory="lazr.restful.example.base.filemanager.FileManager" />
39- ... </configure>
40- ... """)
41-
42-A IWebServiceConfiguration utility is also expected to be defined which
43-defines common configuration option for the webservice.
44-
45- >>> from lazr.restful import directives
46- >>> from lazr.restful.interfaces import IWebServiceConfiguration
47- >>> from lazr.restful.simple import BaseWebServiceConfiguration
48- >>> from lazr.restful.testing.webservice import WebServiceTestPublication
49-
50- >>> class WebServiceConfiguration(BaseWebServiceConfiguration):
51- ... use_https = False
52- ... active_versions = ['beta', '1.0']
53- ... latest_version_uri_prefix = 'dev'
54- ... code_revision = 'test'
55- ... max_batch_size = 100
56- ... directives.publication_class(WebServiceTestPublication)
57-
58- >>> from grokcore.component.testing import grok_component
59- >>> ignore = grok_component(
60- ... 'WebServiceConfiguration', WebServiceConfiguration)
61-
62- >>> from zope.component import getUtility
63- >>> webservice_configuration = getUtility(IWebServiceConfiguration)
64-
65 Defining the web service data model
66 ===================================
67
68@@ -336,3 +298,102 @@
69 >>> dev_collection = getAdapter(contact_set, ICollection, name="dev")
70 >>> len(dev_collection.find())
71 2
72+
73+Web service infrastructure initialization
74+=========================================
75+
76+Now that we've defined the data model, it's time to set up the web
77+service infrastructure.
78+
79+ >>> from zope.configuration import xmlconfig
80+ >>> zcmlcontext = xmlconfig.string("""
81+ ... <configure xmlns="http://namespaces.zope.org/zope">
82+ ... <include package="lazr.restful" file="basic-site.zcml"/>
83+ ... <utility
84+ ... factory="lazr.restful.example.base.filemanager.FileManager" />
85+ ... </configure>
86+ ... """)
87+
88+Here's the configuration, which defines the three versions: 'beta',
89+'1.0', and 'dev'.
90+
91+ >>> from lazr.restful import directives
92+ >>> from lazr.restful.interfaces import IWebServiceConfiguration
93+ >>> from lazr.restful.simple import BaseWebServiceConfiguration
94+ >>> from lazr.restful.testing.webservice import WebServiceTestPublication
95+
96+ >>> class WebServiceConfiguration(BaseWebServiceConfiguration):
97+ ... hostname = 'api.multiversion.dev'
98+ ... use_https = False
99+ ... active_versions = ['beta', '1.0']
100+ ... latest_version_uri_prefix = 'dev'
101+ ... code_revision = 'test'
102+ ... max_batch_size = 100
103+ ... directives.publication_class(WebServiceTestPublication)
104+
105+ >>> from grokcore.component.testing import grok_component
106+ >>> ignore = grok_component(
107+ ... 'WebServiceConfiguration', WebServiceConfiguration)
108+
109+ >>> from zope.component import getUtility
110+ >>> config = getUtility(IWebServiceConfiguration)
111+
112+
113+The service root resource
114+=========================
115+
116+To make things more interesting we'll define two distinct service
117+roots. The 'beta' web service will publish the contact set as
118+'contact_list', and subsequent versions will publish it as 'contacts'.
119+
120+ >>> from lazr.restful.interfaces import IServiceRootResource
121+ >>> from lazr.restful.simple import RootResource
122+ >>> from zope.traversing.browser.interfaces import IAbsoluteURL
123+
124+ >>> class BetaServiceRootResource(RootResource):
125+ ... implements(IAbsoluteURL)
126+ ...
127+ ... top_level_objects = { 'contact_list': ContactSet() }
128+
129+ >>> class PostBetaServiceRootResource(RootResource):
130+ ... implements(IAbsoluteURL)
131+ ...
132+ ... top_level_objects = { 'contacts': ContactSet() }
133+
134+ >>> for version, cls in (('beta', BetaServiceRootResource),
135+ ... ('1.0', PostBetaServiceRootResource),
136+ ... ('dev', PostBetaServiceRootResource)):
137+ ... app = cls()
138+ ... sm.registerUtility(app, IServiceRootResource, name=version)
139+
140+ >>> beta_app = getUtility(IServiceRootResource, 'beta')
141+ >>> dev_app = getUtility(IServiceRootResource, 'dev')
142+
143+ >>> beta_app.top_level_names
144+ ['contact_list']
145+
146+ >>> dev_app.top_level_names
147+ ['contacts']
148+
149+Both classes will use the default lazr.restful code to generate their
150+URLs.
151+
152+ >>> from lazr.restful.simple import RootResourceAbsoluteURL
153+ >>> for cls in (BetaServiceRootResource, PostBetaServiceRootResource):
154+ ... sm.registerAdapter(
155+ ... RootResourceAbsoluteURL, [cls, IBrowserRequest])
156+
157+ >>> from zope.traversing.browser import absoluteURL
158+ >>> from lazr.restful.testing.webservice import (
159+ ... create_web_service_request)
160+
161+ >>> beta_request = create_web_service_request('/beta/')
162+ >>> ignore = beta_request.traverse(None)
163+ >>> print absoluteURL(beta_app, beta_request)
164+ http://api.multiversion.dev/beta/
165+
166+ >>> dev_request = create_web_service_request('/dev/')
167+ >>> ignore = dev_request.traverse(None)
168+ >>> print absoluteURL(dev_app, dev_request)
169+ http://api.multiversion.dev/dev/
170+
171
172=== modified file 'src/lazr/restful/docs/webservice.txt'
173--- src/lazr/restful/docs/webservice.txt 2009-11-18 17:41:31 +0000
174+++ src/lazr/restful/docs/webservice.txt 2009-11-19 16:50:21 +0000
175@@ -505,6 +505,7 @@
176 >>> from lazr.restful.testing.webservice import WebServiceTestPublication
177
178 >>> class WebServiceConfiguration(BaseWebServiceConfiguration):
179+ ... hostname = 'api.cookbooks.dev'
180 ... use_https = False
181 ... active_versions = ['beta']
182 ... latest_version_uri_prefix = 'devel'
183@@ -957,30 +958,9 @@
184 will act as though you had performed a GET on the URL
185 'http://api.cookbooks.dev/beta/'.
186
187- >>> from cStringIO import StringIO
188 >>> webservice_configuration.root = app
189- >>> def create_web_service_request(
190- ... path_info, method='GET', body=None, environ=None,
191- ... http_host='api.cookbooks.dev'):
192- ... test_environ = {
193- ... 'SERVER_URL': 'http://%s' % http_host,
194- ... 'HTTP_HOST': http_host,
195- ... 'PATH_INFO': path_info,
196- ... 'REQUEST_METHOD': method,
197- ... }
198- ... if environ is not None:
199- ... test_environ.update(environ)
200- ... if body is None:
201- ... body_instream = StringIO('')
202- ... else:
203- ... test_environ['CONTENT_LENGTH'] = len(body)
204- ... body_instream = StringIO(body)
205- ... request = webservice_configuration.createRequest(
206- ... body_instream, test_environ)
207- ... request.processInputs()
208- ... # This sets the request as the current interaction.
209- ... request.publication.beforeTraversal(request)
210- ... return request
211+ >>> from lazr.restful.testing.webservice import (
212+ ... create_web_service_request)
213
214 >>> request = create_web_service_request('/beta/')
215 >>> ignore = request.traverse(app)
216@@ -1397,7 +1377,8 @@
217 ... return self.result
218
219 >>> def make_dummy_operation_request(result):
220- ... request = create_web_service_request('/')
221+ ... request = create_web_service_request('/beta/')
222+ ... ignore = request.traverse(app)
223 ... operation = DummyOperation(None, request)
224 ... operation.result = result
225 ... return request, operation
226@@ -1880,7 +1861,7 @@
227 ... resource = create_web_service_request(
228 ... '/beta/cookbooks/The%20Joy%20of%20Cooking',
229 ... body=simplejson.dumps(representation), environ=headers,
230- ... method='PATCH', http_host=host).traverse(app)
231+ ... method='PATCH', hostname=host).traverse(app)
232 ... return resource()
233 >>> path = '/beta/authors/Julia%20Child'
234
235
236=== modified file 'src/lazr/restful/publisher.py'
237--- src/lazr/restful/publisher.py 2009-11-12 19:08:10 +0000
238+++ src/lazr/restful/publisher.py 2009-11-19 16:50:21 +0000
239@@ -20,6 +20,7 @@
240
241 from zope.component import (
242 adapter, getMultiAdapter, getUtility, queryAdapter, queryMultiAdapter)
243+from zope.component.interfaces import ComponentLookupError
244 from zope.interface import alsoProvides, implementer, implements
245 from zope.publisher.interfaces import NotFound
246 from zope.publisher.interfaces.browser import IBrowserRequest
247@@ -33,7 +34,7 @@
248 EntryResource, ScopedCollection, ServiceRootResource)
249 from lazr.restful.interfaces import (
250 IByteStorage, ICollection, ICollectionField, IEntry, IEntryField,
251- IHTTPResource, IWebBrowserInitiatedRequest,
252+ IHTTPResource, IServiceRootResource, IWebBrowserInitiatedRequest,
253 IWebServiceClientRequest, IWebServiceConfiguration)
254
255
256@@ -212,6 +213,14 @@
257 on the result of the base class's traversal.
258 """
259 self._removeVirtualHostTraversals()
260+
261+ # We don't trust the value of 'ob' passed in (it's probably
262+ # None) because the publication depends on which version of
263+ # the web service was requested.
264+ # _removeVirtualHostTraversals() has determined which version
265+ # was requested and has set the application appropriately, so
266+ # now we can get a good value for 'ob' and traverse it.
267+ ob = self.publication.getApplication(self)
268 result = super(WebServiceRequestTraversal, self).traverse(ob)
269 return self.publication.getResource(self, result)
270
271@@ -246,6 +255,16 @@
272 raise NotFound(self, '', self)
273 self.annotations[self.VERSION_ANNOTATION] = version
274
275+ # Find the appropriate service root for this version and set
276+ # the publication's application appropriately.
277+ try:
278+ # First, try to find a version-specific service root.
279+ service_root = getUtility(IServiceRootResource, name=version)
280+ except ComponentLookupError:
281+ # Next, try a version-independent service root.
282+ service_root = getUtility(IServiceRootResource)
283+ self.publication.application = service_root
284+
285 def _popTraversal(self, name=None):
286 """Remove a name from the traversal stack, if it is present.
287
288
289=== modified file 'src/lazr/restful/testing/webservice.py'
290--- src/lazr/restful/testing/webservice.py 2009-11-12 19:08:10 +0000
291+++ src/lazr/restful/testing/webservice.py 2009-11-19 16:50:21 +0000
292@@ -4,6 +4,7 @@
293
294 __metaclass__ = type
295 __all__ = [
296+ 'create_web_service_request',
297 'ExampleWebServiceTestCaller',
298 'ExampleWebServicePublication',
299 'FakeRequest',
300@@ -14,6 +15,7 @@
301 'TestPublication',
302 ]
303
304+from cStringIO import StringIO
305 import os
306 import traceback
307 import simplejson
308@@ -40,6 +42,41 @@
309 from lazr.uri import URI
310
311
312+def create_web_service_request(path_info, method='GET', body=None,
313+ environ=None, hostname=None):
314+ """Create a web service request object with the given parameters.
315+
316+ :param path_info: The path portion of the requested URL.
317+ :hostname: The hostname portion of the requested URL. Defaults to
318+ the value specified in the configuration object.
319+ :param method: The HTTP method to use.
320+ :body: The HTTP entity-body to submit.
321+ :environ: The environment of the created request.
322+ """
323+ config = getUtility(IWebServiceConfiguration)
324+ if hostname is None:
325+ hostname = config.hostname
326+ test_environ = {
327+ 'SERVER_URL': 'http://%s' % hostname,
328+ 'HTTP_HOST': hostname,
329+ 'PATH_INFO': path_info,
330+ 'REQUEST_METHOD': method,
331+ }
332+ if environ is not None:
333+ test_environ.update(environ)
334+ if body is None:
335+ body_instream = StringIO('')
336+ else:
337+ test_environ['CONTENT_LENGTH'] = len(body)
338+ body_instream = StringIO(body)
339+ request = config.createRequest(body_instream, test_environ)
340+
341+ request.processInputs()
342+ # This sets the request as the current interaction.
343+ request.publication.beforeTraversal(request)
344+ return request
345+
346+
347 class FakeResponse:
348 """Simple response wrapper object."""
349 def __init__(self):

Subscribers

People subscribed via source and target branches