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