Merge lp:~leonardr/lazr.restful/latest-version into lp:lazr.restful

Proposed by Leonard Richardson
Status: Merged
Approved by: Brad Crittenden
Approved revision: not available
Merged at revision: not available
Proposed branch: lp:~leonardr/lazr.restful/latest-version
Merge into: lp:lazr.restful
Diff against target: 368 lines (+134/-26)
12 files modified
src/lazr/restful/NEWS.txt (+14/-0)
src/lazr/restful/docs/absoluteurl.txt (+31/-0)
src/lazr/restful/docs/webservice-marshallers.txt (+1/-0)
src/lazr/restful/docs/webservice.txt (+1/-0)
src/lazr/restful/example/base/root.py (+9/-1)
src/lazr/restful/example/base/tests/hostedfile.txt (+1/-1)
src/lazr/restful/example/base/tests/root.txt (+0/-3)
src/lazr/restful/example/base/tests/service.txt (+35/-3)
src/lazr/restful/interfaces/_rest.py (+14/-0)
src/lazr/restful/publisher.py (+17/-9)
src/lazr/restful/simple.py (+2/-1)
src/lazr/restful/testing/webservice.py (+9/-8)
To merge this branch: bzr merge lp:~leonardr/lazr.restful/latest-version
Reviewer Review Type Date Requested Status
Brad Crittenden (community) code Approve
Review via email: mp+14791@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Leonard Richardson (leonardr) wrote :

