Merge lp:~leonardr/lazr.restful/benji-updated-size-link into lp:lazr.restful

Proposed by Leonard Richardson
Status: Merged
Approved by: Graham Binns
Approved revision: 160
Merge reported by: Leonard Richardson
Merged at revision: not available
Proposed branch: lp:~leonardr/lazr.restful/benji-updated-size-link
Merge into: lp:lazr.restful
Diff against target: 1233 lines (+451/-244)
21 files modified
setup.py (+1/-1)
src/lazr/restful/NEWS.txt (+9/-0)
src/lazr/restful/_operation.py (+28/-2)
src/lazr/restful/_resource.py (+48/-18)
src/lazr/restful/declarations.py (+19/-12)
src/lazr/restful/docs/webservice-declarations.txt (+62/-15)
src/lazr/restful/docs/webservice.txt (+1/-0)
src/lazr/restful/example/base/root.py (+1/-0)
src/lazr/restful/example/base/tests/collection.txt (+17/-8)
src/lazr/restful/example/base/tests/wadl.txt (+6/-5)
src/lazr/restful/example/multiversion/tests/wadl.txt (+29/-0)
src/lazr/restful/interfaces/_rest.py (+7/-0)
src/lazr/restful/simple.py (+2/-1)
src/lazr/restful/tales.py (+7/-1)
src/lazr/restful/templates/wadl-root.pt (+31/-8)
src/lazr/restful/testing/webservice.py (+3/-0)
src/lazr/restful/tests/test_utils.py (+23/-1)
src/lazr/restful/tests/test_webservice.py (+78/-2)
src/lazr/restful/utils.py (+12/-0)
src/lazr/restful/version.txt (+1/-1)
versions.cfg (+66/-169)
To merge this branch: bzr merge lp:~leonardr/lazr.restful/benji-updated-size-link
Reviewer Review Type Date Requested Status
Graham Binns (community) code Approve
Review via email: mp+32187@code.launchpad.net

Description of the change

This branch makes lazr.restful send a 'total_size_link' in lieu of a 'total_size', to spare the underlying data model the pain of calculating the total size of a collection all the time.

