Merge lp:~leonardr/lazr.restful/version-specific-request-interface into lp:lazr.restful

Proposed by Leonard Richardson
Status: Work in progress
Proposed branch: lp:~leonardr/lazr.restful/version-specific-request-interface
Merge into: lp: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:~leonardr/lazr.restful/version-specific-request-interface
Reviewer Review Type Date Requested Status
Francis J. Lacoste (community) code Approve
Review via email: mp+15172@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Leonard Richardson (leonardr) wrote :

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. IWebServiceRequestBeta. 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(IWebServiceRootResource, 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 = IWebServiceRootResource(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.

Revision history for this message
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.

Revision history for this message
Francis J. Lacoste (flacoste) wrote :
Download full text (5.2 KiB)

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/restful/docs/multiversion.txt'
> --- src/lazr/restful/docs/multiversion.txt 2009-11-19 16:43:08 +0000
> +++ src/lazr/restful/docs/multiversion.txt 2010-01-06 17:40:25 +0000
> @@ -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.interfaces import IComponentLookup
> + >>> from zope.interface import implementer, Interface
> + >>> @implementer(IComponentLookup)
> + ... @adapter(Interface)
> + ... def everything_uses_the_global_site_manager(context):
> + ... return sm
> + >>> sm.registerAdapter(everything_uses_the_global_site_manager)

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
> +IWebServiceClientRequest. These marker interfaces let lazr.restful
> +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.interfaces import (
> + ... IVersionedClientRequestImplementation, IWebServiceClientRequest)
> + >>> class IWebServiceRequestBeta(IWebServiceClientRequest):
> + ... pass
> +
> + >>> class IWebServiceRequest10(IWebServiceClientRequest):
> + ... pass
> +
> + >>> class IWebServiceRequestDev(IWebServiceClientRequest):
> + ... pass
> +
> + >>> request_classes = [IWebServiceRequestBeta,
> + ... IWebServiceRequest10, IWebServiceRequestDev]
> +
> + >>> from zope.interface import alsoProvides
> + >>> for cls, version in ((IWebServiceRequestBeta, 'beta'),
> + ... (IWebServiceRequest10, '1.0'),
> + ... (IWebServiceRequestDev, 'dev')):
> + ... alsoProvides(cls, IVersionedClientRequestImplementation)
> + ... sm.registerUtility(cls, IVersionedClientRequestImplementation,
> + ... name=version)
> +

Can you explain the use of IVersionedClientRequestImplementation? You
explained on IRC that it to make it easy to retrieve the interface related to
a version string.

Would IVersionedWebClientRequestFactory be a better name?

> +<<<<<<< TREE
> >>> for version in ['beta', 'dev', '1.0']:
> ... sm.registerAdapter(
> ... ContactCollection, provided=ICollection, name=version)
> @@ -397,3 +495,105 @@
> >>> print absoluteURL(dev_app, dev_request)
> http://api.multiversion.dev/d...

Read more...

review: Approve (code)
95. By Leonard Richardson

Merged from original branch to get rid of conflict markers.

Revision history for this message
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

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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)

Subscribers

People subscribed via source and target branches