Merge lp://staging/~leonardr/lazr.restful/version-specific-request-interface into lp://staging/lazr.restful
- version-specific-request-interface
- Merge into trunk
Status: | Work in progress |
---|---|
Proposed branch: | lp://staging/~leonardr/lazr.restful/version-specific-request-interface |
Merge into: | lp://staging/lazr.restful |
Diff against target: |
395 lines (+180/-76) 3 files modified
src/lazr/restful/docs/multiversion.txt (+146/-73) src/lazr/restful/interfaces/_rest.py (+17/-1) src/lazr/restful/publisher.py (+17/-2) |
To merge this branch: | bzr merge lp://staging/~leonardr/lazr.restful/version-specific-request-interface |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Francis J. Lacoste (community) | code | Approve | |
Review via email: mp+15172@code.staging.launchpad.net |
Commit message
Description of the change
Leonard Richardson (leonardr) wrote : | # |
Because this branch is complicated and I'm not yet certain it will pay off, I don't plan to land it immediately. Instead, I'll be merging other branches into it and landing the whole thing once I have something useful.
Francis J. Lacoste (flacoste) wrote : | # |
Hi Leonard,
Nice to see this coming along.
This is good to go once you fix the conflit marker and handle my few other
comments.
Cheers
> === modified file 'src/lazr/
> --- src/lazr/
> +++ src/lazr/
> @@ -5,10 +5,99 @@
> services. Typically these different services represent successive
> versions of a single web service, improved over time.
>
> +Setup
> +=====
> +
> +First, let's set up the web service infrastructure. Doing this first
> +will let us create HTTP requests for different versions of the web
> +service. The first step is to make all component lookups use the
> +global site manager.
> +
> + >>> from zope.component import getSiteManager
> + >>> sm = getSiteManager()
> +
> + >>> from zope.component import adapter
> + >>> from zope.component.
> + >>> from zope.interface import implementer, Interface
> + >>> @implementer(
> + ... @adapter(Interface)
> + ... def everything_
> + ... return sm
> + >>> sm.registerAdap
Like discussed on IRC, the waving of this dead chicken isn't needed :-)
> +
> +Defining the request interfaces
> +------
> +
> +Every version must have a corresponding subclass of
> +IWebServiceCli
> +keep track of which version of the web service a particular client
> +wants to use. In a real application, these interfaces will be
> +generated and registered automatically.
> +
> + >>> from lazr.restful.
> + ... IVersionedClien
> + >>> class IWebServiceRequ
> + ... pass
> +
> + >>> class IWebServiceRequ
> + ... pass
> +
> + >>> class IWebServiceRequ
> + ... pass
> +
> + >>> request_classes = [IWebServiceReq
> + ... IWebServiceRequ
> +
> + >>> from zope.interface import alsoProvides
> + >>> for cls, version in ((IWebServiceRe
> + ... (IWebServiceReq
> + ... (IWebServiceReq
> + ... alsoProvides(cls, IVersionedClien
> + ... sm.registerUtil
> + ... name=version)
> +
Can you explain the use of IVersionedClien
explained on IRC that it to make it easy to retrieve the interface related to
a version string.
Would IVersionedWebCl
> +<<<<<<< TREE
> >>> for version in ['beta', 'dev', '1.0']:
> ... sm.registerAdapter(
> ... ContactCollection, provided=
> @@ -397,3 +495,105 @@
> >>> print absoluteURL(
> http://
- 95. By Leonard Richardson
-
Merged from original branch to get rid of conflict markers.
Leonard Richardson (leonardr) wrote : | # |
I removed the rubber chicken and changed the text in the section on named utilities to make it more clear. I did this in the second branch because this branch has a lot of test failures (which the job of the second branch is to fix). The second branch already included a doctest of request.version, in the "Request lifecycle" section of multiversion.txt.
Unmerged revisions
- 95. By Leonard Richardson
-
Merged from original branch to get rid of conflict markers.
Preview Diff
1 | === modified file 'src/lazr/restful/docs/multiversion.txt' |
2 | --- src/lazr/restful/docs/multiversion.txt 2009-11-19 16:43:08 +0000 |
3 | +++ src/lazr/restful/docs/multiversion.txt 2010-01-06 21:06:12 +0000 |
4 | @@ -5,10 +5,96 @@ |
5 | services. Typically these different services represent successive |
6 | versions of a single web service, improved over time. |
7 | |
8 | +Setup |
9 | +===== |
10 | + |
11 | +First, let's set up the web service infrastructure. Doing this first |
12 | +will let us create HTTP requests for different versions of the web |
13 | +service. The first step is to make all component lookups use the |
14 | +global site manager. |
15 | + |
16 | + >>> from zope.component import getSiteManager |
17 | + >>> sm = getSiteManager() |
18 | + |
19 | + >>> from zope.component import adapter |
20 | + >>> from zope.component.interfaces import IComponentLookup |
21 | + >>> from zope.interface import implementer, Interface |
22 | + >>> @implementer(IComponentLookup) |
23 | + ... @adapter(Interface) |
24 | + ... def everything_uses_the_global_site_manager(context): |
25 | + ... return sm |
26 | + >>> sm.registerAdapter(everything_uses_the_global_site_manager) |
27 | + |
28 | +Then, let's install the common ZCML used by all lazr.restful web services. |
29 | + |
30 | + >>> from zope.configuration import xmlconfig |
31 | + >>> zcmlcontext = xmlconfig.string(""" |
32 | + ... <configure xmlns="http://namespaces.zope.org/zope"> |
33 | + ... <include package="lazr.restful" file="basic-site.zcml"/> |
34 | + ... <utility |
35 | + ... factory="lazr.restful.example.base.filemanager.FileManager" /> |
36 | + ... </configure> |
37 | + ... """) |
38 | + |
39 | +Web service configuration object |
40 | +-------------------------------- |
41 | + |
42 | +Here's the web service configuration, which defines the three |
43 | +versions: 'beta', '1.0', and 'dev'. |
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 | + ... hostname = 'api.multiversion.dev' |
52 | + ... use_https = False |
53 | + ... active_versions = ['beta', '1.0'] |
54 | + ... latest_version_uri_prefix = 'dev' |
55 | + ... code_revision = 'test' |
56 | + ... max_batch_size = 100 |
57 | + ... directives.publication_class(WebServiceTestPublication) |
58 | + |
59 | + >>> from grokcore.component.testing import grok_component |
60 | + >>> ignore = grok_component( |
61 | + ... 'WebServiceConfiguration', WebServiceConfiguration) |
62 | + |
63 | + >>> from zope.component import getUtility |
64 | + >>> config = getUtility(IWebServiceConfiguration) |
65 | + |
66 | +Defining the request interfaces |
67 | +------------------------------- |
68 | + |
69 | +Every version must have a corresponding subclass of |
70 | +IWebServiceClientRequest. These marker interfaces let lazr.restful |
71 | +keep track of which version of the web service a particular client |
72 | +wants to use. In a real application, these interfaces will be |
73 | +generated and registered automatically. |
74 | + |
75 | + >>> from lazr.restful.interfaces import ( |
76 | + ... IVersionedClientRequestImplementation, IWebServiceClientRequest) |
77 | + >>> class IWebServiceRequestBeta(IWebServiceClientRequest): |
78 | + ... pass |
79 | + |
80 | + >>> class IWebServiceRequest10(IWebServiceClientRequest): |
81 | + ... pass |
82 | + |
83 | + >>> class IWebServiceRequestDev(IWebServiceClientRequest): |
84 | + ... pass |
85 | + |
86 | + >>> from zope.interface import alsoProvides |
87 | + >>> for cls, version in ((IWebServiceRequestBeta, 'beta'), |
88 | + ... (IWebServiceRequest10, '1.0'), |
89 | + ... (IWebServiceRequestDev, 'dev')): |
90 | + ... alsoProvides(cls, IVersionedClientRequestImplementation) |
91 | + ... sm.registerUtility(cls, IVersionedClientRequestImplementation, |
92 | + ... name=version) |
93 | + |
94 | Example model objects |
95 | ===================== |
96 | |
97 | -First let's define the data model. The model in webservice.txt is |
98 | +Now let's define the data model. The model in webservice.txt is |
99 | pretty complicated; this model will be just complicated enough to |
100 | illustrate how to publish multiple versions of a web service. |
101 | |
102 | @@ -38,21 +124,6 @@ |
103 | ... def get(self, name): |
104 | ... "Retrieve a single contact by name." |
105 | |
106 | -Before we can define any classes, a bit of web service setup. Let's |
107 | -make all component lookups use the global site manager. |
108 | - |
109 | - >>> from zope.component import getSiteManager |
110 | - >>> sm = getSiteManager() |
111 | - |
112 | - >>> from zope.component import adapter |
113 | - >>> from zope.component.interfaces import IComponentLookup |
114 | - >>> from zope.interface import implementer, Interface |
115 | - >>> @implementer(IComponentLookup) |
116 | - ... @adapter(Interface) |
117 | - ... def everything_uses_the_global_site_manager(context): |
118 | - ... return sm |
119 | - >>> sm.registerAdapter(everything_uses_the_global_site_manager) |
120 | - |
121 | Here's a simple implementation of IContact. |
122 | |
123 | >>> from urllib import quote |
124 | @@ -169,14 +240,18 @@ |
125 | ... implements(IContactEntryBeta) |
126 | ... delegates(IContactEntryBeta) |
127 | ... schema = IContactEntryBeta |
128 | + ... def __init__(self, context, request): |
129 | + ... self.context = context |
130 | + |
131 | >>> sm.registerAdapter( |
132 | - ... ContactEntryBeta, provided=IContactEntry, name="beta") |
133 | + ... ContactEntryBeta, [IContact, IWebServiceRequestBeta], |
134 | + ... provided=IContactEntry) |
135 | |
136 | By wrapping one of our predefined Contacts in a ContactEntryBeta |
137 | object, we can verify that it implements IContactEntryBeta and |
138 | IContactEntry. |
139 | |
140 | - >>> entry = ContactEntryBeta(C1) |
141 | + >>> entry = ContactEntryBeta(C1, None) |
142 | >>> IContactEntry.validateInvariants(entry) |
143 | >>> IContactEntryBeta.validateInvariants(entry) |
144 | |
145 | @@ -188,7 +263,7 @@ |
146 | ... implements(IContactEntry10) |
147 | ... schema = IContactEntry10 |
148 | ... |
149 | - ... def __init__(self, contact): |
150 | + ... def __init__(self, contact, request): |
151 | ... self.contact = contact |
152 | ... |
153 | ... @property |
154 | @@ -199,9 +274,10 @@ |
155 | ... def fax_number(self): |
156 | ... return self.contact.fax |
157 | >>> sm.registerAdapter( |
158 | - ... ContactEntry10, provided=IContactEntry, name="1.0") |
159 | + ... ContactEntry10, [IContact, IWebServiceRequest10], |
160 | + ... provided=IContactEntry) |
161 | |
162 | - >>> entry = ContactEntry10(C1) |
163 | + >>> entry = ContactEntry10(C1, None) |
164 | >>> IContactEntry.validateInvariants(entry) |
165 | >>> IContactEntry10.validateInvariants(entry) |
166 | |
167 | @@ -213,16 +289,17 @@ |
168 | ... implements(IContactEntryDev) |
169 | ... schema = IContactEntryDev |
170 | ... |
171 | - ... def __init__(self, contact): |
172 | + ... def __init__(self, contact, request): |
173 | ... self.contact = contact |
174 | ... |
175 | ... @property |
176 | ... def phone_number(self): |
177 | ... return self.contact.phone |
178 | >>> sm.registerAdapter( |
179 | - ... ContactEntryDev, provided=IContactEntry, name="dev") |
180 | + ... ContactEntryDev, [IContact, IWebServiceRequestDev], |
181 | + ... provided=IContactEntry) |
182 | |
183 | - >>> entry = ContactEntryDev(C1) |
184 | + >>> entry = ContactEntryDev(C1, None) |
185 | >>> IContactEntry.validateInvariants(entry) |
186 | >>> IContactEntryDev.validateInvariants(entry) |
187 | |
188 | @@ -239,19 +316,35 @@ |
189 | ... |
190 | ComponentLookupError: ... |
191 | |
192 | -When adapting Contact to IEntry you must specify a version number as |
193 | -the name of the adapter. The object you get back will implement the |
194 | -appropriate version of the web service. |
195 | - |
196 | - >>> beta_entry = getAdapter(C1, IEntry, name="beta") |
197 | +When adapting Contact to IEntry you must provide a versioned request |
198 | +object. The IEntry object you get back will implement the appropriate |
199 | +version of the web service. |
200 | + |
201 | +To test this we'll need to manually create some versioned request |
202 | +objects. The traversal process would take care of this for us (see |
203 | +"Request lifecycle" below), but it won't work yet because we have yet |
204 | +to define a service root resource. |
205 | + |
206 | + >>> from lazr.restful.testing.webservice import ( |
207 | + ... create_web_service_request) |
208 | + >>> from zope.interface import alsoProvides |
209 | + |
210 | + >>> from zope.component import getMultiAdapter |
211 | + >>> request_beta = create_web_service_request('/beta/') |
212 | + >>> alsoProvides(request_beta, IWebServiceRequestBeta) |
213 | + >>> beta_entry = getMultiAdapter((C1, request_beta), IEntry) |
214 | >>> print beta_entry.fax |
215 | 111-2121 |
216 | |
217 | - >>> one_oh_entry = getAdapter(C1, IEntry, name="1.0") |
218 | + >>> request_10 = create_web_service_request('/1.0/') |
219 | + >>> alsoProvides(request_10, IWebServiceRequest10) |
220 | + >>> one_oh_entry = getMultiAdapter((C1, request_10), IEntry) |
221 | >>> print one_oh_entry.fax_number |
222 | 111-2121 |
223 | |
224 | - >>> dev_entry = getAdapter(C1, IEntry, name="dev") |
225 | + >>> request_dev = create_web_service_request('/dev/') |
226 | + >>> alsoProvides(request_dev, IWebServiceRequestDev) |
227 | + >>> dev_entry = getMultiAdapter((C1, request_dev), IEntry) |
228 | >>> print dev_entry.fax |
229 | Traceback (most recent call last): |
230 | ... |
231 | @@ -299,46 +392,6 @@ |
232 | >>> len(dev_collection.find()) |
233 | 2 |
234 | |
235 | -Web service infrastructure initialization |
236 | -========================================= |
237 | - |
238 | -Now that we've defined the data model, it's time to set up the web |
239 | -service infrastructure. |
240 | - |
241 | - >>> from zope.configuration import xmlconfig |
242 | - >>> zcmlcontext = xmlconfig.string(""" |
243 | - ... <configure xmlns="http://namespaces.zope.org/zope"> |
244 | - ... <include package="lazr.restful" file="basic-site.zcml"/> |
245 | - ... <utility |
246 | - ... factory="lazr.restful.example.base.filemanager.FileManager" /> |
247 | - ... </configure> |
248 | - ... """) |
249 | - |
250 | -Here's the configuration, which defines the three versions: 'beta', |
251 | -'1.0', and 'dev'. |
252 | - |
253 | - >>> from lazr.restful import directives |
254 | - >>> from lazr.restful.interfaces import IWebServiceConfiguration |
255 | - >>> from lazr.restful.simple import BaseWebServiceConfiguration |
256 | - >>> from lazr.restful.testing.webservice import WebServiceTestPublication |
257 | - |
258 | - >>> class WebServiceConfiguration(BaseWebServiceConfiguration): |
259 | - ... hostname = 'api.multiversion.dev' |
260 | - ... use_https = False |
261 | - ... active_versions = ['beta', '1.0'] |
262 | - ... latest_version_uri_prefix = 'dev' |
263 | - ... code_revision = 'test' |
264 | - ... max_batch_size = 100 |
265 | - ... directives.publication_class(WebServiceTestPublication) |
266 | - |
267 | - >>> from grokcore.component.testing import grok_component |
268 | - >>> ignore = grok_component( |
269 | - ... 'WebServiceConfiguration', WebServiceConfiguration) |
270 | - |
271 | - >>> from zope.component import getUtility |
272 | - >>> config = getUtility(IWebServiceConfiguration) |
273 | - |
274 | - |
275 | The service root resource |
276 | ========================= |
277 | |
278 | @@ -384,8 +437,6 @@ |
279 | ... RootResourceAbsoluteURL, [cls, IBrowserRequest]) |
280 | |
281 | >>> from zope.traversing.browser import absoluteURL |
282 | - >>> from lazr.restful.testing.webservice import ( |
283 | - ... create_web_service_request) |
284 | |
285 | >>> beta_request = create_web_service_request('/beta/') |
286 | >>> ignore = beta_request.traverse(None) |
287 | @@ -397,3 +448,25 @@ |
288 | >>> print absoluteURL(dev_app, dev_request) |
289 | http://api.multiversion.dev/dev/ |
290 | |
291 | + |
292 | +Request lifecycle |
293 | +================= |
294 | + |
295 | +When a request first comes in, there's no way to tell which version |
296 | +it's associated with. |
297 | + |
298 | + >>> from lazr.restful.testing.webservice import ( |
299 | + ... create_web_service_request) |
300 | + |
301 | + >>> request_beta = create_web_service_request('/beta/') |
302 | + >>> IWebServiceRequestBeta.providedBy(request_beta) |
303 | + False |
304 | + |
305 | +The traversal process associates the request with a particular version. |
306 | + |
307 | + >>> request_beta.traverse(None) |
308 | + <BetaServiceRootResource object ...> |
309 | + >>> IWebServiceRequestBeta.providedBy(request_beta) |
310 | + True |
311 | + >>> print request_beta.version |
312 | + beta |
313 | |
314 | === modified file 'src/lazr/restful/interfaces/_rest.py' |
315 | --- src/lazr/restful/interfaces/_rest.py 2009-11-18 17:33:20 +0000 |
316 | +++ src/lazr/restful/interfaces/_rest.py 2010-01-06 21:06:12 +0000 |
317 | @@ -48,6 +48,7 @@ |
318 | 'LAZR_WEBSERVICE_NAME', |
319 | 'LAZR_WEBSERVICE_NS', |
320 | 'IWebServiceClientRequest', |
321 | + 'IVersionedClientRequestImplementation', |
322 | 'IWebServiceLayer', |
323 | ] |
324 | |
325 | @@ -251,13 +252,28 @@ |
326 | |
327 | |
328 | class IWebServiceClientRequest(IBrowserRequest): |
329 | - """Marker interface requests to the web service.""" |
330 | + """Interface for requests to the web service.""" |
331 | + version = Attribute("The version of the web service that the client " |
332 | + "requested.") |
333 | |
334 | |
335 | class IWebServiceLayer(IWebServiceClientRequest, IDefaultBrowserLayer): |
336 | """Marker interface for registering views on the web service.""" |
337 | |
338 | |
339 | +class IVersionedClientRequestImplementation(Interface): |
340 | + """Used to register IWebServiceClientRequest subclasses as utilities. |
341 | + |
342 | + Every version of a web service must register a subclass of |
343 | + IWebServiceClientRequest as an |
344 | + IVersionedClientRequestImplementation utility, with a name that's |
345 | + the web service version name. For instance: |
346 | + |
347 | + registerUtility(IWebServiceClientRequestBeta, |
348 | + IVersionedClientRequestImplementation, name="beta") |
349 | + """ |
350 | + pass |
351 | + |
352 | class IJSONRequestCache(Interface): |
353 | """A cache of objects exposed as URLs or JSON representations.""" |
354 | |
355 | |
356 | === modified file 'src/lazr/restful/publisher.py' |
357 | --- src/lazr/restful/publisher.py 2009-11-19 15:53:26 +0000 |
358 | +++ src/lazr/restful/publisher.py 2010-01-06 21:06:12 +0000 |
359 | @@ -34,7 +34,8 @@ |
360 | EntryResource, ScopedCollection, ServiceRootResource) |
361 | from lazr.restful.interfaces import ( |
362 | IByteStorage, ICollection, ICollectionField, IEntry, IEntryField, |
363 | - IHTTPResource, IServiceRootResource, IWebBrowserInitiatedRequest, |
364 | + IHTTPResource, IServiceRootResource, |
365 | + IVersionedClientRequestImplementation, IWebBrowserInitiatedRequest, |
366 | IWebServiceClientRequest, IWebServiceConfiguration) |
367 | |
368 | |
369 | @@ -255,11 +256,25 @@ |
370 | raise NotFound(self, '', self) |
371 | self.annotations[self.VERSION_ANNOTATION] = version |
372 | |
373 | + # Find the version-specific interface this request should |
374 | + # provide, and provide it. |
375 | + try: |
376 | + to_provide = getUtility(IVersionedClientRequestImplementation, |
377 | + name=version) |
378 | + alsoProvides(self, to_provide) |
379 | + except ComponentLookupError: |
380 | + # XXX leonardr 2009-11-23 This stops single-version tests |
381 | + # from breaking. I'll probably remove it once the |
382 | + # necessary version-specific interfaces are automatically |
383 | + # generated. |
384 | + pass |
385 | + self.version = version |
386 | + |
387 | # Find the appropriate service root for this version and set |
388 | # the publication's application appropriately. |
389 | try: |
390 | # First, try to find a version-specific service root. |
391 | - service_root = getUtility(IServiceRootResource, name=version) |
392 | + service_root = getUtility(IServiceRootResource, name=self.version) |
393 | except ComponentLookupError: |
394 | # Next, try a version-independent service root. |
395 | service_root = getUtility(IServiceRootResource) |
This branch changes the way an incoming web service request is associated with a specific version of the web service. Previously, the version number was stuffed into a Zope annotation on the request object. Versioned lookups were done as named lookups using the version number as the name.
Now, the web service is expected to define a marker interface for every version it publishes, eg. IWebServiceRequ estBeta. Once the traversal code knows which version of the web service the client is requesting, it calls alsoProvides on the request object to mark it with the appropriate marker interface.
The idea here is to get rid of named lookups. Instead of this:
getAdapter( data_model_ object, IEntry, name="1.0")
We can now do this:
getMultiAdapter ([data_ model_object, request], IEntry)
Unfortunately, the benefits of this are mostly in the future. The two named lookups we do right now cannot be turned into multiadapter lookups. The first is the lookup that gets us the version-specific request interface in the first place. The second is a utility lookup:
root = getUtility( IWebServiceRoot Resource, name="1.0")
There's no such thing as a multi-value utility lookup. However, I'm considering writing a wrapper class that lets the utility lookup look like a simple adaptation:
root = IWebServiceRoot Resource( request)
But I'm not sure if that's valuable to the programmer.
To avoid a million test failures, I preserved the behavior of the unversioned code (where the incoming request does not have any special interface applied to it). I'll almost certainly remove this code once I make the service generation code support multi-versioning, but that's a way in the future.