Various changes to support further high-level automation efforts.
* added a RESTful interface for TKO. right now there's only a single, simple resource for accessing test attributes.
* extended the REST server library in a few ways, most notably to support
* querying on keyvals, with something like ?has_keyval=mykey=myvalue&...
* operators, delimited by a colon, like ?hostname:in=host1,host2,host3
* loading relationships over many items efficiently (see InstanceEntry.prepare_for_full_representation()). this is used to fill in keyvals when requesting a job listing, but it can (and should) be used in other places, such as listing labels for a host collection.
* loading a collection with inlined full representations, by passing full_representations=true
* added various features to the AFE RESTful interface as necessary.
* various fixes to the rest_client library, most notably
* changed HTTP client in rest_client.py to use DI rather than singleton, easing testability. the same should be done for _get_request_headers(), to be honest.
* better support for query params, including accepting a MultiValueDict and supporting URIs that already have query args
* basic support for redirects
* builtin support for requesting a full collection (get_full()), when clients explicitly expect the result not to be paged. i'm still considering alternative approaches to this -- it may make sense to have something like this be the default, and have clients set a default page size limit rather than passing it every time.
* minor change to mock.py to provide better debugging output.
Signed-off-by: Steve Howard <showard@google.com>
git-svn-id: http://test.kernel.org/svn/autotest/trunk@4438 592f7852-d20e-0410-864c-8624ca9c26a4
diff --git a/frontend/shared/resource_lib.py b/frontend/shared/resource_lib.py
index 5d77410..fff8ff0 100644
--- a/frontend/shared/resource_lib.py
+++ b/frontend/shared/resource_lib.py
@@ -131,10 +131,12 @@
def read_query_parameters(self, parameters):
"""Read relevant query parameters from a Django MultiValueDict."""
- for param_name, _ in self._query_parameters_accepted():
- if param_name in parameters:
- self._query_params.setlist(param_name,
- parameters.getlist(param_name))
+ params_acccepted = set(param_name for param_name, _
+ in self._query_parameters_accepted())
+ for name, values in parameters.iterlists():
+ base_name = name.split(':', 1)[0]
+ if base_name in params_acccepted:
+ self._query_params.setlist(name, values)
def set_query_parameters(self, **parameters):
@@ -216,6 +218,9 @@
except ValueError, exc:
raise exceptions.BadRequest('Error decoding request body: '
'%s\n%r' % (exc, raw_data))
+ if not isinstance(raw_dict, dict):
+ raise exceptions.BadRequest('Expected dict input, got %s: %r' %
+ (type(raw_dict), raw_dict))
elif content_type == 'application/x-www-form-urlencoded':
cgi_dict = cgi.parse_qs(raw_data) # django won't do this for PUT
raw_dict = {}
@@ -319,6 +324,7 @@
assert self.model is not None
super(Entry, self).__init__(request)
self.instance = instance
+ self._is_prepared_for_full_representation = False
@classmethod
@@ -332,6 +338,41 @@
self.instance.delete()
+ def full_representation(self):
+ self.prepare_for_full_representation([self])
+ return super(InstanceEntry, self).full_representation()
+
+
+ @classmethod
+ def prepare_for_full_representation(cls, entries):
+ """
+ Prepare the given list of entries to generate full representations.
+
+ This method delegates to _do_prepare_for_full_representation(), which
+ subclasses may override as necessary to do the actual processing. This
+ method also marks the instance as prepared, so it's safe to call this
+ multiple times with the same instance(s) without wasting work.
+ """
+ not_prepared = [entry for entry in entries
+ if not entry._is_prepared_for_full_representation]
+ cls._do_prepare_for_full_representation([entry.instance
+ for entry in not_prepared])
+ for entry in not_prepared:
+ entry._is_prepared_for_full_representation = True
+
+
+ @classmethod
+ def _do_prepare_for_full_representation(cls, instances):
+ """
+ Subclasses may override this to gather data as needed for full
+ representations of the given model instances. Typically, this involves
+ querying over related objects, and this method offers a chance to query
+ for many instances at once, which can provide a great performance
+ benefit.
+ """
+ pass
+
+
class Collection(Resource):
_DEFAULT_ITEMS_PER_PAGE = 50
@@ -354,7 +395,9 @@
def _query_parameters_accepted(self):
params = [('start_index', 'Index of first member to include'),
- ('items_per_page', 'Number of members to include')]
+ ('items_per_page', 'Number of members to include'),
+ ('full_representations',
+ 'True to include full representations of members')]
for selector in self._query_processor.selectors():
params.append((selector.name, selector.doc))
return params
@@ -371,16 +414,33 @@
def _representation(self, entry_instances):
+ entries = [self._entry_from_instance(instance)
+ for instance in entry_instances]
+
+ want_full_representation = self._read_bool_parameter(
+ 'full_representations')
+ if want_full_representation:
+ self.entry_class.prepare_for_full_representation(entries)
+
members = []
- for instance in entry_instances:
- entry = self._entry_from_instance(instance)
- members.append(entry.short_representation())
+ for entry in entries:
+ if want_full_representation:
+ rep = entry.full_representation()
+ else:
+ rep = entry.short_representation()
+ members.append(rep)
rep = self.link()
rep.update({'members': members})
return rep
+ def _read_bool_parameter(self, name):
+ if name not in self._query_params:
+ return False
+ return (self._query_params[name].lower() == 'true')
+
+
def _read_int_parameter(self, name, default):
if name not in self._query_params:
return default
@@ -395,12 +455,17 @@
def _apply_form_query(self, queryset):
"""Apply any query selectors passed as form variables."""
for parameter, values in self._query_params.lists():
+ if ':' in parameter:
+ parameter, comparison_type = parameter.split(':', 1)
+ else:
+ comparison_type = None
+
if not self._query_processor.has_selector(parameter):
continue
for value in values: # forms keys can have multiple values
- queryset = self._query_processor.apply_selector(queryset,
- parameter,
- value)
+ queryset = self._query_processor.apply_selector(
+ queryset, parameter, value,
+ comparison_type=comparison_type)
return queryset