Merge lp:~leonardr/lazr.restful/generate-multiversion-operations into lp:lazr.restful

Proposed by Leonard Richardson
Status: Merged
Merged at revision: not available
Proposed branch: lp:~leonardr/lazr.restful/generate-multiversion-operations
Merge into: lp:lazr.restful
Diff against target: 882 lines (+466/-111)
8 files modified
src/lazr/restful/declarations.py (+90/-42)
src/lazr/restful/directives/__init__.py (+4/-3)
src/lazr/restful/docs/webservice-declarations.txt (+99/-28)
src/lazr/restful/example/multiversion/resources.py (+33/-4)
src/lazr/restful/example/multiversion/root.py (+3/-3)
src/lazr/restful/example/multiversion/tests/introduction.txt (+87/-4)
src/lazr/restful/metazcml.py (+147/-25)
src/lazr/restful/tests/test_webservice.py (+3/-2)
To merge this branch: bzr merge lp:~leonardr/lazr.restful/generate-multiversion-operations
Reviewer Review Type Date Requested Status
Brad Crittenden (community) code Approve
Review via email: mp+18032@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Leonard Richardson (leonardr) wrote :

This branch adds version information to all generated named operations, and introduces two new declarations (@operation_for_version and @operation_removed_in_version) that let you publish the same underlying Python method differently in different versions of the web service.

Each web service version can apply its own set of annotations to a given method, possibly even making a drastic change like changing an operation from a read operation to a write operation. By default, annotations are inherited from earlier versions, and if a particular version does not modify a named operation at all, it inherits the previous version's implementation (or lack of an implementation).

Because the list of versions is not available at the time these annotations are being processed, I use None to designate the earliest version in the web service. All subsequent versions that I need to deal with are passed as arguments to @operation_for_version and @operation_removed_in_version, so I can just use those strings. If the developer passes versions into those annotations that turn out not to be in the IWebServiceConfiguration version list, an error will happen during the second stage of ZCML processing.

I add a new type of operation to OPERATION_TYPES, 'removed_operation'. When iterating over an interface definition, this makes it easy to notice an operation that was present in older versions but not in the latest version. Simply removing the 'type' annotation makes it look like the method was incompletely defined.

The core of the code is in metazcml#register_webservice_operations, which takes code that used to be run only once, and runs it once for every versioned set of annotations. Because register_webservice_operations does not have access to the IWebServiceVersion marker interfaces, it can't register the operations directly. Instead, it passes the version name into register_adapter_for_version(), which runs later, after the IWebServiceVersion interfaces have been created. This function looks up the IWebServiceVersion utility given the version name, and registers the named operation with that versioned marker interface, so that it will only be used for that version of the web service.

generate_operation_adapter() now takes a version number, and generates the appropriate adapter class for that version, using that version's annotations.

Adapter lookups for operations no longer work if you check against IWebServiceRequestVersion. You need to check against a specific versioned request interface. This caused several miscellaneous test failures.

Revision history for this message
Brad Crittenden (bac) wrote :

Hi Leonard,

The branch is really interesting and the tests are incredibly clear and well-written. I've only got two little issues.

--Brad

> === modified file 'src/lazr/restful/declarations.py'
> --- src/lazr/restful/declarations.py 2010-01-20 21:32:53 +0000
+++ src/lazr/restful/declarations.py 2010-01-25 20:02:11 +0000

> + # It's possible that call_with, operation_parameters, and/or
> + # operation_returns_* weren't used.
> + annotations.setdefault('call_with', {})
> + annotations.setdefault('params', {})
> + annotations.setdefault('return_type', None)
> +
> + # Make sure that all parameters exists and that we miss none.

typo: exist

> @@ -495,6 +521,10 @@
> # new dict is empty rather than copying the old annotations
> annotations.push(self.version, True)
>
> + # We need to set a special 'type' so that lazr.restful can
> + # easily distinguish a method that's not present in the latest
> + # version from a method that was incompletely annotated.
> + annotations['type'] = 'removed_operation'

Use REMOVED_OPERATION_TYPE?

