First version of new RESTful AFE interface. Includes a substantial library (under frontend/shared) and a definition of the interface for AFE (frontend/afe/resources.py).
If you want to see what this interface looks like, I encourage you to check out
http://your-autotest-server/afe/server/resources/?alt=json-html
>From there you can explore the entire interface through your browser (this is one of its great strengths).
For an introduction to the idea behind RESTful services, try http://bitworking.org/news/How_to_create_a_REST_Protocol.
This is still very much under development and there are plenty of TODOs, but it's working so I wanted to get it out there so we can start seeing how useful it turns out to be.
Signed-off-by: Steve Howard <showard@google.com>
git-svn-id: http://test.kernel.org/svn/autotest/trunk@4165 592f7852-d20e-0410-864c-8624ca9c26a4
diff --git a/frontend/shared/rest_client.py b/frontend/shared/rest_client.py
new file mode 100644
index 0000000..d6aca5d
--- /dev/null
+++ b/frontend/shared/rest_client.py
@@ -0,0 +1,159 @@
+import logging, pprint, re, urllib
+import httplib2
+from django.utils import simplejson
+
+
+_RESOURCE_DIRECTORY_PATH = '/afe/server/resources/'
+
+
+_http = httplib2.Http()
+
+
+class RestClientError(Exception):
+ pass
+
+
+class ClientError(Exception):
+ pass
+
+
+class ServerError(Exception):
+ pass
+
+
+class Response(object):
+ def __init__(self, httplib_response, httplib_content):
+ self.status = int(httplib_response['status'])
+ self.headers = httplib_response
+ self.entity_body = httplib_content
+
+
+ def decoded_body(self):
+ return simplejson.loads(self.entity_body)
+
+
+ def __str__(self):
+ return '\n'.join([str(self.status), self.entity_body])
+
+
+class Resource(object):
+ def __init__(self, base_uri, representation_dict):
+ self._base_uri = base_uri
+
+ assert 'href' in representation_dict
+ for key, value in representation_dict.iteritems():
+ setattr(self, str(key), value)
+
+
+ def __repr__(self):
+ return 'Resource(%r)' % self._representation()
+
+
+ def pprint(self):
+ # pretty-print support for debugging/interactive use
+ pprint.pprint(self._representation())
+
+
+ @classmethod
+ def directory(cls, base_uri):
+ directory = cls(base_uri, {'href': _RESOURCE_DIRECTORY_PATH})
+ return directory.get()
+
+
+ def _read_representation(self, value):
+ # recursively convert representation dicts to Resource objects
+ if isinstance(value, list):
+ return [self._read_representation(element) for element in value]
+ if isinstance(value, dict):
+ converted_dict = dict((key, self._read_representation(sub_value))
+ for key, sub_value in value.iteritems())
+ if 'href' in converted_dict:
+ return type(self)(self._base_uri, converted_dict)
+ return converted_dict
+ return value
+
+
+ def _write_representation(self, value):
+ # recursively convert Resource objects to representation dicts
+ if isinstance(value, list):
+ return [self._write_representation(element) for element in value]
+ if isinstance(value, dict):
+ return dict((key, self._write_representation(sub_value))
+ for key, sub_value in value.iteritems())
+ if isinstance(value, Resource):
+ return value._representation()
+ return value
+
+
+ def _representation(self):
+ return dict((key, self._write_representation(value))
+ for key, value in self.__dict__.iteritems()
+ if not key.startswith('_')
+ and not callable(value))
+
+
+ def _request(self, method, query_parameters=None, encoded_body=None):
+ uri_parts = []
+ if not re.match(r'^https?://', self.href):
+ uri_parts.append(self._base_uri)
+ uri_parts.append(self.href)
+ if query_parameters:
+ query_string = urllib.urlencode(query_parameters)
+ uri_parts.extend(['?', query_string])
+
+ if encoded_body:
+ entity_body = simplejson.dumps(encoded_body)
+ else:
+ entity_body = None
+
+ full_uri = ''.join(uri_parts)
+ logging.debug('%s %s', method, full_uri)
+ if entity_body:
+ logging.debug(entity_body)
+ headers, response_body = _http.request(
+ ''.join(uri_parts), method, body=entity_body,
+ headers={'Content-Type': 'application/json'})
+ logging.debug('Response: %s', headers['status'])
+
+ response = Response(headers, response_body)
+ if 300 <= response.status < 400: # redirection
+ raise NotImplementedError(str(response)) # TODO
+ if 400 <= response.status < 500:
+ raise ClientError(str(response))
+ if 500 <= response.status < 600:
+ raise ServerError(str(response))
+ return response
+
+
+ def _stringify_query_parameter(self, value):
+ if isinstance(value, (list, tuple)):
+ return ','.join(value)
+ return str(value)
+
+
+ def get(self, **query_parameters):
+ string_parameters = dict((key, self._stringify_query_parameter(value))
+ for key, value in query_parameters.iteritems()
+ if value is not None)
+ response = self._request('GET', query_parameters=string_parameters)
+ assert response.status == 200
+ return self._read_representation(response.decoded_body())
+
+
+ def put(self):
+ response = self._request('PUT', encoded_body=self._representation())
+ assert response.status == 200
+ return self._read_representation(response.decoded_body())
+
+
+ def delete(self):
+ response = self._request('DELETE')
+ assert response.status == 204 # no content
+
+
+ def post(self, request_dict):
+ # request_dict may still have resources in it
+ request_dict = self._write_representation(request_dict)
+ response = self._request('POST', encoded_body=request_dict)
+ assert response.status == 201 # created
+ return self._read_representation({'href': response.headers['location']})