The branch has already been reviewed (https://code.edge.launchpad.net/~benji/lazr.restful/size-link/+merge/31971). I'm asking for a review of the changes benji and I made in response to the review. A diff of just these changes is here: http://paste.ubuntu.com/475892/

Summary of changes:

1. Rather than passing total_size_only as an argument into batch(), create a new method get_total_size and call it when the total size is desired, instead of calling batch().

2. When generating the WADL, use a new TALES method to determine whether or not total_size_link is active, rather than putting that boolean in the ZPT namespace. I had to rename namespace['context'] to namespace['service'] to get this to work, because there's code in the ZPT template that assigns local variables to 'context', clobbering namespace['context'].

3. Removed a no-longer-necessary named operation and field from webservice-declarations.txt. (This operation was introduced earlier in the branch, at a time when you had to explicitly tag a named operation to make use of total_size_only. Now, all named operations implicitly have total_size_only.)

To post a comment you must log in.
Revision history for this message
Graham Binns (gmb) wrote :

The changes in http://paste.ubuntu.com/475892/ look good; r=me.

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'setup.py'
--- setup.py 2010-03-04 14:27:58 +0000
+++ setup.py 2010-08-10 12:24:46 +0000
@@ -57,7 +57,7 @@
57 'docutils',57 'docutils',
58 'epydoc', # used by wadl generation58 'epydoc', # used by wadl generation
59 'grokcore.component==1.6',59 'grokcore.component==1.6',
60 'lazr.batchnavigator',60 'lazr.batchnavigator>=1.2.0-dev',
61 'lazr.delegates',61 'lazr.delegates',
62 'lazr.enum',62 'lazr.enum',
63 'lazr.lifecycle',63 'lazr.lifecycle',
6464
=== modified file 'src/lazr/restful/NEWS.txt'
--- src/lazr/restful/NEWS.txt 2010-08-05 14:01:44 +0000
+++ src/lazr/restful/NEWS.txt 2010-08-10 12:24:46 +0000
@@ -2,6 +2,15 @@
2NEWS for lazr.restful2NEWS for lazr.restful
3=====================3=====================
44
50.11.0 (unreleased)
6===================
7
8Added an optimization to total_size so that it is fetched via a link when
9possible. The new configuration option first_version_with_total_size_link
10specifies what version should be the first to expose the behavior. The default
11is for it to be enabled for all versions so set this option to preserve the
12earlier behavior for previously released web services.
13
50.10.0 (2010-08-05)140.10.0 (2010-08-05)
6===================15===================
716
817
=== modified file 'src/lazr/restful/_operation.py'
--- src/lazr/restful/_operation.py 2010-04-20 18:26:49 +0000
+++ src/lazr/restful/_operation.py 2010-08-10 12:24:46 +0000
@@ -44,6 +44,23 @@
44 def __init__(self, context, request):44 def __init__(self, context, request):
45 self.context = context45 self.context = context
46 self.request = request46 self.request = request
47 self.total_size_only = False
48
49 def total_size_link(self, navigator):
50 """Return a link to the total size of a collection."""
51 if getattr(self, 'include_total_size', True):
52 # This is a named operation that includes the total size
53 # inline rather than with a link.
54 return None
55 if not IResourceGETOperation.providedBy(self):
56 # Only GET operations can have their total size split out into
57 # a link, because only GET operations are safe.
58 return None
59 base = str(self.request.URL)
60 query = navigator.getCleanQueryString()
61 if query != '':
62 query += '&'
63 return base + '?' + query + "ws.show=total_size"
4764
48 def __call__(self):65 def __call__(self):
49 values, errors = self.validate()66 values, errors = self.validate()
@@ -80,13 +97,22 @@
80 # this object served to the client.97 # this object served to the client.
81 return result98 return result
8299
100 # The similar patterns in the two branches below suggest some deeper
101 # symmetry that should be extracted.
83 if queryMultiAdapter((result, self.request), ICollection):102 if queryMultiAdapter((result, self.request), ICollection):
84 # If the result is a web service collection, serve only one103 # If the result is a web service collection, serve only one
85 # batch of the collection.104 # batch of the collection.
86 collection = getMultiAdapter((result, self.request), ICollection)105 collection = getMultiAdapter((result, self.request), ICollection)
87 result = CollectionResource(collection, self.request).batch() + '}'106 resource = CollectionResource(collection, self.request)
107 if self.total_size_only:
108 result = resource.get_total_size(collection)
109 else:
110 result = resource.batch() + '}'
88 elif self.should_batch(result):111 elif self.should_batch(result):
89 result = self.batch(result, self.request) + '}'112 if self.total_size_only:
113 result = self.get_total_size(result)
114 else:
115 result = self.batch(result, self.request) + '}'
90 else:116 else:
91 # Serialize the result to JSON. Any embedded entries will be117 # Serialize the result to JSON. Any embedded entries will be
92 # automatically serialized.118 # automatically serialized.
93119
=== modified file 'src/lazr/restful/_resource.py'
--- src/lazr/restful/_resource.py 2010-08-04 18:25:21 +0000
+++ src/lazr/restful/_resource.py 2010-08-10 12:24:46 +0000
@@ -110,6 +110,7 @@
110 status_reasons[code] = reason110 status_reasons[code] = reason
111init_status_codes()111init_status_codes()
112112
113
113def decode_value(value):114def decode_value(value):
114 """Return a unicode value curresponding to `value`."""115 """Return a unicode value curresponding to `value`."""
115 if isinstance(value, unicode):116 if isinstance(value, unicode):
@@ -589,27 +590,45 @@
589590
590 """A mixin for resources that need to batch lists of entries."""591 """A mixin for resources that need to batch lists of entries."""
591592
592 def __init__(self, context, request):593 # TODO: determine real need for __init__ and super() call
593 """A basic constructor."""594
594 # Like all mixin classes, this class is designed to be used595 def total_size_link(self, navigator):
595 # with multiple inheritance. That requires defining __init__596 """Return the URL to fetch to find out the collection's total size.
596 # to call the next constructor in the chain, which means using597
597 # super() even though this class itself has no superclass.598 If this is None, the total size will be included inline.
598 super(BatchingResourceMixin, self).__init__(context, request)599
600 :param navigator: A BatchNavigator object for the current batch.
601 """
602 return None
603
604 def get_total_size(self, entries):
605 """Get the number of items in entries.
606
607 :return: a JSON string representing the number of objects in the list
608 """
609 if not hasattr(entries, '__len__'):
610 entries = IFiniteSequence(entries)
611
612 return simplejson.dumps(len(entries))
613
599614
600 def batch(self, entries, request):615 def batch(self, entries, request):
601 """Prepare a batch from a (possibly huge) list of entries.616 """Prepare a batch from a (possibly huge) list of entries.
602617
603 :return: A JSON string representing a hash:618 :return: a JSON string representing a hash:
619
604 'entries' contains a list of EntryResource objects for the620 'entries' contains a list of EntryResource objects for the
605 entries that actually made it into this batch621 entries that actually made it into this batch
606 'total_size' contains the total size of the list.622 'total_size' contains the total size of the list.
623 'total_size_link' contains a link to the total size of the list.
607 'next_url', if present, contains a URL to get the next batch624 'next_url', if present, contains a URL to get the next batch
608 in the list.625 in the list.
609 'prev_url', if present, contains a URL to get the previous batch626 'prev_url', if present, contains a URL to get the previous batch
610 in the list.627 in the list.
611 'start' contains the starting index of this batch628 'start' contains the starting index of this batch
612629
630 Only one of 'total_size' or 'total_size_link' will be present.
631
613 Note that the JSON string will be missing its final curly632 Note that the JSON string will be missing its final curly
614 brace. This is in case the caller wants to add some additional633 brace. This is in case the caller wants to add some additional
615 keys to the JSON hash. It's the caller's responsibility to add634 keys to the JSON hash. It's the caller's responsibility to add
@@ -620,11 +639,12 @@
620 navigator = WebServiceBatchNavigator(entries, request)639 navigator = WebServiceBatchNavigator(entries, request)
621640
622 view_permission = getUtility(IWebServiceConfiguration).view_permission641 view_permission = getUtility(IWebServiceConfiguration).view_permission
623 resources = [EntryResource(entry, request)642 batch = { 'start' : navigator.batch.start }
624 for entry in navigator.batch643 total_size_link = self.total_size_link(navigator)
625 if checkPermission(view_permission, entry)]644 if total_size_link is None:
626 batch = { 'total_size' : navigator.batch.listlength,645 batch['total_size'] = navigator.batch.listlength
627 'start' : navigator.batch.start }646 else:
647 batch['total_size_link'] = total_size_link
628 if navigator.batch.start < 0:648 if navigator.batch.start < 0:
629 batch['start'] = None649 batch['start'] = None
630 next_url = navigator.nextBatchURL()650 next_url = navigator.nextBatchURL()
@@ -637,6 +657,9 @@
637657
638 # String together a bunch of entry representations, possibly658 # String together a bunch of entry representations, possibly
639 # obtained from a representation cache.659 # obtained from a representation cache.
660 resources = [EntryResource(entry, request)
661 for entry in navigator.batch
662 if checkPermission(view_permission, entry)]
640 entry_strings = [663 entry_strings = [
641 resource._representation(HTTPResource.JSON_TYPE)664 resource._representation(HTTPResource.JSON_TYPE)
642 for resource in resources]665 for resource in resources]
@@ -674,6 +697,10 @@
674 except ComponentLookupError:697 except ComponentLookupError:
675 self.request.response.setStatus(400)698 self.request.response.setStatus(400)
676 return "No such operation: " + operation_name699 return "No such operation: " + operation_name
700
701 show = self.request.form.get('ws.show')
702 if show == 'total_size':
703 operation.total_size_only = True
677 return operation()704 return operation()
678705
679 def handleCustomPOST(self, operation_name):706 def handleCustomPOST(self, operation_name):
@@ -1664,14 +1691,17 @@
1664 self.request.response.setHeader('Content-type', self.JSON_TYPE)1691 self.request.response.setHeader('Content-type', self.JSON_TYPE)
1665 return result1692 return result
16661693
1667 def batch(self, entries=None):1694 def batch(self, entries=None, request=None):
1668 """Return a JSON representation of a batch of entries.1695 """Return a JSON representation of a batch of entries.
16691696
1670 :param entries: (Optional) A precomputed list of entries to batch.1697 :param entries: (Optional) A precomputed list of entries to batch.
1698 :param request: (Optional) The current request.
1671 """1699 """
1672 if entries is None:1700 if entries is None:
1673 entries = self.collection.find()1701 entries = self.collection.find()
1674 result = super(CollectionResource, self).batch(entries, self.request)1702 if request is None:
1703 request = self.request
1704 result = super(CollectionResource, self).batch(entries, request)
1675 result += (1705 result += (
1676 ', "resource_type_link" : ' + simplejson.dumps(self.type_url)1706 ', "resource_type_link" : ' + simplejson.dumps(self.type_url)
1677 + '}')1707 + '}')
@@ -1862,7 +1892,7 @@
1862 # by the entry classes.1892 # by the entry classes.
1863 collection_classes.append(registration.factory)1893 collection_classes.append(registration.factory)
1864 namespace = self.WADL_TEMPLATE.pt_getContext()1894 namespace = self.WADL_TEMPLATE.pt_getContext()
1865 namespace['context'] = self1895 namespace['service'] = self
1866 namespace['request'] = self.request1896 namespace['request'] = self.request
1867 namespace['entries'] = entry_classes1897 namespace['entries'] = entry_classes
1868 namespace['collections'] = collection_classes1898 namespace['collections'] = collection_classes
@@ -2061,13 +2091,13 @@
2061 def singular_type(self):2091 def singular_type(self):
2062 """Return the singular name for this object type."""2092 """Return the singular name for this object type."""
2063 interface = self.entry_interface2093 interface = self.entry_interface
2064 return interface.queryTaggedValue(LAZR_WEBSERVICE_NAME)['singular']2094 return interface.getTaggedValue(LAZR_WEBSERVICE_NAME)['singular']
20652095
2066 @property2096 @property
2067 def plural_type(self):2097 def plural_type(self):
2068 """Return the plural name for this object type."""2098 """Return the plural name for this object type."""
2069 interface = self.entry_interface2099 interface = self.entry_interface
2070 return interface.queryTaggedValue(LAZR_WEBSERVICE_NAME)['plural']2100 return interface.getTaggedValue(LAZR_WEBSERVICE_NAME)['plural']
20712101
2072 @property2102 @property
2073 def type_link(self):2103 def type_link(self):
20742104
=== modified file 'src/lazr/restful/declarations.py'
--- src/lazr/restful/declarations.py 2010-08-04 18:25:21 +0000
+++ src/lazr/restful/declarations.py 2010-08-10 12:24:46 +0000
@@ -63,7 +63,7 @@
63from lazr.restful.security import protect_schema63from lazr.restful.security import protect_schema
64from lazr.restful.utils import (64from lazr.restful.utils import (
65 camelcase_to_underscore_separated, get_current_web_service_request,65 camelcase_to_underscore_separated, get_current_web_service_request,
66 make_identifier_safe, VersionedDict)66 make_identifier_safe, VersionedDict, is_total_size_link_active)
6767
68LAZR_WEBSERVICE_EXPORTED = '%s.exported' % LAZR_WEBSERVICE_NS68LAZR_WEBSERVICE_EXPORTED = '%s.exported' % LAZR_WEBSERVICE_NS
69LAZR_WEBSERVICE_MUTATORS = '%s.exported.mutators' % LAZR_WEBSERVICE_NS69LAZR_WEBSERVICE_MUTATORS = '%s.exported.mutators' % LAZR_WEBSERVICE_NS
@@ -712,10 +712,9 @@
712class operation_returns_collection_of(_method_annotator):712class operation_returns_collection_of(_method_annotator):
713 """Specify that the exported operation returns a collection.713 """Specify that the exported operation returns a collection.
714714
715 The decorator takes a single argument: an interface that's been715 The decorator takes one required argument, "schema", an interface that's
716 exported as an entry.716 been exported as an entry.
717 """717 """
718
719 def __init__(self, schema):718 def __init__(self, schema):
720 _check_called_from_interface_def('%s()' % self.__class__.__name__)719 _check_called_from_interface_def('%s()' % self.__class__.__name__)
721 if not IInterface.providedBy(schema):720 if not IInterface.providedBy(schema):
@@ -1209,23 +1208,22 @@
1209 version = getUtility(IWebServiceConfiguration).active_versions[0]1208 version = getUtility(IWebServiceConfiguration).active_versions[0]
12101209
1211 bases = (BaseResourceOperationAdapter, )1210 bases = (BaseResourceOperationAdapter, )
1212 if tag['type'] == 'read_operation':1211 operation_type = tag['type']
1212 if operation_type == 'read_operation':
1213 prefix = 'GET'1213 prefix = 'GET'
1214 provides = IResourceGETOperation1214 provides = IResourceGETOperation
1215 elif tag['type'] in ('factory', 'write_operation'):1215 elif operation_type in ('factory', 'write_operation'):
1216 provides = IResourcePOSTOperation1216 provides = IResourcePOSTOperation
1217 prefix = 'POST'1217 prefix = 'POST'
1218 if tag['type'] == 'factory':1218 if operation_type == 'factory':
1219 bases = (BaseFactoryResourceOperationAdapter,)1219 bases = (BaseFactoryResourceOperationAdapter,)
1220 elif tag['type'] == 'destructor':1220 elif operation_type == 'destructor':
1221 provides = IResourceDELETEOperation1221 provides = IResourceDELETEOperation
1222 prefix = 'DELETE'1222 prefix = 'DELETE'
1223 else:1223 else:
1224 raise AssertionError('Unknown method export type: %s' % tag['type'])1224 raise AssertionError('Unknown method export type: %s' % operation_type)
12251225
1226 return_type = tag['return_type']1226 return_type = tag['return_type']
1227 if return_type is None:
1228 return_type = None
12291227
1230 name = _versioned_class_name(1228 name = _versioned_class_name(
1231 '%s_%s_%s' % (prefix, method.interface.__name__, tag['as']),1229 '%s_%s_%s' % (prefix, method.interface.__name__, tag['as']),
@@ -1238,7 +1236,16 @@
1238 '_method_name': method.__name__,1236 '_method_name': method.__name__,
1239 '__doc__': method.__doc__}1237 '__doc__': method.__doc__}
12401238
1241 if tag['type'] == 'write_operation':1239 if isinstance(return_type, CollectionField):
1240 # If the version we're being asked for is equal to or later than the
1241 # version in which we started exposing total_size_link and this is a
1242 # read operation, then include it, otherwise include total_size.
1243 config = getUtility(IWebServiceConfiguration)
1244 class_dict['include_total_size'] = not (
1245 is_total_size_link_active(version, config) and
1246 operation_type == 'read_operation')
1247
1248 if operation_type == 'write_operation':
1242 class_dict['send_modification_event'] = True1249 class_dict['send_modification_event'] = True
1243 factory = type(name, bases, class_dict)1250 factory = type(name, bases, class_dict)
1244 classImplements(factory, provides)1251 classImplements(factory, provides)
12451252
=== modified file 'src/lazr/restful/docs/webservice-declarations.txt'
--- src/lazr/restful/docs/webservice-declarations.txt 2010-08-02 20:10:33 +0000
+++ src/lazr/restful/docs/webservice-declarations.txt 2010-08-10 12:24:46 +0000
@@ -32,7 +32,7 @@
32field, but not the inventory_number field.32field, but not the inventory_number field.
3333
34 >>> from zope.interface import Interface34 >>> from zope.interface import Interface
35 >>> from zope.schema import TextLine, Float, List35 >>> from zope.schema import Text, TextLine, Float, List
36 >>> from lazr.restful.declarations import (36 >>> from lazr.restful.declarations import (
37 ... export_as_webservice_entry, exported)37 ... export_as_webservice_entry, exported)
38 >>> class IBook(Interface):38 >>> class IBook(Interface):
@@ -213,7 +213,7 @@
213 TypeError: export_as_webservice_collection() is missing a method213 TypeError: export_as_webservice_collection() is missing a method
214 tagged with @collection_default_content.214 tagged with @collection_default_content.
215215
216As it is an error, to mark more than one methods:216As it is an error, to mark more than one method:
217217
218 >>> class TwoDefaultContent(Interface):218 >>> class TwoDefaultContent(Interface):
219 ... export_as_webservice_collection(IDummyInterface)219 ... export_as_webservice_collection(IDummyInterface)
@@ -330,8 +330,8 @@
330 ... text=copy_field(IBook['title'], title=u'Text to search for.'))330 ... text=copy_field(IBook['title'], title=u'Text to search for.'))
331 ... @operation_returns_collection_of(IBook)331 ... @operation_returns_collection_of(IBook)
332 ... @export_read_operation()332 ... @export_read_operation()
333 ... def searchBooks(text):333 ... def searchBookTitles(text):
334 ... """Return list of books containing 'text'."""334 ... """Return list of books whose titles contain 'text'."""
335 ...335 ...
336 ... @operation_parameters(336 ... @operation_parameters(
337 ... text=copy_field(IBook['title'], title=u'Text to search for.'))337 ... text=copy_field(IBook['title'], title=u'Text to search for.'))
@@ -393,11 +393,11 @@
393 return_type: <lazr.restful._operation.ObjectLink object...>393 return_type: <lazr.restful._operation.ObjectLink object...>
394 type: 'factory'394 type: 'factory'
395395
396We did specify the return type for the 'searchBooks' method: it396We did specify the return type for the 'searchBookTitles' method: it
397returns a collection.397returns a collection.
398398
399 >>> print_export_tag(IBookSetOnSteroids['searchBooks'])399 >>> print_export_tag(IBookSetOnSteroids['searchBookTitles'])
400 as: 'searchBooks'400 as: 'searchBookTitles'
401 call_with: {}401 call_with: {}
402 params: {'text': <...TextLine...>}402 params: {'text': <...TextLine...>}
403 return_type: <lazr.restful.fields.CollectionField object...>403 return_type: <lazr.restful.fields.CollectionField object...>
@@ -920,7 +920,8 @@
920 >>> class Book(object):920 >>> class Book(object):
921 ... """Simple IBook implementation."""921 ... """Simple IBook implementation."""
922 ... implements(IBook)922 ... implements(IBook)
923 ... def __init__(self, author, title, base_price, inventory_number):923 ... def __init__(self, author, title, base_price,
924 ... inventory_number):
924 ... self.author = author925 ... self.author = author
925 ... self.title = title926 ... self.title = title
926 ... self.base_price = base_price927 ... self.base_price = base_price
@@ -937,6 +938,7 @@
937 >>> class MyWebServiceConfiguration(TestWebServiceConfiguration):938 >>> class MyWebServiceConfiguration(TestWebServiceConfiguration):
938 ... active_versions = ["beta", "1.0", "2.0", "3.0"]939 ... active_versions = ["beta", "1.0", "2.0", "3.0"]
939 ... last_version_with_mutator_named_operations = "1.0"940 ... last_version_with_mutator_named_operations = "1.0"
941 ... first_version_with_total_size_link = "2.0"
940 ... code_revision = "1.0b"942 ... code_revision = "1.0b"
941 ... default_batch_size = 50943 ... default_batch_size = 50
942 >>> provideUtility(MyWebServiceConfiguration(), IWebServiceConfiguration)944 >>> provideUtility(MyWebServiceConfiguration(), IWebServiceConfiguration)
@@ -1087,7 +1089,7 @@
1087 ... generate_operation_adapter)1089 ... generate_operation_adapter)
10881090
1089 >>> read_method_adapter_factory = generate_operation_adapter(1091 >>> read_method_adapter_factory = generate_operation_adapter(
1090 ... IBookSetOnSteroids['searchBooks'])1092 ... IBookSetOnSteroids['searchBookTitles'])
1091 >>> IResourceGETOperation.implementedBy(read_method_adapter_factory)1093 >>> IResourceGETOperation.implementedBy(read_method_adapter_factory)
1092 True1094 True
10931095
@@ -1099,14 +1101,14 @@
10991101
1100 >>> from lazr.restful import ResourceOperation1102 >>> from lazr.restful import ResourceOperation
1101 >>> read_method_adapter_factory.__name__1103 >>> read_method_adapter_factory.__name__
1102 'GET_IBookSetOnSteroids_searchBooks_beta'1104 'GET_IBookSetOnSteroids_searchBookTitles_beta'
1103 >>> issubclass(read_method_adapter_factory, ResourceOperation)1105 >>> issubclass(read_method_adapter_factory, ResourceOperation)
1104 True1106 True
11051107
1106The adapter's docstring is taken from the decorated method docstring.1108The adapter's docstring is taken from the decorated method docstring.
11071109
1108 >>> read_method_adapter_factory.__doc__1110 >>> read_method_adapter_factory.__doc__
1109 "Return list of books containing 'text'."1111 "Return list of books whose titles contain 'text'."
11101112
1111The adapter's params attribute contains the specification of the1113The adapter's params attribute contains the specification of the
1112parameters accepted by the operation.1114parameters accepted by the operation.
@@ -1126,7 +1128,7 @@
1126 ...1128 ...
1127 ... result = None1129 ... result = None
1128 ...1130 ...
1129 ... def searchBooks(self, text):1131 ... def searchBookTitles(self, text):
1130 ... return self.result1132 ... return self.result
1131 ...1133 ...
1132 ... def new(self, author, base_price, title):1134 ... def new(self, author, base_price, title):
@@ -1148,7 +1150,7 @@
1148return value is a dictionary containing a batched list.1150return value is a dictionary containing a batched list.
11491151
1150 >>> print read_method_adapter.call(text='')1152 >>> print read_method_adapter.call(text='')
1151 {"total_size": 0, "start": null, "entries": []}1153 {"total_size": 0, "start": 0, "entries": []}
11521154
1153Methods exported as a write operations generates an adapter providing1155Methods exported as a write operations generates an adapter providing
1154IResourcePOSTOperation.1156IResourcePOSTOperation.
@@ -1649,6 +1651,51 @@
1649 AssertionError: 'IMultiVersionCollection' isn't tagged for export1651 AssertionError: 'IMultiVersionCollection' isn't tagged for export
1650 to web service version 'NoSuchVersion'.1652 to web service version 'NoSuchVersion'.
16511653
1654total_size_link
1655~~~~~~~~~~~~~~~
1656
1657Collections previously exposed their total size via a `total_size` attribute.
1658However, newer versions of lazr.restful expose a `total_size_link` intead. To
1659facilitate transitioning from one approach to the other the configuration
1660option `first_version_with_total_size_link` has been added to
1661IWebServiceConfiguration.
1662
1663By default the first_version_with_total_size_link is set to the earliest
1664available web service version, but if you have stable versions of your web
1665service you wish to maintain compatability with you can specify the version in
1666which you want the new behavior to take effect in the web service
1667configuration.
1668
1669 >>> from zope.component import getUtility
1670 >>> config = getUtility(IWebServiceConfiguration)
1671 >>> config.last_version_with_mutator_named_operations = '1.0'
1672
1673 >>> from lazr.restful.declarations import (
1674 ... export_read_operation, operation_returns_collection_of,
1675 ... operation_for_version)
1676 >>> class IWithMultiVersionCollection(Interface):
1677 ... export_as_webservice_entry()
1678 ...
1679 ... @operation_for_version('2.0')
1680 ... @operation_for_version('1.0')
1681 ... @operation_returns_collection_of(Interface)
1682 ... @export_read_operation()
1683 ... def method():
1684 ... """A method that returns a collection."""
1685
1686 >>> method = IWithMultiVersionCollection['method']
1687 >>> dummy_data = None # this would be an intance that has the method
1688 >>> v10 = generate_operation_adapter(method, '1.0')(dummy_data, request)
1689 >>> v20 = generate_operation_adapter(method, '2.0')(dummy_data, request)
1690
1691We can see that version 1.0 includes the total size for backward compatability
1692while version 2.0 includes a link to fetch the total size.
1693
1694 >>> v10.include_total_size
1695 True
1696 >>> v20.include_total_size
1697 False
1698
1652Entries1699Entries
1653-------1700-------
16541701
@@ -2541,8 +2588,8 @@
2541 >>> request_interface = IWebServiceClientRequest2588 >>> request_interface = IWebServiceClientRequest
2542 >>> adapter_registry.lookup(2589 >>> adapter_registry.lookup(
2543 ... (IBookSetOnSteroids, request_interface),2590 ... (IBookSetOnSteroids, request_interface),
2544 ... IResourceGETOperation, 'searchBooks')2591 ... IResourceGETOperation, 'searchBookTitles')
2545 <class '...GET_IBookSetOnSteroids_searchBooks_beta'>2592 <class '...GET_IBookSetOnSteroids_searchBookTitles_beta'>
2546 >>> adapter_registry.lookup(2593 >>> adapter_registry.lookup(
2547 ... (IBookSetOnSteroids, request_interface),2594 ... (IBookSetOnSteroids, request_interface),
2548 ... IResourcePOSTOperation, 'create_book')2595 ... IResourcePOSTOperation, 'create_book')
25492596
=== modified file 'src/lazr/restful/docs/webservice.txt'
--- src/lazr/restful/docs/webservice.txt 2010-08-04 18:25:21 +0000
+++ src/lazr/restful/docs/webservice.txt 2010-08-10 12:24:46 +0000
@@ -503,6 +503,7 @@
503 ... code_revision = 'test'503 ... code_revision = 'test'
504 ... max_batch_size = 100504 ... max_batch_size = 100
505 ... directives.publication_class(WebServiceTestPublication)505 ... directives.publication_class(WebServiceTestPublication)
506 ... first_version_with_total_size_link = 'devel'
506507
507 >>> from grokcore.component.testing import grok_component508 >>> from grokcore.component.testing import grok_component
508 >>> ignore = grok_component(509 >>> ignore = grok_component(
509510
=== modified file 'src/lazr/restful/example/base/root.py'
--- src/lazr/restful/example/base/root.py 2010-06-14 14:09:31 +0000
+++ src/lazr/restful/example/base/root.py 2010-08-10 12:24:46 +0000
@@ -398,6 +398,7 @@
398 <p>Don't use this unless you like changing things.</p>"""398 <p>Don't use this unless you like changing things.</p>"""
399 }399 }
400 last_version_with_mutator_named_operations = None400 last_version_with_mutator_named_operations = None
401 first_version_with_total_size_link = None
401 use_https = False402 use_https = False
402 view_permission = 'lazr.restful.example.base.View'403 view_permission = 'lazr.restful.example.base.View'
403404
404405
=== modified file 'src/lazr/restful/example/base/tests/collection.txt'
--- src/lazr/restful/example/base/tests/collection.txt 2009-04-24 15:14:07 +0000
+++ src/lazr/restful/example/base/tests/collection.txt 2010-08-10 12:24:46 +0000
@@ -173,14 +173,12 @@
173 ... "/cookbooks?ws.op=find_recipes&%s" % args).jsonBody()173 ... "/cookbooks?ws.op=find_recipes&%s" % args).jsonBody()
174174
175 >>> s_recipes = search_recipes("chicken")175 >>> s_recipes = search_recipes("chicken")
176 >>> s_recipes['total_size']
177 3
178 >>> sorted(r['instructions'] for r in s_recipes['entries'])176 >>> sorted(r['instructions'] for r in s_recipes['entries'])
179 [u'Draw, singe, stuff, and truss...', u'You can always judge...']177 [u'Draw, singe, stuff, and truss...', u'You can always judge...']
180178
181 >>> veg_recipes = search_recipes("chicken", True)179 >>> veg_recipes = search_recipes("chicken", True)
182 >>> veg_recipes['total_size']180 >>> veg_recipes['entries']
183 0181 []
184182
185A custom operation that returns a list of objects is paginated, just183A custom operation that returns a list of objects is paginated, just
186like a collection.184like a collection.
@@ -196,11 +194,21 @@
196empty list of results:194empty list of results:
197195
198 >>> empty_collection = search_recipes("nosuchrecipe")196 >>> empty_collection = search_recipes("nosuchrecipe")
199 >>> empty_collection['total_size']
200 0
201 >>> [r['instructions'] for r in empty_collection['entries']]197 >>> [r['instructions'] for r in empty_collection['entries']]
202 []198 []
203199
200When an operation yields a collection of objects, the representation
201includes a link that yields the total size of the collection.
202
203 >>> print s_recipes['total_size_link']
204 http://.../cookbooks?search=chicken&vegetarian=false&ws.op=find_recipes&ws.show=total_size
205
206Sending a GET request to that link yields a JSON representation of the
207total size.
208
209 >>> print webservice.get(s_recipes['total_size_link']).jsonBody()
210 3
211
204Custom operations may have error handling. In this case, the error212Custom operations may have error handling. In this case, the error
205handling is in the validate() method of the 'search' field.213handling is in the validate() method of the 'search' field.
206214
@@ -216,8 +224,9 @@
216224
217 >>> general_cookbooks = webservice.get(225 >>> general_cookbooks = webservice.get(
218 ... "/cookbooks?ws.op=find_for_cuisine&cuisine=General")226 ... "/cookbooks?ws.op=find_for_cuisine&cuisine=General")
219 >>> general_cookbooks.jsonBody()['total_size']227 >>> print general_cookbooks.jsonBody()['total_size_link']
220 3228 http://cookbooks.dev/devel/cookbooks?cuisine=General&ws.op=find_for_cuisine&ws.show=total_size
229
221230
222POST operations231POST operations
223===============232===============
224233
=== modified file 'src/lazr/restful/example/base/tests/wadl.txt'
--- src/lazr/restful/example/base/tests/wadl.txt 2010-03-10 18:45:04 +0000
+++ src/lazr/restful/example/base/tests/wadl.txt 2010-08-10 12:24:46 +0000
@@ -537,8 +537,9 @@
537'entry_resource_descriptions'.537'entry_resource_descriptions'.
538538
539 >>> entry_resource_descriptions = []539 >>> entry_resource_descriptions = []
540 >>> entry_resource_types = other_children[first_entry_type_index:-2]540 >>> entry_resource_types = other_children[first_entry_type_index:-3]
541 >>> hosted_binary_resource_type, simple_binary_type = other_children[-2:]541 >>> (hosted_binary_resource_type, scalar_type, simple_binary_type
542 ... ) = other_children[-3:]
542 >>> for index in range(0, len(entry_resource_types), 5):543 >>> for index in range(0, len(entry_resource_types), 5):
543 ... entry_resource_descriptions.append(544 ... entry_resource_descriptions.append(
544 ... (tuple(entry_resource_types[index:index + 5])))545 ... (tuple(entry_resource_types[index:index + 5])))
@@ -1155,9 +1156,9 @@
1155All collection representations have the same five <param> tags.1156All collection representations have the same five <param> tags.
11561157
1157 >>> [param.attrib['name'] for param in collection_rep]1158 >>> [param.attrib['name'] for param in collection_rep]
1158 ['resource_type_link', 'total_size', 'start', 'next_collection_link',1159 ['resource_type_link', 'total_size', 'total_size_link', 'start',
1159 'prev_collection_link', 'entries', 'entry_links']1160 'next_collection_link', 'prev_collection_link', 'entries', 'entry_links']
1160 >>> (type_link, size, start, next, prev, entries,1161 >>> (type_link, size, size_link, start, next, prev, entries,
1161 ... entry_links) = collection_rep1162 ... entry_links) = collection_rep
11621163
1163So what's the difference between a collection of people and a1164So what's the difference between a collection of people and a
11641165
=== modified file 'src/lazr/restful/example/multiversion/tests/wadl.txt'
--- src/lazr/restful/example/multiversion/tests/wadl.txt 2010-03-10 20:44:03 +0000
+++ src/lazr/restful/example/multiversion/tests/wadl.txt 2010-08-10 12:24:46 +0000
@@ -92,3 +92,32 @@
92 >>> pair_collection = contents['pair_collection']92 >>> pair_collection = contents['pair_collection']
93 >>> sorted([method.attrib['id'] for method in pair_collection])93 >>> sorted([method.attrib['id'] for method in pair_collection])
94 ['key_value_pairs-get']94 ['key_value_pairs-get']
95
96total_size_link
97===============
98
99The version in which total_size_link is introduced is controlled by the
100first_version_with_total_size_link attribute of the web service configuration
101(IWebServiceConfiguration) utility.
102
103We'll configure the web service to begin including `total_size_link` values
104in version 3.0:
105
106 >>> from zope.component import getUtility
107 >>> from lazr.restful.interfaces import IWebServiceConfiguration
108 >>> config = getUtility(IWebServiceConfiguration)
109 >>> config.first_version_with_total_size_link = '3.0'
110
111Now if we request the WADL for 3.0 it will include a description of
112total_size_link.
113
114 >>> webservice.get('/', media_type='application/vnd.sun.wadl+xml',
115 ... api_version='3.0').body
116 '...<wadl:param style="plain" name="total_size_link"...'
117
118If we request an earlier version, total_size_link is not described.
119
120 >>> wadl = webservice.get('/', media_type='application/vnd.sun.wadl+xml',
121 ... api_version='2.0').body
122 >>> 'total_size_link' in wadl
123 False
95124
=== modified file 'src/lazr/restful/interfaces/_rest.py'
--- src/lazr/restful/interfaces/_rest.py 2010-06-03 14:42:44 +0000
+++ src/lazr/restful/interfaces/_rest.py 2010-08-10 12:24:46 +0000
@@ -507,6 +507,13 @@
507 all subsequent versions, they will not be published as named507 all subsequent versions, they will not be published as named
508 operations.""")508 operations.""")
509509
510 first_version_with_total_size_link = TextLine(
511 default=None,
512 description=u"""In earlier versions of lazr.restful collections
513 included a total_size field, now they include a total_size_link
514 instead. Setting this value determines in which version the new
515 behavior takes effect.""")
516
510 code_revision = TextLine(517 code_revision = TextLine(
511 default=u"",518 default=u"",
512 description=u"""A string designating the current revision519 description=u"""A string designating the current revision
513520
=== modified file 'src/lazr/restful/simple.py'
--- src/lazr/restful/simple.py 2010-06-14 18:47:39 +0000
+++ src/lazr/restful/simple.py 2010-08-10 12:24:46 +0000
@@ -477,5 +477,6 @@
477477
478478
479BaseWebServiceConfiguration = implement_from_dict(479BaseWebServiceConfiguration = implement_from_dict(
480 "BaseWebServiceConfiguration", IWebServiceConfiguration, {}, object)480 "BaseWebServiceConfiguration", IWebServiceConfiguration,
481 {'first_version_with_total_size_link': None}, object)
481482
482483
=== modified file 'src/lazr/restful/tales.py'
--- src/lazr/restful/tales.py 2010-03-10 20:44:03 +0000
+++ src/lazr/restful/tales.py 2010-08-10 12:24:46 +0000
@@ -34,7 +34,8 @@
34 IResourceOperation, IResourcePOSTOperation, IScopedCollection,34 IResourceOperation, IResourcePOSTOperation, IScopedCollection,
35 ITopLevelEntryLink, IWebServiceClientRequest, IWebServiceConfiguration,35 ITopLevelEntryLink, IWebServiceClientRequest, IWebServiceConfiguration,
36 IWebServiceVersion, LAZR_WEBSERVICE_NAME)36 IWebServiceVersion, LAZR_WEBSERVICE_NAME)
37from lazr.restful.utils import get_current_web_service_request37from lazr.restful.utils import (get_current_web_service_request,
38 is_total_size_link_active)
3839
3940
40class WadlDocstringLinker(DocstringLinker):41class WadlDocstringLinker(DocstringLinker):
@@ -234,6 +235,11 @@
234 return config.version_descriptions.get(self.service_version, None)235 return config.version_descriptions.get(self.service_version, None)
235236
236 @property237 @property
238 def is_total_size_link_active(self):
239 config = getUtility(IWebServiceConfiguration)
240 return is_total_size_link_active(self.resource.request.version, config)
241
242 @property
237 def top_level_resources(self):243 def top_level_resources(self):
238 """Return a list of dicts describing the top-level resources."""244 """Return a list of dicts describing the top-level resources."""
239 resource_dicts = []245 resource_dicts = []
240246
=== modified file 'src/lazr/restful/templates/wadl-root.pt'
--- src/lazr/restful/templates/wadl-root.pt 2010-03-10 18:50:27 +0000
+++ src/lazr/restful/templates/wadl-root.pt 2010-08-10 12:24:46 +0000
@@ -13,21 +13,21 @@
1313
14 <wadl:doc xmlns="http://www.w3.org/1999/xhtml"14 <wadl:doc xmlns="http://www.w3.org/1999/xhtml"
15 title="About this service"15 title="About this service"
16 tal:content="structure context/wadl:description">16 tal:content="structure service/wadl:description">
17 Version-independent description of the web service.17 Version-independent description of the web service.
18 </wadl:doc>18 </wadl:doc>
1919
20 <wadl:doc xmlns="http://www.w3.org/1999/xhtml"20 <wadl:doc xmlns="http://www.w3.org/1999/xhtml"
21 tal:define="version context/wadl:service_version"21 tal:define="version service/wadl:service_version"
22 tal:attributes="title string:About version ${version}"22 tal:attributes="title string:About version ${version}"
23 tal:content="structure context/wadl:version_description">23 tal:content="structure service/wadl:version_description">
24 Description of this version of the web service.24 Description of this version of the web service.
25 </wadl:doc>25 </wadl:doc>
2626
27 <!--There is one "service root" resource, located (as you'd expect)27 <!--There is one "service root" resource, located (as you'd expect)
28 at the service root. This very document is the WADL28 at the service root. This very document is the WADL
29 representation of the "service root" resource.-->29 representation of the "service root" resource.-->
30 <resources tal:attributes="base context/wadl:url">30 <resources tal:attributes="base service/wadl:url">
31 <resource path="" type="#service-root" />31 <resource path="" type="#service-root" />
32 </resources>32 </resources>
3333
@@ -46,7 +46,7 @@
46 <!--The JSON representation of a "service root" resource contains a46 <!--The JSON representation of a "service root" resource contains a
47 number of links to collection-type resources.-->47 number of links to collection-type resources.-->
48 <representation mediaType="application/json" id="service-root-json">48 <representation mediaType="application/json" id="service-root-json">
49 <tal:root_params tal:repeat="param context/wadl:top_level_resources">49 <tal:root_params tal:repeat="param service/wadl:top_level_resources">
50 <param style="plain" tal:attributes="name param/name;50 <param style="plain" tal:attributes="name param/name;
51 path param/path">51 path param/path">
52 <link tal:attributes="resource_type param/resource/wadl:type_link" />52 <link tal:attributes="resource_type param/resource/wadl:type_link" />
@@ -284,15 +284,29 @@
284284
285 <representation mediaType="application/json"285 <representation mediaType="application/json"
286 tal:attributes="id286 tal:attributes="id
287 string:${context/wadl_entry:entry_page_representation_id}">287 string:${context/wadl_entry:entry_page_representation_id}"
288 tal:define="is_total_size_link_active
289 service/wadl:is_total_size_link_active">
288290
289 <param style="plain" name="resource_type_link"291 <param style="plain" name="resource_type_link"
290 path="$['resource_type_link']">292 path="$['resource_type_link']">
291 <link />293 <link />
292 </param>294 </param>
293295
296 <tal:comment condition="nothing">
297 If we are not using total_size_link, continue to signal that
298 total_size is required as it has been in the past.
299 </tal:comment>
300
294 <param style="plain" name="total_size" path="$['total_size']"301 <param style="plain" name="total_size" path="$['total_size']"
295 required="true" />302 tal:attributes="
303 required python:is_total_size_link_active and 'false' or 'true'"/>
304
305 <param tal:condition="is_total_size_link_active"
306 style="plain" name="total_size_link" path="$['total_size_link']"
307 required="false">
308 <link resource_type="#ScalarValue" />
309 </param>
296310
297 <param style="plain" name="start" path="$['start']" required="true" />311 <param style="plain" name="start" path="$['start']" required="true" />
298312
@@ -321,7 +335,7 @@
321 <!--End representation and resource_type definitions for entry335 <!--End representation and resource_type definitions for entry
322 resources. -->336 resources. -->
323337
324 <!--Finally, describe the 'hosted binary file' type.-->338 <!--Finally, describe the 'hosted binary file' type...-->
325 <resource_type id="HostedFile">339 <resource_type id="HostedFile">
326 <method name="GET" id="HostedFile-get">340 <method name="GET" id="HostedFile-get">
327 <response>341 <response>
@@ -334,6 +348,15 @@
334 <method name="DELETE" id="HostedFile-delete" />348 <method name="DELETE" id="HostedFile-delete" />
335 </resource_type>349 </resource_type>
336350
351 <!--...and the simple 'scalar value' type.-->
352 <resource_type id="ScalarValue">
353 <method name="GET" id="ScalarValue-get">
354 <response>
355 <representation mediaType="application/json" />
356 </response>
357 </method>
358 </resource_type>
359
337 <!--Define a data type for binary data.-->360 <!--Define a data type for binary data.-->
338 <xsd:simpleType name="binary">361 <xsd:simpleType name="binary">
339 <xsd:list itemType="byte" />362 <xsd:list itemType="byte" />
340363
=== modified file 'src/lazr/restful/testing/webservice.py'
--- src/lazr/restful/testing/webservice.py 2010-03-03 16:40:14 +0000
+++ src/lazr/restful/testing/webservice.py 2010-08-10 12:24:46 +0000
@@ -135,6 +135,7 @@
135 self.traversed_objects = []135 self.traversed_objects = []
136 self.query_string_params = {}136 self.query_string_params = {}
137 self.method = 'GET'137 self.method = 'GET'
138 self.URL = 'http://api.example.org/'
138139
139140
140 def getTraversalStack(self):141 def getTraversalStack(self):
@@ -173,6 +174,8 @@
173def pprint_collection(json_body):174def pprint_collection(json_body):
174 """Pretty-print a webservice collection JSON representation."""175 """Pretty-print a webservice collection JSON representation."""
175 for key, value in sorted(json_body.items()):176 for key, value in sorted(json_body.items()):
177 if key == 'total_size_link':
178 continue
176 if key != 'entries':179 if key != 'entries':
177 print '%s: %r' % (key, value)180 print '%s: %r' % (key, value)
178 print '---'181 print '---'
179182
=== modified file 'src/lazr/restful/tests/test_utils.py'
--- src/lazr/restful/tests/test_utils.py 2010-03-03 13:08:12 +0000
+++ src/lazr/restful/tests/test_utils.py 2010-08-10 12:24:46 +0000
@@ -10,7 +10,8 @@
10from zope.security.management import (10from zope.security.management import (
11 endInteraction, newInteraction, queryInteraction)11 endInteraction, newInteraction, queryInteraction)
1212
13from lazr.restful.utils import get_current_browser_request13from lazr.restful.utils import (get_current_browser_request,
14 is_total_size_link_active)
1415
1516
16class TestUtils(unittest.TestCase):17class TestUtils(unittest.TestCase):
@@ -28,6 +29,27 @@
28 self.assertEquals(request, get_current_browser_request())29 self.assertEquals(request, get_current_browser_request())
29 endInteraction()30 endInteraction()
3031
32 def test_is_total_size_link_active(self):
33 # Parts of the code want to know if the sizes of collections should be
34 # reported in an attribute or via a link back to the service. The
35 # is_total_size_link_active function takes the version of the API in
36 # question and a web service configuration object and returns a
37 # boolean that is true if a link should be used, false otherwise.
38
39 # Here's the fake web service config we'll be using.
40 class FakeConfig:
41 active_versions = ['1.0', '2.0', '3.0']
42 first_version_with_total_size_link = '2.0'
43
44 # First, if the version is lower than the threshold for using links,
45 # the result is false (i.e., links should not be used).
46 self.assertEqual(is_total_size_link_active('1.0', FakeConfig), False)
47
48 # However, if the requested version is equal to, or higher than the
49 # threshold, the result is true (i.e., links should be used).
50 self.assertEqual(is_total_size_link_active('2.0', FakeConfig), True)
51 self.assertEqual(is_total_size_link_active('3.0', FakeConfig), True)
52
31 # For the sake of convenience, test_get_current_web_service_request()53 # For the sake of convenience, test_get_current_web_service_request()
32 # and tag_request_with_version_name() are tested in test_webservice.py.54 # and tag_request_with_version_name() are tested in test_webservice.py.
3355
3456
=== modified file 'src/lazr/restful/tests/test_webservice.py'
--- src/lazr/restful/tests/test_webservice.py 2010-03-03 16:33:21 +0000
+++ src/lazr/restful/tests/test_webservice.py 2010-08-10 12:24:46 +0000
@@ -9,20 +9,22 @@
9import unittest9import unittest
1010
11from zope.component import getGlobalSiteManager, getUtility11from zope.component import getGlobalSiteManager, getUtility
12from zope.interface import implements, Interface12from zope.interface import implements, Interface, directlyProvides
13from zope.publisher.browser import TestRequest13from zope.publisher.browser import TestRequest
14from zope.schema import Date, Datetime, TextLine14from zope.schema import Date, Datetime, TextLine
15from zope.security.management import (15from zope.security.management import (
16 endInteraction, newInteraction, queryInteraction)16 endInteraction, newInteraction, queryInteraction)
17from zope.traversing.browser.interfaces import IAbsoluteURL17from zope.traversing.browser.interfaces import IAbsoluteURL
1818
19from lazr.restful import ResourceOperation
19from lazr.restful.fields import Reference20from lazr.restful.fields import Reference
20from lazr.restful.interfaces import (21from lazr.restful.interfaces import (
21 ICollection, IEntry, IEntryResource, IResourceGETOperation,22 ICollection, IEntry, IEntryResource, IResourceGETOperation,
22 IServiceRootResource, IWebServiceConfiguration,23 IServiceRootResource, IWebServiceConfiguration,
23 IWebServiceClientRequest, IWebServiceVersion)24 IWebServiceClientRequest, IWebServiceVersion)
24from lazr.restful import EntryResource, ResourceGETOperation25from lazr.restful import EntryResource, ResourceGETOperation
25from lazr.restful.declarations import exported, export_as_webservice_entry26from lazr.restful.declarations import (exported, export_as_webservice_entry,
27 LAZR_WEBSERVICE_NAME)
26from lazr.restful.testing.webservice import (28from lazr.restful.testing.webservice import (
27 create_web_service_request, IGenericCollection, IGenericEntry,29 create_web_service_request, IGenericCollection, IGenericEntry,
28 WebServiceTestCase, WebServiceTestPublication)30 WebServiceTestCase, WebServiceTestPublication)
@@ -30,6 +32,7 @@
30from lazr.restful.utils import (32from lazr.restful.utils import (
31 get_current_browser_request, get_current_web_service_request,33 get_current_browser_request, get_current_web_service_request,
32 tag_request_with_version_name)34 tag_request_with_version_name)
35from lazr.restful._resource import CollectionResource, BatchingResourceMixin
3336
3437
35def get_resource_factory(model_interface, resource_interface):38def get_resource_factory(model_interface, resource_interface):
@@ -350,5 +353,78 @@
350 self.assertEquals("2.0", webservice_request.version)353 self.assertEquals("2.0", webservice_request.version)
351 self.assertTrue(marker_20.providedBy(webservice_request))354 self.assertTrue(marker_20.providedBy(webservice_request))
352355
356
353def additional_tests():357def additional_tests():
354 return unittest.TestLoader().loadTestsFromName(__name__)358 return unittest.TestLoader().loadTestsFromName(__name__)
359
360
361class ITestEntry(IEntry):
362 """Interface for a test entry."""
363 export_as_webservice_entry()
364
365
366class TestEntry:
367 implements(ITestEntry)
368 def __init__(self, context, request):
369 pass
370
371
372class BaseBatchingTest:
373 """A base class which tests BatchingResourceMixin and subclasses."""
374
375 testmodule_objects = [HasRestrictedField, IHasRestrictedField]
376
377 def setUp(self):
378 super(BaseBatchingTest, self).setUp()
379 # Register TestEntry as the IEntry implementation for ITestEntry.
380 getGlobalSiteManager().registerAdapter(
381 TestEntry, [ITestEntry, IWebServiceClientRequest], provided=IEntry)
382 # Is doing this by hand the right way?
383 ITestEntry.setTaggedValue(
384 LAZR_WEBSERVICE_NAME,
385 dict(singular='test_entity', plural='test_entities'))
386
387
388 def make_instance(self, entries, request):
389 raise NotImplementedError('You have to make your own instances.')
390
391 def test_getting_a_batch(self):
392 entries = [1, 2, 3]
393 request = create_web_service_request('/devel')
394 instance = self.make_instance(entries, request)
395 total_size = instance.get_total_size(entries)
396 self.assertEquals(total_size, '3')
397
398
399class TestBatchingResourceMixin(BaseBatchingTest, WebServiceTestCase):
400 """Test that BatchingResourceMixin does batching correctly."""
401
402 def make_instance(self, entries, request):
403 return BatchingResourceMixin()
404
405
406class TestCollectionResourceBatching(BaseBatchingTest, WebServiceTestCase):
407 """Test that CollectionResource does batching correctly."""
408
409 def make_instance(self, entries, request):
410 class Collection:
411 implements(ICollection)
412 entry_schema = ITestEntry
413
414 def __init__(self, entries):
415 self.entries = entries
416
417 def find(self):
418 return self.entries
419
420 return CollectionResource(Collection(entries), request)
421
422
423class TestResourceOperationBatching(BaseBatchingTest, WebServiceTestCase):
424 """Test that ResourceOperation does batching correctly."""
425
426 def make_instance(self, entries, request):
427 # constructor parameters are ignored
428 return ResourceOperation(None, request)
429
430
355431
=== modified file 'src/lazr/restful/utils.py'
--- src/lazr/restful/utils.py 2010-03-10 20:44:03 +0000
+++ src/lazr/restful/utils.py 2010-08-10 12:24:46 +0000
@@ -37,6 +37,18 @@
37missing = object()37missing = object()
3838
3939
40def is_total_size_link_active(version, config):
41 versions = config.active_versions
42 total_size_link_version = config.first_version_with_total_size_link
43 # The version "None" is a special marker for the earliest version.
44 if total_size_link_version is None:
45 total_size_link_version = versions[0]
46 # If the version we're being asked about is equal to or later than the
47 # version in which we started exposing total_size_link, then we should
48 # return True, False otherwise.
49 return versions.index(total_size_link_version) <= versions.index(version)
50
51
40class VersionedDict(object):52class VersionedDict(object):
41 """A stack of named dictionaries.53 """A stack of named dictionaries.
4254
4355
=== modified file 'src/lazr/restful/version.txt'
--- src/lazr/restful/version.txt 2010-08-05 14:01:44 +0000
+++ src/lazr/restful/version.txt 2010-08-10 12:24:46 +0000
@@ -1,1 +1,1 @@
10.10.010.11.0-dev
22
=== modified file 'versions.cfg'
--- versions.cfg 2009-04-15 14:43:40 +0000
+++ versions.cfg 2010-08-10 12:24:46 +0000
@@ -1,172 +1,69 @@
1[buildout]
2versions = versions
3allow-picked-versions = false
4use-dependency-links = false
5
1[versions]6[versions]
2ClientForm = 0.2.97# Alphabetical, case-SENSITIVE, blank line after this comment
3RestrictedPython = 3.4.28
4ZConfig = 2.5.19Jinja2 = 2.5
5ZODB3 = 3.8.110Pygments = 1.3.1
6docutils = 0.411RestrictedPython = 3.5.1
7jquery.javascript = 1.0.012Sphinx = 1.0.1
8jquery.layer = 1.0.013ZConfig = 2.7.1
9lxml = 1.3.614ZODB3 = 3.9.2
10mechanize = 0.1.7b15docutils = 0.5
11pytz = 2007k16epydoc = 3.0.1
12setuptools = 0.6c917grokcore.component = 1.6
13z3c.coverage = 1.1.218lazr.batchnavigator = 1.2.0
14z3c.csvvocabulary = 1.0.019lazr.delegates = 1.2.0
15z3c.etestbrowser = 1.0.420lazr.enum = 1.1.2
16z3c.form = 1.9.021lazr.lifecycle = 1.0
17z3c.formdemo = 1.5.322lazr.uri = 1.0.2
18z3c.formjs = 0.4.023lxml = 2.2.7
19z3c.formjsdemo = 0.3.124martian = 0.11
20z3c.formui = 1.4.225pytz = 2010h
21z3c.i18n = 0.1.126setuptools = 0.6c11
22z3c.layer = 0.2.327simplejson = 2.0.9
23z3c.macro = 1.1.028transaction = 1.0.0
24z3c.macroviewlet = 1.0.029van.testing = 2.0.1
25z3c.menu = 0.2.030wsgi-intercept = 0.4
26z3c.optionstorage = 1.0.431wsgiref = 0.1.2
27z3c.pagelet = 1.0.232z3c.recipe.sphinxdoc = 0.0.8
28z3c.rml = 0.7.333z3c.recipe.staticlxml = 0.7.1
29z3c.skin.pagelet = 1.0.234z3c.recipe.tag = 0.2.0
30z3c.template = 1.1.035zc.buildout = 1.4.3
31z3c.testing = 0.2.036zc.lockfile = 1.0.0
32z3c.traverser = 0.2.337zc.recipe.cmmi = 1.3.1
33z3c.viewlet = 1.0.038zc.recipe.egg = 1.2.3b2
34z3c.viewtemplate = 0.3.239zc.recipe.testrunner = 1.3.0
35z3c.zrtresource = 1.0.140zdaemon = 2.0.4
36zc.buildout = 1.1.141zope.annotation = 3.5.0
37zc.catalog = 1.2.042zope.app.pagetemplate = 3.7.1
38zc.datetimewidget = 0.5.243zope.browser = 1.2
39zc.i18n = 0.5.244zope.cachedescriptors = 3.5.0
40zc.recipe.egg = 1.0.045zope.component = 3.9.3
41zc.recipe.filestorage = 1.0.046zope.configuration = 3.6.0
42zc.recipe.testrunner = 1.0.047zope.contenttype = 3.5.0
43zc.resourcelibrary = 1.0.148zope.copy = 3.5.0
44zc.table = 0.6
45zc.zope3recipes = 0.6.2
46zdaemon = 2.0.2
47zodbcode = 3.4.0
48zope.annotation = 3.4.1
49zope.app.annotation = 3.4.0
50zope.app.apidoc = 3.4.3
51zope.app.applicationcontrol = 3.4.3
52zope.app.appsetup = 3.4.1
53zope.app.authentication = 3.4.4
54zope.app.basicskin = 3.4.0
55zope.app.boston = 3.4.0
56zope.app.broken = 3.4.0
57zope.app.cache = 3.4.1
58zope.app.catalog = 3.5.1
59zope.app.component = 3.4.1
60zope.app.container = 3.5.6
61zope.app.content = 3.4.0
62zope.app.dav = 3.4.1
63zope.app.debug = 3.4.1
64zope.app.debugskin = 3.4.0
65zope.app.dependable = 3.4.0
66zope.app.dtmlpage = 3.4.1
67zope.app.error = 3.5.1
68zope.app.exception = 3.4.1
69zope.app.externaleditor = 3.4.0
70zope.app.file = 3.4.4
71zope.app.folder = 3.4.0
72zope.app.form = 3.4.1
73zope.app.ftp = 3.4.0
74zope.app.generations = 3.4.1
75zope.app.homefolder = 3.4.0
76zope.app.http = 3.4.1
77zope.app.i18n = 3.4.4
78zope.app.i18nfile = 3.4.1
79zope.app.interface = 3.4.0
80zope.app.interpreter = 3.4.0
81zope.app.intid = 3.4.1
82zope.app.keyreference = 3.4.1
83zope.app.layers = 3.4.0
84zope.app.locales = 3.4.5
85zope.app.locking = 3.4.0
86zope.app.module = 3.4.0
87zope.app.onlinehelp = 3.4.1
88zope.app.pagetemplate = 3.4.1
89zope.app.pluggableauth = 3.4.0
90zope.app.preference = 3.4.1
91zope.app.preview = 3.4.0
92zope.app.principalannotation = 3.4.0
93zope.app.publication = 3.4.3
94zope.app.publisher = 3.4.1
95zope.app.pythonpage = 3.4.1
96zope.app.renderer = 3.4.0
97zope.app.rotterdam = 3.4.1
98zope.app.schema = 3.4.0
99zope.app.security = 3.5.2
100zope.app.securitypolicy = 3.4.6
101zope.app.server = 3.4.2
102zope.app.session = 3.5.1
103zope.app.skins = 3.4.0
104zope.app.sqlscript = 3.4.1
105zope.app.testing = 3.4.3
106zope.app.traversing = 3.4.0
107zope.app.tree = 3.4.0
108zope.app.twisted = 3.4.1
109zope.app.undo = 3.4.0
110zope.app.wfmc = 0.1.2
111zope.app.workflow = 3.4.1
112zope.app.wsgi = 3.4.1
113zope.app.xmlrpcintrospection = 3.4.0
114zope.app.zapi = 3.4.0
115zope.app.zcmlfiles = 3.4.3
116zope.app.zopeappgenerations = 3.4.0
117zope.app.zptpage = 3.4.1
118zope.cachedescriptors = 3.4.1
119zope.component = 3.4.0
120zope.configuration = 3.4.0
121zope.contentprovider = 3.4.0
122zope.contenttype = 3.4.0
123zope.copypastemove = 3.4.0
124zope.datetime = 3.4.049zope.datetime = 3.4.0
125zope.decorator = 3.4.050zope.dublincore = 3.5.0
126zope.deferredimport = 3.4.051zope.event = 3.4.1
127zope.deprecation = 3.4.052zope.exceptions = 3.5.2
128zope.documenttemplate = 3.4.053zope.hookable = 3.4.1
129zope.dottedname = 3.4.254zope.i18n = 3.7.1
130zope.dublincore = 3.4.055zope.i18nmessageid = 3.5.0
131zope.error = 3.5.156zope.interface = 3.5.2
132zope.event = 3.4.057zope.lifecycleevent = 3.5.2
133zope.exceptions = 3.4.058zope.location = 3.7.0
134zope.file = 0.3.059zope.pagetemplate = 3.5.0
135zope.filerepresentation = 3.4.060zope.proxy = 3.5.0
136zope.formlib = 3.4.061zope.publisher = 3.12.0
137zope.hookable = 3.4.062zope.schema = 3.5.4
138zope.html = 1.0.163zope.security = 3.7.1
139zope.i18n = 3.4.064zope.size = 3.4.1
140zope.i18nmessageid = 3.4.365zope.tal = 3.5.1
141zope.index = 3.4.1
142zope.interface = 3.4.1
143zope.lifecycleevent = 3.4.0
144zope.location = 3.4.0
145zope.mimetype = 0.3.0
146zope.minmax = 1.1.0
147zope.modulealias = 3.4.0
148zope.pagetemplate = 3.4.0
149zope.proxy = 3.4.2
150zope.publisher = 3.4.6
151zope.rdb = 3.4.0
152zope.schema = 3.4.0
153zope.security = 3.4.1
154zope.securitypolicy = 3.4.1
155zope.sendmail = 3.4.0
156zope.sequencesort = 3.4.0
157zope.server = 3.4.3
158zope.session = 3.4.1
159zope.size = 3.4.0
160zope.structuredtext = 3.4.0
161zope.tal = 3.4.1
162zope.tales = 3.4.066zope.tales = 3.4.0
163zope.testbrowser = 3.4.267zope.testing = 3.9.4
164zope.testing = 3.5.668zope.testrunner = 4.0.0b5
165zope.testrecorder = 0.3.069zope.traversing = 3.8.0
166zope.thread = 3.4
167zope.traversing = 3.6.0
168zope.ucol = 1.0.2
169zope.viewlet = 3.4.2
170zope.wfmc = 3.4.0
171zope.xmlpickle = 3.4.0
172van.testing = 2.0.1

Subscribers

People subscribed via source and target branches