> class export_operation_as(_method_annotator):
> """Decorator specifying the name to export the method as."""

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'src/lazr/restful/declarations.py'
--- src/lazr/restful/declarations.py 2010-01-20 21:32:53 +0000
+++ src/lazr/restful/declarations.py 2010-01-25 20:02:11 +0000
@@ -59,14 +59,16 @@
59from lazr.restful.security import protect_schema59from lazr.restful.security import protect_schema
60from lazr.restful.utils import (60from lazr.restful.utils import (
61 camelcase_to_underscore_separated, get_current_browser_request,61 camelcase_to_underscore_separated, get_current_browser_request,
62 VersionedDict)62 make_identifier_safe, VersionedDict)
6363
64LAZR_WEBSERVICE_EXPORTED = '%s.exported' % LAZR_WEBSERVICE_NS64LAZR_WEBSERVICE_EXPORTED = '%s.exported' % LAZR_WEBSERVICE_NS
65COLLECTION_TYPE = 'collection'65COLLECTION_TYPE = 'collection'
66ENTRY_TYPE = 'entry'66ENTRY_TYPE = 'entry'
67FIELD_TYPE = 'field'67FIELD_TYPE = 'field'
68REMOVED_OPERATION_TYPE = 'removed_operation'
68OPERATION_TYPES = (69OPERATION_TYPES = (
69 'destructor', 'factory', 'read_operation', 'write_operation')70 'destructor', 'factory', 'read_operation', 'write_operation',
71 REMOVED_OPERATION_TYPE)
7072
71# Marker to specify that a parameter should contain the request user.73# Marker to specify that a parameter should contain the request user.
72REQUEST_USER = object()74REQUEST_USER = object()
@@ -312,6 +314,13 @@
312 # version.314 # version.
313 annotations = VersionedDict()315 annotations = VersionedDict()
314 annotations.push(None)316 annotations.push(None)
317
318 # The initial presumption is that an operation is not
319 # published in the earliest version of the web service. An
320 # @export_*_operation declaration will modify
321 # annotations['type'] in place to signal that it is in
322 # fact being published.
323 annotations['type'] = REMOVED_OPERATION_TYPE
315 method.__dict__[LAZR_WEBSERVICE_EXPORTED] = annotations324 method.__dict__[LAZR_WEBSERVICE_EXPORTED] = annotations
316 self.annotate_method(method, annotations)325 self.annotate_method(method, annotations)
317 return method326 return method
@@ -336,42 +345,59 @@
336 for name, method in interface.namesAndDescriptions(True):345 for name, method in interface.namesAndDescriptions(True):
337 if not IMethod.providedBy(method):346 if not IMethod.providedBy(method):
338 continue347 continue
339 annotations = method.queryTaggedValue(LAZR_WEBSERVICE_EXPORTED)348 annotation_stack = method.queryTaggedValue(LAZR_WEBSERVICE_EXPORTED)
340 if annotations is None:349 if annotation_stack is None:
341 continue350 continue
342 if annotations.get('type') is None:351 if annotation_stack.get('type') is None:
343 continue352 continue
344 # Method is exported under its own name by default.353
345 if 'as' not in annotations:354 # Make sure that each version of the web service defines
346 annotations['as'] = method.__name__355 # a self-consistent view of this method.
347356 for version, annotations in annotation_stack.stack:
348 # It's possible that call_with, operation_parameters, and/or357 if version is None:
349 # operation_returns_* weren't used.358 # Create a human-readable name for the earliest version
350 annotations.setdefault('call_with', {})359 # without trying to look it up.
351 annotations.setdefault('params', {})360 version = "(earliest version)"
352 annotations.setdefault('return_type', None)361
353362 if annotations['type'] == REMOVED_OPERATION_TYPE:
354 # Make sure that all parameters exists and that we miss none.363 # The method is published in other versions of the web
355 info = method.getSignatureInfo()364 # service, but not in this one. Don't try to validate this
356 defined_params = set(info['optional'])365 # version's annotations.
357 defined_params.update(info['required'])366 continue
358 exported_params = set(annotations['params'])367
359 exported_params.update(annotations['call_with'])368 # Method is exported under its own name by default.
360 undefined_params = exported_params.difference(defined_params)369 if 'as' not in annotations:
361 if undefined_params and info['kwargs'] is None:370 annotations['as'] = method.__name__
362 raise TypeError(371
363 'method "%s" doesn\'t have the following exported '372 # It's possible that call_with, operation_parameters, and/or
364 'parameters: %s.' % (373 # operation_returns_* weren't used.
365 method.__name__, ", ".join(sorted(undefined_params))))374 annotations.setdefault('call_with', {})
366 missing_params = set(375 annotations.setdefault('params', {})
367 info['required']).difference(exported_params)376 annotations.setdefault('return_type', None)
368 if missing_params:377
369 raise TypeError(378 # Make sure that all parameters exists and that we miss none.
370 'method "%s" needs more parameters definitions to be '379 info = method.getSignatureInfo()
371 'exported: %s' % (380 defined_params = set(info['optional'])
372 method.__name__, ", ".join(sorted(missing_params))))381 defined_params.update(info['required'])
373382 exported_params = set(annotations['params'])
374 _update_default_and_required_params(annotations['params'], info)383 exported_params.update(annotations['call_with'])
384 undefined_params = exported_params.difference(defined_params)
385 if undefined_params and info['kwargs'] is None:
386 raise TypeError(
387 'method "%s" doesn\'t have the following exported '
388 'parameters in version "%s": %s.' % (
389 method.__name__, version,
390 ", ".join(sorted(undefined_params))))
391 missing_params = set(
392 info['required']).difference(exported_params)
393 if missing_params:
394 raise TypeError(
395 'method "%s" is missing exported parameter definitions '
396 'in version "%s": %s' % (
397 method.__name__, version,
398 ", ".join(sorted(missing_params))))
399
400 _update_default_and_required_params(annotations['params'], info)
375401
376402
377def _update_default_and_required_params(params, method_info):403def _update_default_and_required_params(params, method_info):
@@ -495,6 +521,10 @@
495 # new dict is empty rather than copying the old annotations521 # new dict is empty rather than copying the old annotations
496 annotations.push(self.version, True)522 annotations.push(self.version, True)
497523
524 # We need to set a special 'type' so that lazr.restful can
525 # easily distinguish a method that's not present in the latest
526 # version from a method that was incompletely annotated.
527 annotations['type'] = 'removed_operation'
498528
499class export_operation_as(_method_annotator):529class export_operation_as(_method_annotator):
500 """Decorator specifying the name to export the method as."""530 """Decorator specifying the name to export the method as."""
@@ -940,8 +970,13 @@
940 return u''970 return u''
941971
942972
943def generate_operation_adapter(method):973def generate_operation_adapter(method, version=None):
944 """Create an IResourceOperation adapter for the exported method."""974 """Create an IResourceOperation adapter for the exported method.
975
976 :param version: The name of the version for which to generate an
977 operation adapter. None means to generate an adapter for the earliest
978 version.
979 """
945980
946 if not IMethod.providedBy(method):981 if not IMethod.providedBy(method):
947 raise TypeError("%r doesn't provide IMethod." % method)982 raise TypeError("%r doesn't provide IMethod." % method)
@@ -949,6 +984,18 @@
949 if tag is None:984 if tag is None:
950 raise TypeError(985 raise TypeError(
951 "'%s' isn't tagged for webservice export." % method.__name__)986 "'%s' isn't tagged for webservice export." % method.__name__)
987 match = [annotations for version_name, annotations in tag.stack
988 if version_name==version]
989 if len(match) == 0:
990 raise AssertionError("'%s' isn't tagged for export to web service "
991 "version '%s'" % (method.__name__, version))
992 tag = match[0]
993 if version is None:
994 # We need to incorporate the version into a Python class name,
995 # but we won't find out the name of the earliest version until
996 # runtime. Use a generic string that won't conflict with a
997 # real version string.
998 version = "__Earliest"
952999
953 bases = (BaseResourceOperationAdapter, )1000 bases = (BaseResourceOperationAdapter, )
954 if tag['type'] == 'read_operation':1001 if tag['type'] == 'read_operation':
@@ -969,7 +1016,8 @@
969 if return_type is None:1016 if return_type is None:
970 return_type = None1017 return_type = None
9711018
972 name = '%s_%s_%s' % (prefix, method.interface.__name__, tag['as'])1019 name = '%s_%s_%s_%s' % (prefix, method.interface.__name__, tag['as'],
1020 version)
973 class_dict = {'params' : tuple(tag['params'].values()),1021 class_dict = {'params' : tuple(tag['params'].values()),
974 'return_type' : return_type,1022 'return_type' : return_type,
975 '_export_info': tag,1023 '_export_info': tag,
@@ -978,7 +1026,7 @@
9781026
979 if tag['type'] == 'write_operation':1027 if tag['type'] == 'write_operation':
980 class_dict['send_modification_event'] = True1028 class_dict['send_modification_event'] = True
981 factory = type(name, bases, class_dict)1029 factory = type(make_identifier_safe(name), bases, class_dict)
982 classImplements(factory, provides)1030 classImplements(factory, provides)
983 protect_schema(factory, provides)1031 protect_schema(factory, provides)
9841032
9851033
=== modified file 'src/lazr/restful/directives/__init__.py'
--- src/lazr/restful/directives/__init__.py 2010-01-12 15:06:15 +0000
+++ src/lazr/restful/directives/__init__.py 2010-01-25 20:02:11 +0000
@@ -98,13 +98,14 @@
98 sm.registerUtility(utility, IWebServiceConfiguration)98 sm.registerUtility(utility, IWebServiceConfiguration)
9999
100 # Create and register marker interfaces for request objects.100 # Create and register marker interfaces for request objects.
101 for version in set(101 superclass = IWebServiceClientRequest
102 for version in (
102 utility.active_versions + [utility.latest_version_uri_prefix]):103 utility.active_versions + [utility.latest_version_uri_prefix]):
103 classname = ("IWebServiceClientRequestVersion" +104 classname = ("IWebServiceClientRequestVersion" +
104 make_identifier_safe(version))105 make_identifier_safe(version))
105 marker_interface = InterfaceClass(106 marker_interface = InterfaceClass(classname, (superclass,), {})
106 classname, (IWebServiceClientRequest,), {})
107 register_versioned_request_utility(marker_interface, version)107 register_versioned_request_utility(marker_interface, version)
108 superclass = marker_interface
108 return True109 return True
109110
110111
111112
=== modified file 'src/lazr/restful/docs/webservice-declarations.txt'
--- src/lazr/restful/docs/webservice-declarations.txt 2010-01-20 21:24:53 +0000
+++ src/lazr/restful/docs/webservice-declarations.txt 2010-01-25 20:02:11 +0000
@@ -535,8 +535,8 @@
535 ... def a_method(param1, param2, param3, param4): pass535 ... def a_method(param1, param2, param3, param4): pass
536 Traceback (most recent call last):536 Traceback (most recent call last):
537 ...537 ...
538 TypeError: method "a_method" needs more parameters definitions to be538 TypeError: method "a_method" is missing exported parameter definitions
539 exported: param3, param4539 in version "(earliest version)": param3, param4
540540
541Defining a parameter not available on the method also results in an541Defining a parameter not available on the method also results in an
542error:542error:
@@ -549,8 +549,8 @@
549 ... def a_method(): pass549 ... def a_method(): pass
550 Traceback (most recent call last):550 Traceback (most recent call last):
551 ...551 ...
552 TypeError: method "a_method" doesn't have the following exported552 TypeError: method "a_method" doesn't have the following exported parameters
553 parameters: no_such_param.553 in version "(earliest version)": no_such_param.
554554
555But that's not a problem if the exported method actually takes arbitrary555But that's not a problem if the exported method actually takes arbitrary
556keyword parameters:556keyword parameters:
@@ -1042,12 +1042,15 @@
1042 >>> IResourceGETOperation.implementedBy(read_method_adapter_factory)1042 >>> IResourceGETOperation.implementedBy(read_method_adapter_factory)
1043 True1043 True
10441044
1045The defined adapter is named GET_<interface>_<exported_name> and uses1045The defined adapter is named GET_<interface>_<exported_name>___Earliest
1046the ResourceOperation base class.1046and uses the ResourceOperation base class. The "___Earliest" indicates
1047that the adapter will be used in the earliest version of the web
1048service, and any subsequent versions, until a newer implementation
1049supercedes it.
10471050
1048 >>> from lazr.restful import ResourceOperation1051 >>> from lazr.restful import ResourceOperation
1049 >>> read_method_adapter_factory.__name__1052 >>> read_method_adapter_factory.__name__
1050 'GET_IBookSetOnSteroids_searchBooks'1053 'GET_IBookSetOnSteroids_searchBooks___Earliest'
1051 >>> issubclass(read_method_adapter_factory, ResourceOperation)1054 >>> issubclass(read_method_adapter_factory, ResourceOperation)
1052 True1055 True
10531056
@@ -1108,10 +1111,10 @@
1108 >>> IResourcePOSTOperation.implementedBy(write_method_adapter_factory)1111 >>> IResourcePOSTOperation.implementedBy(write_method_adapter_factory)
1109 True1112 True
11101113
1111The generated adapter class name is POST_<interface>_<operation>.1114The generated adapter class name is POST_<interface>_<operation>___Earliest.
11121115
1113 >>> print write_method_adapter_factory.__name__1116 >>> print write_method_adapter_factory.__name__
1114 POST_IBookOnSteroids_checkout1117 POST_IBookOnSteroids_checkout___Earliest
11151118
1116The adapter's params property also contains the available parameters1119The adapter's params property also contains the available parameters
1117(for which there are none in this case.)1120(for which there are none in this case.)
@@ -1156,10 +1159,11 @@
1156 >>> factory_method_adapter.send_modification_event1159 >>> factory_method_adapter.send_modification_event
1157 False1160 False
11581161
1159The generated adapter class name is also POST_<interface>_<operation>.1162The generated adapter class name is also
1163POST_<interface>_<operation>___Earliest.
11601164
1161 >>> print write_method_adapter_factory.__name__1165 >>> print write_method_adapter_factory.__name__
1162 POST_IBookOnSteroids_checkout1166 POST_IBookOnSteroids_checkout___Earliest
11631167
1164The adapter's params property also contains the available parameters.1168The adapter's params property also contains the available parameters.
11651169
@@ -1230,10 +1234,11 @@
1230 ... destructor_method_adapter_factory)1234 ... destructor_method_adapter_factory)
1231 True1235 True
12321236
1233The generated adapter class name is DELETE_<interface>_<operation>.1237The generated adapter class name is
1238DELETE_<interface>_<operation>___Earliest.
12341239
1235 >>> print destructor_method_adapter_factory.__name__1240 >>> print destructor_method_adapter_factory.__name__
1236 DELETE_IBookOnSteroids_destroy1241 DELETE_IBookOnSteroids_destroy___Earliest
12371242
12381243
1239=== Destructor ===1244=== Destructor ===
@@ -1513,7 +1518,7 @@
15132.0, 1.0, and in an unnamed pre-1.0 version.15182.0, 1.0, and in an unnamed pre-1.0 version.
15141519
1515 >>> from lazr.restful.declarations import operation_for_version1520 >>> from lazr.restful.declarations import operation_for_version
1516 >>> class MultiVersionMethod(Interface):1521 >>> class IMultiVersionMethod(Interface):
1517 ... export_as_webservice_entry()1522 ... export_as_webservice_entry()
1518 ...1523 ...
1519 ... @call_with(fixed='2.0 value')1524 ... @call_with(fixed='2.0 value')
@@ -1532,14 +1537,79 @@
1532 ... fixed=TextLine()1537 ... fixed=TextLine()
1533 ... )1538 ... )
1534 ... @export_read_operation()1539 ... @export_read_operation()
1535 ... def a_method(required, fixed='Fixed value'):1540 ... def a_method(required, fixed):
1536 ... """Method demonstrating multiversion publication."""1541 ... """Method demonstrating multiversion publication."""
15371542
1543Here's a simple implementation of IMultiVersionMethod. It'll
1544illustrate how the different versions of the web service invoke
1545`a_method` with different hard-coded values for the `fixed` argument.
1546
1547 >>> class MultiVersionMethod():
1548 ... """Simple IMultiVersionMethod implementation."""
1549 ... implements(IMultiVersionMethod)
1550 ...
1551 ... def a_method(self, required, fixed='Fixed value'):
1552 ... return "Required value: %s. Fixed value: %s" % (
1553 ... required, fixed)
1554
1555By passing a version string into generate_operation_adapter(), we can
1556get different adapter classes for different versions of the web
1557service. We'll be invoking each version against the same data model
1558object. Here it is:
1559
1560 >>> data_object = MultiVersionMethod()
1561
1562Passing in None to generate_operation_adapter gets us the method as it
1563appears in the earliest version of the web service.
1564
1565 >>> method = IMultiVersionMethod['a_method']
1566 >>> adapter_earliest_factory = generate_operation_adapter(method, None)
1567 >>> print adapter_earliest_factory.__name__
1568 GET_IMultiVersionMethod_a_method___Earliest
1569
1570 >>> method_earliest = adapter_earliest_factory(data_object, request)
1571 >>> print method_earliest.call(required="foo")
1572 Required value: foo. Fixed value: pre-1.0 value
1573
1574Passing in '1.0' or '2.0' gets us the method as it appears in the
1575appropriate version of the web service. Note that the name of the
1576adapter factory changes to reflect the fact that the method's name in
15771.0 is 'new_name', not 'a_method'.
1578
1579 >>> adapter_10_factory = generate_operation_adapter(method, '1.0')
1580 >>> print adapter_10_factory.__name__
1581 GET_IMultiVersionMethod_new_name_1_0
1582
1583 >>> method_10 = adapter_10_factory(data_object, request)
1584 >>> print method_10.call(required="bar")
1585 Required value: bar. Fixed value: 1.0 value
1586
1587 >>> adapter_20_factory = generate_operation_adapter(method, '2.0')
1588 >>> print adapter_20_factory.__name__
1589 GET_IMultiVersionMethod_new_name_2_0
1590
1591 >>> method_20 = adapter_20_factory(data_object, request)
1592 >>> print method_20.call(required="baz")
1593 Required value: baz. Fixed value: 2.0 value
1594
1595An error occurs when we try to generate an adapter for a version
1596that's not mentioned in the annotations.
1597
1598 >>> generate_operation_adapter(method, 'NoSuchVersion')
1599 Traceback (most recent call last):
1600 ...
1601 AssertionError: 'a_method' isn't tagged for export to web service
1602 version 'NoSuchVersion'
1603
1604Now that we've seen how lazr.restful uses the annotations to create
1605classes, let's take a closer look at how the 'a_method' method object
1606is annotated.
1607
1608 >>> dictionary = method.getTaggedValue('lazr.restful.exported')
1609
1538The tagged value containing the annotations looks like a dictionary,1610The tagged value containing the annotations looks like a dictionary,
1539but it's actually a stack of dictionaries named after the versions.1611but it's actually a stack of dictionaries named after the versions.
15401612
1541 >>> dictionary = MultiVersionMethod['a_method'].getTaggedValue(
1542 ... 'lazr.restful.exported')
1543 >>> dictionary.dict_names1613 >>> dictionary.dict_names
1544 [None, '1.0', '2.0']1614 [None, '1.0', '2.0']
15451615
@@ -1581,8 +1651,8 @@
1581'fixed' argument is fixed to the string 'pre-1.0 value'.1651'fixed' argument is fixed to the string 'pre-1.0 value'.
15821652
1583 >>> ignored = dictionary.pop()1653 >>> ignored = dictionary.pop()
1584 >>> print dictionary.get('as')1654 >>> print dictionary['as']
1585 None1655 a_method
1586 >>> print dictionary['params']['required'].__name__1656 >>> print dictionary['params']['required'].__name__
1587 required1657 required
1588 >>> dictionary['call_with']1658 >>> dictionary['call_with']
@@ -1618,7 +1688,7 @@
1618 >>> print version1688 >>> print version
1619 2.01689 2.0
1620 >>> sorted(attrs.items())1690 >>> sorted(attrs.items())
1621 []1691 [('type', 'removed_operation')]
16221692
1623It is present in 1.0:1693It is present in 1.0:
16241694
@@ -1634,7 +1704,7 @@
1634been defined yet:1704been defined yet:
16351705
1636 >>> print dictionary.pop()1706 >>> print dictionary.pop()
1637 (None, {})1707 (None, {'type': 'removed_operation'})
16381708
1639The @operation_removed_in_version declaration can also be used to1709The @operation_removed_in_version declaration can also be used to
1640reset a named operation's definition if you need to completely re-do1710reset a named operation's definition if you need to completely re-do
@@ -1792,22 +1862,23 @@
1792IResourceOperation adapters named under the exported method names1862IResourceOperation adapters named under the exported method names
1793are also available for IBookSetOnSteroids and IBookOnSteroids.1863are also available for IBookSetOnSteroids and IBookOnSteroids.
17941864
1795 >>> from zope.component import getGlobalSiteManager1865 >>> from zope.component import getGlobalSiteManager, getUtility
1796 >>> adapter_registry = getGlobalSiteManager().adapters1866 >>> adapter_registry = getGlobalSiteManager().adapters
17971867
1868 >>> request_interface = getUtility(IWebServiceVersion, name='beta')
1798 >>> from lazr.restful.interfaces import IWebServiceClientRequest1869 >>> from lazr.restful.interfaces import IWebServiceClientRequest
1799 >>> adapter_registry.lookup(1870 >>> adapter_registry.lookup(
1800 ... (IBookSetOnSteroids, IWebServiceClientRequest),1871 ... (IBookSetOnSteroids, request_interface),
1801 ... IResourceGETOperation, 'searchBooks')1872 ... IResourceGETOperation, 'searchBooks')
1802 <class '...GET_IBookSetOnSteroids_searchBooks'>1873 <class '...GET_IBookSetOnSteroids_searchBooks___Earliest'>
1803 >>> adapter_registry.lookup(1874 >>> adapter_registry.lookup(
1804 ... (IBookSetOnSteroids, IWebServiceClientRequest),1875 ... (IBookSetOnSteroids, request_interface),
1805 ... IResourcePOSTOperation, 'create_book')1876 ... IResourcePOSTOperation, 'create_book')
1806 <class '...POST_IBookSetOnSteroids_create_book'>1877 <class '...POST_IBookSetOnSteroids_create_book___Earliest'>
1807 >>> adapter_registry.lookup(1878 >>> adapter_registry.lookup(
1808 ... (IBookOnSteroids, IWebServiceClientRequest),1879 ... (IBookOnSteroids, request_interface),
1809 ... IResourcePOSTOperation, 'checkout')1880 ... IResourcePOSTOperation, 'checkout')
1810 <class '...POST_IBookOnSteroids_checkout'>1881 <class '...POST_IBookOnSteroids_checkout___Earliest'>
18111882
1812There is also a 'index.html' view on the IWebServiceClientRequest1883There is also a 'index.html' view on the IWebServiceClientRequest
1813registered for the InvalidEmail exception.1884registered for the InvalidEmail exception.
18141885
=== modified file 'src/lazr/restful/example/multiversion/resources.py'
--- src/lazr/restful/example/multiversion/resources.py 2009-11-16 14:25:45 +0000
+++ src/lazr/restful/example/multiversion/resources.py 2010-01-25 20:02:11 +0000
@@ -5,16 +5,20 @@
5 'KeyValuePair',5 'KeyValuePair',
6 'PairSet']6 'PairSet']
77
8from zope.interface import implements
8from zope.schema import Text9from zope.schema import Text
9from zope.location.interfaces import ILocation10from zope.location.interfaces import ILocation
1011
11from lazr.restful.declarations import (12from lazr.restful.declarations import (
12 collection_default_content, export_as_webservice_collection,13 collection_default_content, export_as_webservice_collection,
13 export_as_webservice_entry, exported)14 export_as_webservice_entry, export_operation_as,
15 export_read_operation, exported, operation_for_version,
16 operation_parameters, operation_removed_in_version)
1417
15# We don't need separate implementations of these classes, so borrow18# Our implementations of these classes can be based on the
16# the implementations from the WSGI example.19# implementations from the WSGI example.
17from lazr.restful.example.wsgi.resources import PairSet, KeyValuePair20from lazr.restful.example.wsgi.resources import (
21 PairSet as BasicPairSet, KeyValuePair as BasicKeyValuePair)
1822
19# Our interfaces _will_ diverge from the WSGI example interfaces, so23# Our interfaces _will_ diverge from the WSGI example interfaces, so
20# define them separately.24# define them separately.
@@ -33,3 +37,28 @@
3337
34 def get(request, name):38 def get(request, name):
35 """Retrieve a key-value pair by its key."""39 """Retrieve a key-value pair by its key."""
40
41 # This operation is not published in trunk.
42 @operation_removed_in_version('trunk')
43 # In 3.0, it's published as 'by_value'
44 @export_operation_as('by_value')
45 @operation_for_version('3.0')
46 # In 1.0 and 2.0, it's published as 'byValue'
47 @export_operation_as('byValue')
48 @operation_parameters(value=Text())
49 @export_read_operation()
50 @operation_for_version('1.0')
51 # This operation is not published in versions earlier than 1.0.
52 def find_for_value(value):
53 """Find key-value pairs that have the given value."""
54
55
56class PairSet(BasicPairSet):
57 implements(IPairSet)
58
59 def find_for_value(self, value):
60 return [pair for pair in self.pairs if value == pair.value]
61
62
63class KeyValuePair(BasicKeyValuePair):
64 implements(IKeyValuePair)
3665
=== modified file 'src/lazr/restful/example/multiversion/root.py'
--- src/lazr/restful/example/multiversion/root.py 2009-11-16 14:25:45 +0000
+++ src/lazr/restful/example/multiversion/root.py 2010-01-25 20:02:11 +0000
@@ -34,7 +34,7 @@
3434
35class WebServiceConfiguration(BaseWSGIWebServiceConfiguration):35class WebServiceConfiguration(BaseWSGIWebServiceConfiguration):
36 code_revision = '1'36 code_revision = '1'
37 active_versions = ['beta', '1.0', '2.0']37 active_versions = ['beta', '1.0', '2.0', '3.0']
38 latest_version_uri_prefix = 'trunk'38 latest_version_uri_prefix = 'trunk'
39 use_https = False39 use_https = False
40 view_permission = 'zope.Public'40 view_permission = 'zope.Public'
@@ -45,8 +45,8 @@
45 def _build_top_level_objects(self):45 def _build_top_level_objects(self):
46 pairset = PairSet()46 pairset = PairSet()
47 pairset.pairs = [47 pairset.pairs = [
48 KeyValuePair(self, "foo", "bar"),48 KeyValuePair(pairset, "foo", "bar"),
49 KeyValuePair(self, "1", "2")49 KeyValuePair(pairset, "1", "2")
50 ]50 ]
51 collections = dict(pairs=(IKeyValuePair, pairset))51 collections = dict(pairs=(IKeyValuePair, pairset))
52 return collections, {}52 return collections, {}
5353
=== modified file 'src/lazr/restful/example/multiversion/tests/introduction.txt'
--- src/lazr/restful/example/multiversion/tests/introduction.txt 2009-11-16 14:25:45 +0000
+++ src/lazr/restful/example/multiversion/tests/introduction.txt 2010-01-25 20:02:11 +0000
@@ -12,10 +12,10 @@
12 >>> from lazr.restful.testing.webservice import WebServiceCaller12 >>> from lazr.restful.testing.webservice import WebServiceCaller
13 >>> webservice = WebServiceCaller(domain='multiversion.dev')13 >>> webservice = WebServiceCaller(domain='multiversion.dev')
1414
15The multiversion web service serves three named versions of the same15The multiversion web service serves four named versions of the same
16web service: "beta", "1.0", and "2.0". Once you make a request to the16web service: "beta", "1.0", "2.0", and "3.0". Once you make a request
17service root of a particular version, the web service only serves you17to the service root of a particular version, the web service only
18links within that version.18serves you links within that version.
1919
20 >>> top_level_response = webservice.get(20 >>> top_level_response = webservice.get(
21 ... "/", api_version="beta").jsonBody()21 ... "/", api_version="beta").jsonBody()
@@ -32,6 +32,11 @@
32 >>> print top_level_response['key_value_pairs_collection_link']32 >>> print top_level_response['key_value_pairs_collection_link']
33 http://multiversion.dev/2.0/pairs33 http://multiversion.dev/2.0/pairs
3434
35 >>> top_level_response = webservice.get(
36 ... "/", api_version="3.0").jsonBody()
37 >>> print top_level_response['key_value_pairs_collection_link']
38 http://multiversion.dev/3.0/pairs
39
35Like all web services, the multiversion service also serves a40Like all web services, the multiversion service also serves a
36development version which tracks the current state of the web service,41development version which tracks the current state of the web service,
37including all changes that have not yet been folded into a named42including all changes that have not yet been folded into a named
@@ -62,3 +67,81 @@
62 >>> print webservice.get('/', api_version="no_such_version")67 >>> print webservice.get('/', api_version="no_such_version")
63 HTTP/1.1 404 Not Found68 HTTP/1.1 404 Not Found
64 ...69 ...
70
71Collections and entries
72=======================
73
74The web service presents a single collection of key-value pairs.
75
76 >>> body = webservice.get('/pairs').jsonBody()
77 >>> for entry in body['entries']:
78 ... print entry['self_link'], entry['key'], entry['value']
79 http://multiversion.dev/3.0/pairs/foo foo bar
80 http://multiversion.dev/3.0/pairs/1 1 2
81
82 >>> body = webservice.get('/pairs/foo').jsonBody()
83 >>> print body['key'], body['value']
84 foo bar
85
86Named operations
87================
88
89The collection of key-value pairs defines a named operation for
90finding pairs, given a value. This operation is present in some
91versions of the web service but not others. In some versions it's
92called "byValue"; in others, it's called "by_value".
93
94 >>> def show_value(version, op):
95 ... url = '/pairs?ws.op=%s&value=bar' % op
96 ... body = webservice.get(url, api_version=version).jsonBody()
97 ... return body[0]['key']
98
99The named operation is not published at all in the 'beta' version of
100the web service.
101
102 >>> print show_value("beta", 'byValue')
103 Traceback (most recent call last):
104 ...
105 ValueError: No such operation: byValue
106
107 >>> print show_value("beta", 'by_value')
108 Traceback (most recent call last):
109 ...
110 ValueError: No such operation: by_value
111
112In the '1.0' and '2.0' versions, the named operation is published as
113'byValue'. 'by_value' does not work.
114
115 >>> print show_value("1.0", 'byValue')
116 foo
117
118 >>> print show_value("2.0", 'byValue')
119 foo
120 >>> print show_value("2.0", 'by_value')
121 Traceback (most recent call last):
122 ...
123 ValueError: No such operation: by_value
124
125In the '3.0' version, the named operation is published as
126'by_value'. 'byValue' does not work.
127
128 >>> print show_value("3.0", "by_value")
129 foo
130
131 >>> print show_value("3.0", 'byValue')
132 Traceback (most recent call last):
133 ...
134 ValueError: No such operation: byValue
135
136In the 'trunk' version, the named operation has been removed. Neither
137'byValue' nor 'by_value' work.
138
139 >>> print show_value("trunk", 'byValue')
140 Traceback (most recent call last):
141 ...
142 ValueError: No such operation: byValue
143
144 >>> print show_value("trunk", 'by_value')
145 Traceback (most recent call last):
146 ...
147 ValueError: No such operation: by_value
65148
=== modified file 'src/lazr/restful/metazcml.py'
--- src/lazr/restful/metazcml.py 2010-01-06 20:15:10 +0000
+++ src/lazr/restful/metazcml.py 2010-01-25 20:02:11 +0000
@@ -8,6 +8,7 @@
88
9import inspect9import inspect
1010
11from zope.component import getUtility
11from zope.component.zcml import handler12from zope.component.zcml import handler
12from zope.configuration.fields import GlobalObject13from zope.configuration.fields import GlobalObject
13from zope.interface import Interface14from zope.interface import Interface
@@ -15,14 +16,15 @@
1516
1617
17from lazr.restful.declarations import (18from lazr.restful.declarations import (
18 LAZR_WEBSERVICE_EXPORTED, OPERATION_TYPES, generate_collection_adapter,19 LAZR_WEBSERVICE_EXPORTED, OPERATION_TYPES, REMOVED_OPERATION_TYPE,
19 generate_entry_adapter, generate_entry_interface,20 generate_collection_adapter, generate_entry_adapter,
20 generate_operation_adapter)21 generate_entry_interface, generate_operation_adapter)
21from lazr.restful.error import WebServiceExceptionView22from lazr.restful.error import WebServiceExceptionView
2223
23from lazr.restful.interfaces import (24from lazr.restful.interfaces import (
24 ICollection, IEntry, IResourceDELETEOperation, IResourceGETOperation,25 ICollection, IEntry, IResourceDELETEOperation, IResourceGETOperation,
25 IResourcePOSTOperation, IWebServiceClientRequest)26 IResourceOperation, IResourcePOSTOperation, IWebServiceClientRequest,
27 IWebServiceConfiguration, IWebServiceVersion)
2628
2729
28class IRegisterDirective(Interface):30class IRegisterDirective(Interface):
@@ -32,6 +34,41 @@
32 title=u'Module which will be inspected for webservice declarations')34 title=u'Module which will be inspected for webservice declarations')
3335
3436
37def register_adapter_for_version(factory, interface, version_name,
38 provides, name, info):
39 """A version-aware wrapper for the registerAdapter operation.
40
41 During web service generation we often need to register an adapter
42 for a particular version of the web service. The way to do this is
43 to register a multi-adapter using the interface being adapted to,
44 plus the marker interface for the web service version.
45
46 These marker interfaces are not available when the web service is
47 being generated, but the version strings are available. So methods
48 like register_webservice_operations use this function as a handler
49 for the second stage of ZCML processing.
50
51 This function simply looks up the appropriate marker interface and
52 calls Zope's handler('registerAdapter').
53 """
54 if version_name is None:
55 # When we were processing annotations we didn't know the name
56 # of the earliest supported version. We know this now.
57 utility = getUtility(IWebServiceConfiguration)
58 if len(utility.active_versions) > 0:
59 version_name = utility.active_versions[0]
60 else:
61 # This service only publishes a 'development' version.
62 version_name = utility.latest_version_uri_prefix
63 # Make sure the given version string has an
64 # IWebServiceVersion utility registered for it, and is not
65 # just a random string.
66 marker = getUtility(IWebServiceVersion, name=version_name)
67
68 handler('registerAdapter', factory, (interface, marker),
69 provides, name, info)
70
71
35def find_exported_interfaces(module):72def find_exported_interfaces(module):
36 """Find all the interfaces in a module marked for export.73 """Find all the interfaces in a module marked for export.
3774
@@ -95,32 +132,117 @@
95132
96133
97def register_webservice_operations(context, interface):134def register_webservice_operations(context, interface):
98 """Create and register adapters for all exported methods."""135 """Create and register adapters for all exported methods.
99136
137 Different versions of the web service may publish the same
138 operation differently or under different names.
139 """
100 for name, method in interface.namesAndDescriptions(True):140 for name, method in interface.namesAndDescriptions(True):
101 tag = method.queryTaggedValue(LAZR_WEBSERVICE_EXPORTED)141 tag = method.queryTaggedValue(LAZR_WEBSERVICE_EXPORTED)
102 if tag is None or tag['type'] not in OPERATION_TYPES:142 if tag is None or tag['type'] not in OPERATION_TYPES:
143 # This method is not published as a named operation.
103 continue144 continue
104 name = tag['as']145
105 if tag['type'] == 'read_operation':146 operation_name = None
106 provides = IResourceGETOperation147 # If an operation's name does not change between version n and
107 elif tag['type'] in ['factory', 'write_operation']:148 # version n+1, we want lookups for requests that come in for
108 provides = IResourcePOSTOperation149 # version n+1 to find the registered adapter for version n. This
109 elif tag['type'] in ['destructor']:150 # happens automatically. But if the operation name changes
110 provides = IResourceDELETEOperation151 # (because the operation is now published under a new name, or
111 name = ''152 # because the operation has been removed), we need to register a
112 else:153 # masking adapter: something that will stop version n+1's lookups
113 raise AssertionError('Unknown operation type: %s' % tag['type'])154 # from finding the adapter registered for version n. To this end
114 factory = generate_operation_adapter(method)155 # we keep track of what the operation looked like in the previous
115 context.action(156 # version.
116 discriminator=(157 previous_operation_name = None
117 'adapter', (interface, IWebServiceClientRequest),158 previous_operation_provides = None
118 provides, tag['as']),159 for version, tag in tag.stack:
119 callable=handler,160 if tag['type'] == REMOVED_OPERATION_TYPE:
120 args=('registerAdapter',161 # This operation is not present in this version.
121 factory, (interface, IWebServiceClientRequest), provides,162 # We'll represent this by setting the operation_name
122 name, context.info),163 # to None. If the operation was not present in the
123 )164 # previous version either (or there is no previous
165 # version), previous_operation_name will also be None
166 # and nothing will happen. If the operation was
167 # present in the previous version,
168 # previous_operation_name will not be None, and the
169 # code that handles name changes will install a
170 # masking adapter.
171 operation_name = None
172 operation_provides = None
173 factory = None
174 else:
175 if tag['type'] == 'read_operation':
176 operation_provides = IResourceGETOperation
177 elif tag['type']in ['factory', 'write_operation']:
178 operation_provides = IResourcePOSTOperation
179 elif tag['type'] in ['destructor']:
180 operation_provides = IResourceDELETEOperation
181 else:
182 # We know it's not REMOVED_OPERATION_TYPE, because
183 # that case is handled above.
184 raise AssertionError(
185 'Unknown operation type: %s' % tag['type'])
186 operation_name = tag.get('as')
187 if tag['type'] in ['destructor']:
188 operation_name = ''
189 factory = generate_operation_adapter(method, version)
190
191 # Operations are looked up by name. If the operation's
192 # name has changed from the previous version to this
193 # version, or if the operation was removed in this
194 # version, we need to block lookups of the previous name
195 # from working.
196 check = (previous_operation_name, previous_operation_provides,
197 operation_name, operation_provides, version, factory)
198 if (operation_name != previous_operation_name
199 and previous_operation_name is not None):
200 _add_versioned_adapter_action(
201 context, interface, previous_operation_provides,
202 previous_operation_name, version,
203 _mask_adapter_registration)
204
205 # If the operation exists in this version (ie. its name is
206 # not None), register it using this version's name.
207 if operation_name is not None:
208 _add_versioned_adapter_action(
209 context, interface, operation_provides, operation_name,
210 version, factory)
211 previous_operation_name = operation_name
212 previous_operation_provides = operation_provides
213
214
215def _mask_adapter_registration(*args):
216 """A factory function that stops an adapter lookup from succeeding.
217
218 This function is registered when it's necessary to explicitly stop
219 some part of web service version n from being visible to version n+1.
220 """
221 return None
222
223
224def _add_versioned_adapter_action(
225 context, interface, provides, name, version, factory):
226 """Helper function to register a versioned operation factory.
227
228 :param context: The context on which to add a registration action.
229 :param interface: The IEntry subclass that publishes the named
230 operation.
231 :param provides: The IResourceOperation subclass provided by the
232 factory.
233 :param name: The name of the named operation in this version.
234 :param version: The version of the web service that publishes this
235 named operation.
236 :param factory: The object that handles invocations of the named
237 operation.
238 """
239 context.action(
240 discriminator=(
241 'webservice versioned adapter',
242 (interface, IWebServiceClientRequest), provides, name, version),
243 callable=register_adapter_for_version,
244 args=(factory, interface, version, provides, name, context.info),
245 )
124246
125247
126def register_exception_view(context, exception):248def register_exception_view(context, exception):
127249
=== modified file 'src/lazr/restful/tests/test_webservice.py'
--- src/lazr/restful/tests/test_webservice.py 2010-01-14 17:08:20 +0000
+++ src/lazr/restful/tests/test_webservice.py 2010-01-25 20:02:11 +0000
@@ -53,9 +53,10 @@
53 :return: the factory (autogenerated class) that implements the operation53 :return: the factory (autogenerated class) that implements the operation
54 on the webservice.54 on the webservice.
55 """55 """
56 request_interface = getUtility(IWebServiceVersion, name='trunk')
56 return getGlobalSiteManager().adapters.lookup(57 return getGlobalSiteManager().adapters.lookup(
57 (model_interface, IWebServiceClientRequest),58 (model_interface, request_interface),
58 IResourceGETOperation, name=name)59 IResourceGETOperation, name=name)
5960
6061
61class IGenericEntry(Interface):62class IGenericEntry(Interface):

Subscribers

People subscribed via source and target branches