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
=== 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 21:06:12 +0000
@@ -5,10 +5,96 @@
5services. Typically these different services represent successive5services. Typically these different services represent successive
6versions of a single web service, improved over time.6versions of a single web service, improved over time.
77
8Setup
9=====
10
11First, let's set up the web service infrastructure. Doing this first
12will let us create HTTP requests for different versions of the web
13service. The first step is to make all component lookups use the
14global 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
28Then, 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
39Web service configuration object
40--------------------------------
41
42Here's the web service configuration, which defines the three
43versions: '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
66Defining the request interfaces
67-------------------------------
68
69Every version must have a corresponding subclass of
70IWebServiceClientRequest. These marker interfaces let lazr.restful
71keep track of which version of the web service a particular client
72wants to use. In a real application, these interfaces will be
73generated 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
8Example model objects94Example model objects
9=====================95=====================
1096
11First let's define the data model. The model in webservice.txt is97Now let's define the data model. The model in webservice.txt is
12pretty complicated; this model will be just complicated enough to98pretty complicated; this model will be just complicated enough to
13illustrate how to publish multiple versions of a web service.99illustrate how to publish multiple versions of a web service.
14100
@@ -38,21 +124,6 @@
38 ... def get(self, name):124 ... def get(self, name):
39 ... "Retrieve a single contact by name."125 ... "Retrieve a single contact by name."
40126
41Before we can define any classes, a bit of web service setup. Let's
42make all component lookups use the global site manager.
43
44 >>> from zope.component import getSiteManager
45 >>> sm = getSiteManager()
46
47 >>> from zope.component import adapter
48 >>> from zope.component.interfaces import IComponentLookup
49 >>> from zope.interface import implementer, Interface
50 >>> @implementer(IComponentLookup)
51 ... @adapter(Interface)
52 ... def everything_uses_the_global_site_manager(context):
53 ... return sm
54 >>> sm.registerAdapter(everything_uses_the_global_site_manager)
55
56Here's a simple implementation of IContact.127Here's a simple implementation of IContact.
57128
58 >>> from urllib import quote129 >>> from urllib import quote
@@ -169,14 +240,18 @@
169 ... implements(IContactEntryBeta)240 ... implements(IContactEntryBeta)
170 ... delegates(IContactEntryBeta)241 ... delegates(IContactEntryBeta)
171 ... schema = IContactEntryBeta242 ... schema = IContactEntryBeta
243 ... def __init__(self, context, request):
244 ... self.context = context
245
172 >>> sm.registerAdapter(246 >>> sm.registerAdapter(
173 ... ContactEntryBeta, provided=IContactEntry, name="beta")247 ... ContactEntryBeta, [IContact, IWebServiceRequestBeta],
248 ... provided=IContactEntry)
174249
175By wrapping one of our predefined Contacts in a ContactEntryBeta250By wrapping one of our predefined Contacts in a ContactEntryBeta
176object, we can verify that it implements IContactEntryBeta and251object, we can verify that it implements IContactEntryBeta and
177IContactEntry.252IContactEntry.
178253
179 >>> entry = ContactEntryBeta(C1)254 >>> entry = ContactEntryBeta(C1, None)
180 >>> IContactEntry.validateInvariants(entry)255 >>> IContactEntry.validateInvariants(entry)
181 >>> IContactEntryBeta.validateInvariants(entry)256 >>> IContactEntryBeta.validateInvariants(entry)
182257
@@ -188,7 +263,7 @@
188 ... implements(IContactEntry10)263 ... implements(IContactEntry10)
189 ... schema = IContactEntry10264 ... schema = IContactEntry10
190 ...265 ...
191 ... def __init__(self, contact):266 ... def __init__(self, contact, request):
192 ... self.contact = contact267 ... self.contact = contact
193 ...268 ...
194 ... @property269 ... @property
@@ -199,9 +274,10 @@
199 ... def fax_number(self):274 ... def fax_number(self):
200 ... return self.contact.fax275 ... return self.contact.fax
201 >>> sm.registerAdapter(276 >>> sm.registerAdapter(
202 ... ContactEntry10, provided=IContactEntry, name="1.0")277 ... ContactEntry10, [IContact, IWebServiceRequest10],
278 ... provided=IContactEntry)
203279
204 >>> entry = ContactEntry10(C1)280 >>> entry = ContactEntry10(C1, None)
205 >>> IContactEntry.validateInvariants(entry)281 >>> IContactEntry.validateInvariants(entry)
206 >>> IContactEntry10.validateInvariants(entry)282 >>> IContactEntry10.validateInvariants(entry)
207283
@@ -213,16 +289,17 @@
213 ... implements(IContactEntryDev)289 ... implements(IContactEntryDev)
214 ... schema = IContactEntryDev290 ... schema = IContactEntryDev
215 ...291 ...
216 ... def __init__(self, contact):292 ... def __init__(self, contact, request):
217 ... self.contact = contact293 ... self.contact = contact
218 ...294 ...
219 ... @property295 ... @property
220 ... def phone_number(self):296 ... def phone_number(self):
221 ... return self.contact.phone297 ... return self.contact.phone
222 >>> sm.registerAdapter(298 >>> sm.registerAdapter(
223 ... ContactEntryDev, provided=IContactEntry, name="dev")299 ... ContactEntryDev, [IContact, IWebServiceRequestDev],
300 ... provided=IContactEntry)
224301
225 >>> entry = ContactEntryDev(C1)302 >>> entry = ContactEntryDev(C1, None)
226 >>> IContactEntry.validateInvariants(entry)303 >>> IContactEntry.validateInvariants(entry)
227 >>> IContactEntryDev.validateInvariants(entry)304 >>> IContactEntryDev.validateInvariants(entry)
228305
@@ -239,19 +316,35 @@
239 ...316 ...
240 ComponentLookupError: ...317 ComponentLookupError: ...
241318
242When adapting Contact to IEntry you must specify a version number as319When adapting Contact to IEntry you must provide a versioned request
243the name of the adapter. The object you get back will implement the320object. The IEntry object you get back will implement the appropriate
244appropriate version of the web service.321version of the web service.
245322
246 >>> beta_entry = getAdapter(C1, IEntry, name="beta")323To test this we'll need to manually create some versioned request
324objects. The traversal process would take care of this for us (see
325"Request lifecycle" below), but it won't work yet because we have yet
326to define a service root resource.
327
328 >>> from lazr.restful.testing.webservice import (
329 ... create_web_service_request)
330 >>> from zope.interface import alsoProvides
331
332 >>> from zope.component import getMultiAdapter
333 >>> request_beta = create_web_service_request('/beta/')
334 >>> alsoProvides(request_beta, IWebServiceRequestBeta)
335 >>> beta_entry = getMultiAdapter((C1, request_beta), IEntry)
247 >>> print beta_entry.fax336 >>> print beta_entry.fax
248 111-2121337 111-2121
249338
250 >>> one_oh_entry = getAdapter(C1, IEntry, name="1.0")339 >>> request_10 = create_web_service_request('/1.0/')
340 >>> alsoProvides(request_10, IWebServiceRequest10)
341 >>> one_oh_entry = getMultiAdapter((C1, request_10), IEntry)
251 >>> print one_oh_entry.fax_number342 >>> print one_oh_entry.fax_number
252 111-2121343 111-2121
253344
254 >>> dev_entry = getAdapter(C1, IEntry, name="dev")345 >>> request_dev = create_web_service_request('/dev/')
346 >>> alsoProvides(request_dev, IWebServiceRequestDev)
347 >>> dev_entry = getMultiAdapter((C1, request_dev), IEntry)
255 >>> print dev_entry.fax348 >>> print dev_entry.fax
256 Traceback (most recent call last):349 Traceback (most recent call last):
257 ...350 ...
@@ -299,46 +392,6 @@
299 >>> len(dev_collection.find())392 >>> len(dev_collection.find())
300 2393 2
301394
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 resource395The service root resource
343=========================396=========================
344397
@@ -384,8 +437,6 @@
384 ... RootResourceAbsoluteURL, [cls, IBrowserRequest])437 ... RootResourceAbsoluteURL, [cls, IBrowserRequest])
385438
386 >>> from zope.traversing.browser import absoluteURL439 >>> from zope.traversing.browser import absoluteURL
387 >>> from lazr.restful.testing.webservice import (
388 ... create_web_service_request)
389440
390 >>> beta_request = create_web_service_request('/beta/')441 >>> beta_request = create_web_service_request('/beta/')
391 >>> ignore = beta_request.traverse(None)442 >>> ignore = beta_request.traverse(None)
@@ -397,3 +448,25 @@
397 >>> print absoluteURL(dev_app, dev_request)448 >>> print absoluteURL(dev_app, dev_request)
398 http://api.multiversion.dev/dev/449 http://api.multiversion.dev/dev/
399450
451
452Request lifecycle
453=================
454
455When a request first comes in, there's no way to tell which version
456it's associated with.
457
458 >>> from lazr.restful.testing.webservice import (
459 ... create_web_service_request)
460
461 >>> request_beta = create_web_service_request('/beta/')
462 >>> IWebServiceRequestBeta.providedBy(request_beta)
463 False
464
465The traversal process associates the request with a particular version.
466
467 >>> request_beta.traverse(None)
468 <BetaServiceRootResource object ...>
469 >>> IWebServiceRequestBeta.providedBy(request_beta)
470 True
471 >>> print request_beta.version
472 beta
400473
=== modified file 'src/lazr/restful/interfaces/_rest.py'
--- src/lazr/restful/interfaces/_rest.py 2009-11-18 17:33:20 +0000
+++ src/lazr/restful/interfaces/_rest.py 2010-01-06 21:06:12 +0000
@@ -48,6 +48,7 @@
48 'LAZR_WEBSERVICE_NAME',48 'LAZR_WEBSERVICE_NAME',
49 'LAZR_WEBSERVICE_NS',49 'LAZR_WEBSERVICE_NS',
50 'IWebServiceClientRequest',50 'IWebServiceClientRequest',
51 'IVersionedClientRequestImplementation',
51 'IWebServiceLayer',52 'IWebServiceLayer',
52 ]53 ]
5354
@@ -251,13 +252,28 @@
251252
252253
253class IWebServiceClientRequest(IBrowserRequest):254class IWebServiceClientRequest(IBrowserRequest):
254 """Marker interface requests to the web service."""255 """Interface for requests to the web service."""
256 version = Attribute("The version of the web service that the client "
257 "requested.")
255258
256259
257class IWebServiceLayer(IWebServiceClientRequest, IDefaultBrowserLayer):260class IWebServiceLayer(IWebServiceClientRequest, IDefaultBrowserLayer):
258 """Marker interface for registering views on the web service."""261 """Marker interface for registering views on the web service."""
259262
260263
264class IVersionedClientRequestImplementation(Interface):
265 """Used to register IWebServiceClientRequest subclasses as utilities.
266
267 Every version of a web service must register a subclass of
268 IWebServiceClientRequest as an
269 IVersionedClientRequestImplementation utility, with a name that's
270 the web service version name. For instance:
271
272 registerUtility(IWebServiceClientRequestBeta,
273 IVersionedClientRequestImplementation, name="beta")
274 """
275 pass
276
261class IJSONRequestCache(Interface):277class IJSONRequestCache(Interface):
262 """A cache of objects exposed as URLs or JSON representations."""278 """A cache of objects exposed as URLs or JSON representations."""
263279
264280
=== modified file 'src/lazr/restful/publisher.py'
--- src/lazr/restful/publisher.py 2009-11-19 15:53:26 +0000
+++ src/lazr/restful/publisher.py 2010-01-06 21:06:12 +0000
@@ -34,7 +34,8 @@
34 EntryResource, ScopedCollection, ServiceRootResource)34 EntryResource, ScopedCollection, ServiceRootResource)
35from lazr.restful.interfaces import (35from lazr.restful.interfaces import (
36 IByteStorage, ICollection, ICollectionField, IEntry, IEntryField,36 IByteStorage, ICollection, ICollectionField, IEntry, IEntryField,
37 IHTTPResource, IServiceRootResource, IWebBrowserInitiatedRequest,37 IHTTPResource, IServiceRootResource,
38 IVersionedClientRequestImplementation, IWebBrowserInitiatedRequest,
38 IWebServiceClientRequest, IWebServiceConfiguration)39 IWebServiceClientRequest, IWebServiceConfiguration)
3940
4041
@@ -255,11 +256,25 @@
255 raise NotFound(self, '', self)256 raise NotFound(self, '', self)
256 self.annotations[self.VERSION_ANNOTATION] = version257 self.annotations[self.VERSION_ANNOTATION] = version
257258
259 # Find the version-specific interface this request should
260 # provide, and provide it.
261 try:
262 to_provide = getUtility(IVersionedClientRequestImplementation,
263 name=version)
264 alsoProvides(self, to_provide)
265 except ComponentLookupError:
266 # XXX leonardr 2009-11-23 This stops single-version tests
267 # from breaking. I'll probably remove it once the
268 # necessary version-specific interfaces are automatically
269 # generated.
270 pass
271 self.version = version
272
258 # Find the appropriate service root for this version and set273 # Find the appropriate service root for this version and set
259 # the publication's application appropriately.274 # the publication's application appropriately.
260 try:275 try:
261 # First, try to find a version-specific service root.276 # First, try to find a version-specific service root.
262 service_root = getUtility(IServiceRootResource, name=version)277 service_root = getUtility(IServiceRootResource, name=self.version)
263 except ComponentLookupError:278 except ComponentLookupError:
264 # Next, try a version-independent service root.279 # Next, try a version-independent service root.
265 service_root = getUtility(IServiceRootResource)280 service_root = getUtility(IServiceRootResource)

Subscribers

People subscribed via source and target branches