This is the first branch of my multi-version web service project. It introduces the "devel" or "trunk" web service version. (I'm not sure what the default name should be.) A client can request the "devel" version or the version name given in IWebServiceConfiguration.service_version_uri_prefix.

Future branches will allow multiple versions of the web service that can be drastically different. Currently there are only two versions of the web service, and they are exactly the same, except for the URIs.

The two pieces of code that have changed are the traversal code and the code that generates URLs. The traversal code stores the requested web service version in a "lazr.restful.version" annotation on the request object. The URL generation code grabs that annotation and uses it to decide what information to put in the URL. This means that test code that generates URLs without using the traversal code must put fake values in the "lazr.restful.version" annotation.

Revision history for this message
Brad Crittenden (bac) :
review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/lazr/restful/NEWS.txt'
2--- src/lazr/restful/NEWS.txt 2009-11-10 14:30:52 +0000
3+++ src/lazr/restful/NEWS.txt 2009-11-12 17:10:22 +0000
4@@ -2,6 +2,20 @@
5 NEWS for lazr.restful
6 =====================
7
8+Development
9+===========
10+
11+Added the precursor of a versioning system for web services. Clients
12+can now request the "trunk" of a web service as well as one published
13+version. Apart from the URIs served, the two web services are exactly
14+the same.
15+
16+This release introduces a new field to IWebServiceConfiguration:
17+latest_version_uri_prefix. If you are rolling your own
18+IWebServiceConfiguration implementation, rather than subclassing from
19+BaseWebServiceConfiguration or one of its subclasses, you'll need to
20+set a value for this.
21+
22 0.9.17 (2009-11-10)
23 ===================
24
25
26=== modified file 'src/lazr/restful/docs/absoluteurl.txt'
27--- src/lazr/restful/docs/absoluteurl.txt 2009-08-20 13:14:10 +0000
28+++ src/lazr/restful/docs/absoluteurl.txt 2009-11-12 17:10:22 +0000
29@@ -30,6 +30,7 @@
30 ... hostname = "hostname"
31 ... service_root_uri_prefix = "root_uri_prefix/"
32 ... service_version_uri_prefix = "service_version_uri_prefix"
33+ ... latest_version_uri_prefix = "latest_version_uri_prefix"
34 ... port = 1000
35 ... use_https = True
36
37@@ -48,6 +49,8 @@
38
39 >>> resource = RootResource()
40 >>> request = Request("", {})
41+ >>> request.annotations[request.VERSION_ANNOTATION] = (
42+ ... 'service_version_uri_prefix')
43 >>> adapter = getMultiAdapter((resource, request), IAbsoluteURL)
44
45 Calling the RootResourceAbsoluteURL will give the service root's
46@@ -78,6 +81,34 @@
47 >>> print getMultiAdapter((resource, request), IAbsoluteURL)()
48 http://hostname/root_uri_prefix/service_version_uri_prefix/
49
50+The URL generated includes a version identifier taken from the
51+value of the 'lazr.restful.version' annotation.
52+
53+ >>> request.annotations[request.VERSION_ANNOTATION] = (
54+ ... 'latest_version_uri_prefix')
55+ >>> adapter = getMultiAdapter((resource, request), IAbsoluteURL)
56+ >>> print adapter()
57+ http://hostname/root_uri_prefix/latest_version_uri_prefix/
58+
59+For purposes of URL generation, the annotation doesn't have to be a
60+real version defined by the web service. Any string will do.
61+
62+ >>> request.annotations[request.VERSION_ANNOTATION] = (
63+ ... 'no_such_version')
64+ >>> adapter = getMultiAdapter((resource, request), IAbsoluteURL)
65+ >>> print adapter()
66+ http://hostname/root_uri_prefix/no_such_version/
67+
68+(However, the lazr.restful traversal code will reject an invalid
69+version, and so in a real application lazr.restful.version will not be
70+set. See examples/base/tests/service.txt for that test.)
71+
72+Cleanup.
73+
74+ >>> request.annotations[request.VERSION_ANNOTATION] = (
75+ ... 'service_version_uri_prefix')
76+
77+
78 MultiplePathPartAbsoluteURL
79 ===========================
80
81
82=== modified file 'src/lazr/restful/docs/webservice-marshallers.txt'
83--- src/lazr/restful/docs/webservice-marshallers.txt 2009-11-10 13:58:22 +0000
84+++ src/lazr/restful/docs/webservice-marshallers.txt 2009-11-12 17:10:22 +0000
85@@ -13,6 +13,7 @@
86 >>> from lazr.restful.example.base.root import (
87 ... CookbookServiceRootResource)
88 >>> request = Request("", {'HTTP_HOST': 'cookbooks.dev'})
89+ >>> request.annotations[request.VERSION_ANNOTATION] = '1.0'
90 >>> application = CookbookServiceRootResource()
91 >>> request.setPublication(WebServiceTestPublication(application))
92 >>> request.processInputs()
93
94=== modified file 'src/lazr/restful/docs/webservice.txt'
95--- src/lazr/restful/docs/webservice.txt 2009-10-07 17:12:24 +0000
96+++ src/lazr/restful/docs/webservice.txt 2009-11-12 17:10:22 +0000
97@@ -518,6 +518,7 @@
98 >>> class WebServiceConfiguration(BaseWebServiceConfiguration):
99 ... use_https = False
100 ... service_version_uri_prefix = 'beta'
101+ ... latest_version_uri_prefix = 'devel'
102 ... code_revision = 'test'
103 ... max_batch_size = 100
104 ... directives.publication_class(WebServiceTestPublication)
105
106=== modified file 'src/lazr/restful/example/base/root.py'
107--- src/lazr/restful/example/base/root.py 2009-09-03 12:40:34 +0000
108+++ src/lazr/restful/example/base/root.py 2009-11-12 17:10:22 +0000
109@@ -62,8 +62,16 @@
110
111 @property
112 def alias_url(self):
113+ """The URL to the managed file.
114+
115+ This URL will always contain the latest_version_uri_prefix, no
116+ matter the version of the original request. This is not ideal,
117+ but it's acceptable because 1) this is just a test
118+ implementation, and 2) the ByteStorage implementation cannot
119+ change between versions.
120+ """
121 return 'http://cookbooks.dev/%s/filemanager/%s' % (
122- getUtility(IWebServiceConfiguration).service_version_uri_prefix,
123+ getUtility(IWebServiceConfiguration).latest_version_uri_prefix,
124 self.id)
125
126 def createStored(self, mediaType, representation, filename=None):
127
128=== modified file 'src/lazr/restful/example/base/tests/hostedfile.txt'
129--- src/lazr/restful/example/base/tests/hostedfile.txt 2009-05-07 20:46:07 +0000
130+++ src/lazr/restful/example/base/tests/hostedfile.txt 2009-11-12 17:10:22 +0000
131@@ -125,7 +125,7 @@
132 >>> print webservice.get(greens_cover)
133 HTTP/1.1 303 See Other
134 ...
135- Location: http://cookbooks.dev/1.0/filemanager/2
136+ Location: http://cookbooks.dev/devel/filemanager/2
137 ...
138
139 Deleting a cover (with DELETE) disables the redirect.
140
141=== modified file 'src/lazr/restful/example/base/tests/root.txt'
142--- src/lazr/restful/example/base/tests/root.txt 2009-10-14 20:00:07 +0000
143+++ src/lazr/restful/example/base/tests/root.txt 2009-11-12 17:10:22 +0000
144@@ -57,7 +57,6 @@
145 Allow: GET
146 ...
147
148-===============
149 Conditional GET
150 ===============
151
152@@ -103,7 +102,6 @@
153 >>> conditional_response.status
154 200
155
156-===========
157 Compression
158 ===========
159
160@@ -165,7 +163,6 @@
161
162 >>> config.set_hop_by_hop_headers = True
163
164-=====================
165 Top-level entry links
166 =====================
167
168
169=== modified file 'src/lazr/restful/example/base/tests/service.txt'
170--- src/lazr/restful/example/base/tests/service.txt 2009-04-16 20:45:55 +0000
171+++ src/lazr/restful/example/base/tests/service.txt 2009-11-12 17:10:22 +0000
172@@ -7,7 +7,41 @@
173 >>> from lazr.restful.testing.webservice import WebServiceCaller
174 >>> webservice = WebServiceCaller(domain='cookbooks.dev')
175
176-=====================
177+Versioning
178+==========
179+
180+In addition to the published version of the web service, lazr.restful
181+publishes a "development" version which may contain backwards
182+incompatible changes. The default name of the development version is
183+"devel".
184+
185+ >>> body = webservice.get('/', api_version="devel").jsonBody()
186+
187+The development version serves URLs that reflect its version name.
188+
189+ >>> print body['recipes_collection_link']
190+ http://cookbooks.dev/devel/recipes
191+ >>> print body['resource_type_link']
192+ http://cookbooks.dev/devel/#service-root
193+
194+Currently there is no way to publish multiple versions of the web
195+service, so apart from the URLs, the development version is exactly
196+the same as the only published version.
197+
198+All versions of the web service can be accessed through Ajax.
199+
200+ >>> from lazr.restful.testing.webservice import WebServiceAjaxCaller
201+ >>> ajax = WebServiceAjaxCaller(domain='cookbooks.dev')
202+ >>> body = ajax.get('/', api_version="devel").jsonBody()
203+ >>> print body['resource_type_link']
204+ http://cookbooks.dev/devel/#service-root
205+
206+An attempt to access a nonexistent version yields a 404 error.
207+
208+ >>> print webservice.get('/', api_version="no_such_version")
209+ HTTP/1.1 404 Not Found
210+ ...
211+
212 Nonexistent resources
213 =====================
214
215@@ -28,7 +62,6 @@
216 HTTP/1.1 404 Not Found
217 ...
218
219-===================
220 Nonexistent methods
221 ===================
222
223@@ -56,7 +89,6 @@
224 Allow: GET PUT PATCH POST
225 ...
226
227-=========================
228 Inappropriate media types
229 =========================
230
231
232=== modified file 'src/lazr/restful/interfaces/_rest.py'
233--- src/lazr/restful/interfaces/_rest.py 2009-10-14 20:00:07 +0000
234+++ src/lazr/restful/interfaces/_rest.py 2009-11-12 17:10:22 +0000
235@@ -427,6 +427,20 @@
236 The service version URI prefix shows up in URIs *after* any
237 value for service_root_uri_prefix.""")
238
239+ latest_version_uri_prefix = TextLine(
240+ default=u"devel",
241+ description=u"""A string naming the alias for the "development"
242+ version of a multi-versioned web service. This version may
243+ have features not present in older versions, and may be
244+ backwards incompatible with those services, but it is not
245+ necessarily the same as any released version. If you do not
246+ publish a multi-versioned web service, just use the default.
247+
248+ latest_version_uri_prefix shows up in the same place as any
249+ other version URI prefix: after any value for
250+ service_root_uri_prefix.
251+ """)
252+
253 code_revision = TextLine(
254 default=u"",
255 description=u"""A string designating the current revision
256
257=== modified file 'src/lazr/restful/publisher.py'
258--- src/lazr/restful/publisher.py 2009-09-24 15:23:48 +0000
259+++ src/lazr/restful/publisher.py 2009-11-12 17:10:22 +0000
260@@ -201,6 +201,8 @@
261 """
262 implements(IWebServiceClientRequest)
263
264+ VERSION_ANNOTATION = 'lazr.restful.version'
265+
266 def traverse(self, ob):
267 """See `zope.publisher.interfaces.IPublisherRequest`.
268
269@@ -228,15 +230,21 @@
270 # optimizations later in the request lifecycle.
271 alsoProvides(self, IWebBrowserInitiatedRequest)
272
273- version_string = config.service_version_uri_prefix
274- if version_string is not None:
275- # Only accept versioned URLs.
276- version = self._popTraversal(version_string)
277- if version is not None:
278- names.append(version)
279- self.setVirtualHostRoot(names=names)
280- else:
281- raise NotFound(self, '', self)
282+ # Only accept versioned URLs. Either the
283+ # service_version_uri_prefix or the
284+ # latest_version_uri_prefix is acceptable.
285+ version = None
286+ for version_string in [config.service_version_uri_prefix,
287+ config.latest_version_uri_prefix]:
288+ if version_string is not None:
289+ version = self._popTraversal(version_string)
290+ if version is not None:
291+ names.append(version)
292+ self.setVirtualHostRoot(names=names)
293+ break
294+ if version is None:
295+ raise NotFound(self, '', self)
296+ self.annotations[self.VERSION_ANNOTATION] = version
297
298 def _popTraversal(self, name=None):
299 """Remove a name from the traversal stack, if it is present.
300
301=== modified file 'src/lazr/restful/simple.py'
302--- src/lazr/restful/simple.py 2009-09-02 19:36:14 +0000
303+++ src/lazr/restful/simple.py 2009-11-12 17:10:22 +0000
304@@ -249,7 +249,7 @@
305 def __init__(self, context, request):
306 """Initialize with respect to a context and request."""
307 config = getUtility(IWebServiceConfiguration)
308- self.version = config.service_version_uri_prefix
309+ self.version = request.annotations[request.VERSION_ANNOTATION]
310 if config.use_https:
311 self.schema = 'https'
312 else:
313@@ -350,3 +350,4 @@
314
315 BaseWebServiceConfiguration = implement_from_dict(
316 "BaseWebServiceConfiguration", IWebServiceConfiguration, {}, object)
317+
318
319=== modified file 'src/lazr/restful/testing/webservice.py'
320--- src/lazr/restful/testing/webservice.py 2009-10-07 12:53:04 +0000
321+++ src/lazr/restful/testing/webservice.py 2009-11-12 17:10:22 +0000
322@@ -160,7 +160,8 @@
323 def base_url(self):
324 return '%s://%s/' % (self.protocol, self.domain)
325
326- def apiVersion(self):
327+ @property
328+ def default_api_version(self):
329 return getUtility(
330 IWebServiceConfiguration).service_version_uri_prefix
331
332@@ -172,8 +173,7 @@
333 :param api_version: This is the first part of the absolute
334 url after the hostname.
335 """
336- if api_version is None:
337- api_version = self.apiVersion()
338+ api_version = api_version or self.default_api_version
339 if resource_path.startswith('/'):
340 # Prevent os.path.join() from interpreting resource_path as an
341 # absolute url. This allows paths that appear consistent with urls
342@@ -199,8 +199,7 @@
343 api_version=None):
344 path_or_url = str(path_or_url)
345 if path_or_url.startswith('/'):
346- if api_version is None:
347- api_version = self.apiVersion()
348+ api_version = api_version or self.default_api_version
349 full_url = '/'.join([api_version, path_or_url])
350 else:
351 base_url = self.base_url
352@@ -307,11 +306,13 @@
353 class WebServiceAjaxCaller(WebServiceCaller):
354 """A caller that introduces the Ajax path override to the URI prefix."""
355
356- def apiVersion(self):
357+ @property
358+ def default_api_version(self):
359 """Introduce the Ajax path override to the URI prefix."""
360 config = getUtility(IWebServiceConfiguration)
361- return (config.path_override
362- + '/' + config.service_version_uri_prefix)
363+ default_version = super(
364+ WebServiceAjaxCaller, self).default_api_version
365+ return (config.path_override + '/' + default_version)
366
367
368 class WebServiceResponseWrapper(ProxyBase):

Subscribers

People subscribed via source and target branches