Merge lp:~leonardr/lazr.restful/double-your-enjoyment-2 into lp:lazr.restful
- double-your-enjoyment-2
- Merge into trunk
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 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Eleanor Berger (community) | Approve | ||
Review via email: mp+15045@code.launchpad.net |
Commit message
Description of the change
To post a comment you must log in.
Revision history for this message
Leonard Richardson (leonardr) wrote : | # |
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): |
This branch adds the first bit of multi-versioned web service code, by making it possible to register different objects as the IServiceRootRes ource 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 IServiceRootRes ource 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 IServiceRootRes ource utility. But now there can be more than one IServiceRootRes ource 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_ IServiceRootRes ource 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.