diff --git a/apiclient/model.py b/apiclient/model.py index 7bc6858..61a81d4 100644 --- a/apiclient/model.py +++ b/apiclient/model.py @@ -34,9 +34,7 @@ from errors import HttpError FLAGS = gflags.FLAGS gflags.DEFINE_boolean('dump_request_response', False, - 'Dump all http server requests and responses. ' - 'Must use apiclient.model.LoggingJsonModel as ' - 'the model.' + 'Dump all http server requests and responses. ' ) @@ -53,7 +51,7 @@ class Model(object): """ def request(self, headers, path_params, query_params, body_value): - """Updates outgoing requests with a deserialized body. + """Updates outgoing requests with a serialized body. Args: headers: dict, request headers @@ -87,23 +85,43 @@ class Model(object): _abstract() -class JsonModel(Model): - """Model class for JSON. +class BaseModel(Model): + """Base model class. - Serializes and de-serializes between JSON and the Python - object representation of HTTP request and response bodies. + Subclasses should provide implementations for the "serialize" and + "deserialize" methods, as well as values for the following class attributes. + + Attributes: + accept: The value to use for the HTTP Accept header. + content_type: The value to use for the HTTP Content-type header. + no_content_response: The value to return when deserializing a 204 "No + Content" response. + alt_param: The value to supply as the "alt" query parameter for requests. """ - def __init__(self, data_wrapper=False): - """Construct a JsonModel + accept = None + content_type = None + no_content_response = None + alt_param = None - Args: - data_wrapper: boolean, wrap requests and responses in a data wrapper - """ - self._data_wrapper = data_wrapper + def _log_request(self, headers, path_params, query, body): + """Logs debugging information about the request if requested.""" + if FLAGS.dump_request_response: + logging.info('--request-start--') + logging.info('-headers-start-') + for h, v in headers.iteritems(): + logging.info('%s: %s', h, v) + logging.info('-headers-end-') + logging.info('-path-parameters-start-') + for h, v in path_params.iteritems(): + logging.info('%s: %s', h, v) + logging.info('-path-parameters-end-') + logging.info('body: %s', body) + logging.info('query: %s', query) + logging.info('--request-end--') def request(self, headers, path_params, query_params, body_value): - """Updates outgoing requests with JSON bodies. + """Updates outgoing requests with a serialized body. Args: headers: dict, request headers @@ -120,7 +138,7 @@ class JsonModel(Model): body: string, the body serialized as JSON """ query = self._build_query(query_params) - headers['accept'] = 'application/json' + headers['accept'] = self.accept headers['accept-encoding'] = 'gzip, deflate' if 'user-agent' in headers: headers['user-agent'] += ' ' @@ -128,12 +146,10 @@ class JsonModel(Model): headers['user-agent'] = '' headers['user-agent'] += 'google-api-python-client/1.0' - if (isinstance(body_value, dict) and 'data' not in body_value and - self._data_wrapper): - body_value = {'data': body_value} if body_value is not None: - headers['content-type'] = 'application/json' - body_value = simplejson.dumps(body_value) + headers['content-type'] = self.content_type + body_value = self.serialize(body_value) + self._log_request(headers, path_params, query, body_value) return (headers, path_params, query, body_value) def _build_query(self, params): @@ -145,7 +161,7 @@ class JsonModel(Model): Returns: The query parameters properly encoded into an HTTP URI query string. """ - params.update({'alt': 'json'}) + params.update({'alt': self.alt_param}) astuples = [] for key, value in params.iteritems(): if type(value) == type([]): @@ -158,6 +174,16 @@ class JsonModel(Model): astuples.append((key, value)) return '?' + urllib.urlencode(astuples) + def _log_response(self, resp, content): + """Logs debugging information about the response if requested.""" + if FLAGS.dump_request_response: + logging.info('--response-start--') + for h, v in resp.iteritems(): + logging.info('%s: %s', h, v) + if content: + logging.info(content) + logging.info('--response-end--') + def response(self, resp, content): """Convert the response wire format into a Python object. @@ -171,76 +197,105 @@ class JsonModel(Model): Raises: apiclient.errors.HttpError if a non 2xx response is received. """ + self._log_response(resp, content) # Error handling is TBD, for example, do we retry # for some operation/error combinations? if resp.status < 300: if resp.status == 204: # A 204: No Content response should be treated differently # to all the other success states - return simplejson.loads('{}') - body = simplejson.loads(content) - if isinstance(body, dict) and 'data' in body: - body = body['data'] - return body + return self.no_content_response + return self.deserialize(content) else: logging.debug('Content from bad request was: %s' % content) raise HttpError(resp, content) - -class LoggingJsonModel(JsonModel): - """A printable JsonModel class that supports logging response info.""" - - def response(self, resp, content): - """An overloaded response method that will output debug info if requested. + def serialize(self, body_value): + """Perform the actual Python object serialization. Args: - resp: An httplib2.Response object. - content: A string representing the response body. + body_value: object, the request body as a Python object. + + Returns: + string, the body in serialized form. + """ + _abstract() + + def deserialize(self, content): + """Perform the actual deserialization from response string to Python object. + + Args: + content: string, the body of the HTTP response Returns: The body de-serialized as a Python object. """ - if FLAGS.dump_request_response: - logging.info('--response-start--') - for h, v in resp.iteritems(): - logging.info('%s: %s', h, v) - if content: - logging.info(content) - logging.info('--response-end--') - return super(LoggingJsonModel, self).response( - resp, content) + _abstract() - def request(self, headers, path_params, query_params, body_value): - """An overloaded request method that will output debug info if requested. + +class JsonModel(BaseModel): + """Model class for JSON. + + Serializes and de-serializes between JSON and the Python + object representation of HTTP request and response bodies. + """ + accept = 'application/json' + content_type = 'application/json' + alt_param = 'json' + + def __init__(self, data_wrapper=False): + """Construct a JsonModel. Args: - headers: dict, request headers - path_params: dict, parameters that appear in the request path - query_params: dict, parameters that appear in the query - body_value: object, the request body as a Python object, which must be - serializable by simplejson. - Returns: - A tuple of (headers, path_params, query, body) - - headers: dict, request headers - path_params: dict, parameters that appear in the request path - query: string, query part of the request URI - body: string, the body serialized as JSON + data_wrapper: boolean, wrap requests and responses in a data wrapper """ - (headers, path_params, query, body) = super( - LoggingJsonModel, self).request( - headers, path_params, query_params, body_value) - if FLAGS.dump_request_response: - logging.info('--request-start--') - logging.info('-headers-start-') - for h, v in headers.iteritems(): - logging.info('%s: %s', h, v) - logging.info('-headers-end-') - logging.info('-path-parameters-start-') - for h, v in path_params.iteritems(): - logging.info('%s: %s', h, v) - logging.info('-path-parameters-end-') - logging.info('body: %s', body) - logging.info('query: %s', query) - logging.info('--request-end--') - return (headers, path_params, query, body) + self._data_wrapper = data_wrapper + + def serialize(self, body_value): + if (isinstance(body_value, dict) and 'data' not in body_value and + self._data_wrapper): + body_value = {'data': body_value} + return simplejson.dumps(body_value) + + def deserialize(self, content): + body = simplejson.loads(content) + if isinstance(body, dict) and 'data' in body: + body = body['data'] + return body + + @property + def no_content_response(self): + return {} + + +class ProtocolBufferModel(BaseModel): + """Model class for protocol buffers. + + Serializes and de-serializes the binary protocol buffer sent in the HTTP + request and response bodies. + """ + accept = 'application/x-protobuf' + content_type = 'application/x-protobuf' + alt_param = 'proto' + + def __init__(self, protocol_buffer): + """Constructs a ProtocolBufferModel. + + The serialzed protocol buffer returned in an HTTP response will be + de-serialized using the given protocol buffer class. + + Args: + protocol_buffer: The protocol buffer class used to de-serialize a response + from the API. + """ + self._protocol_buffer = protocol_buffer + + def serialize(self, body_value): + return body_value.SerializeToString() + + def deserialize(self, content): + return self._protocol_buffer.FromString(content) + + @property + def no_content_response(self): + return self._protocol_buffer() diff --git a/samples/debugging/main.py b/samples/debugging/main.py index 31a276e..be82ff1 100644 --- a/samples/debugging/main.py +++ b/samples/debugging/main.py @@ -17,7 +17,7 @@ import pprint import sys from apiclient.discovery import build -from apiclient.model import LoggingJsonModel +from apiclient.model import JsonModel FLAGS = gflags.FLAGS @@ -34,7 +34,7 @@ def main(argv): service = build('translate', 'v2', developerKey='AIzaSyAQIKv_gwnob-YNrXV2stnY86GSGY81Zr0', - model=LoggingJsonModel()) + model=JsonModel()) print service.translations().list( source='en', target='fr', diff --git a/tests/test_json_model.py b/tests/test_json_model.py index 29130d2..7ab2f98 100644 --- a/tests/test_json_model.py +++ b/tests/test_json_model.py @@ -31,7 +31,6 @@ import apiclient.model from apiclient.anyjson import simplejson from apiclient.errors import HttpError from apiclient.model import JsonModel -from apiclient.model import LoggingJsonModel FLAGS = gflags.FLAGS @@ -186,10 +185,16 @@ class Model(unittest.TestCase): content = model.response(resp, content) self.assertEqual(content, 'data goes here') + def test_no_content_response(self): + model = JsonModel(data_wrapper=False) + resp = httplib2.Response({'status': '204'}) + resp.reason = 'No Content' + content = '' -class LoggingModel(unittest.TestCase): + content = model.response(resp, content) + self.assertEqual(content, {}) - def test_logging_json_model(self): + def test_logging(self): class MockLogging(object): def __init__(self): self.info_record = [] @@ -206,10 +211,11 @@ class LoggingModel(unittest.TestCase): self.status = items['status'] for key, value in items.iteritems(): self[key] = value + old_logging = apiclient.model.logging apiclient.model.logging = MockLogging() apiclient.model.FLAGS = copy.deepcopy(FLAGS) apiclient.model.FLAGS.dump_request_response = True - model = LoggingJsonModel() + model = JsonModel() request_body = { 'field1': 'value1', 'field2': 'value2' @@ -234,7 +240,7 @@ class LoggingModel(unittest.TestCase): request_body) self.assertEqual(apiclient.model.logging.info_record[-1], '--response-end--') - + apiclient.model.logging = old_logging if __name__ == '__main__': diff --git a/tests/test_protobuf_model.py b/tests/test_protobuf_model.py new file mode 100644 index 0000000..02e8846 --- /dev/null +++ b/tests/test_protobuf_model.py @@ -0,0 +1,106 @@ +#!/usr/bin/python2.4 +# +# Copyright 2010 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Protocol Buffer Model tests + +Unit tests for the Protocol Buffer model. +""" + +__author__ = 'mmcdonald@google.com (Matt McDonald)' + +import gflags +import unittest +import httplib2 +import apiclient.model + +from apiclient.errors import HttpError +from apiclient.model import ProtocolBufferModel + +FLAGS = gflags.FLAGS + +# Python 2.5 requires different modules +try: + from urlparse import parse_qs +except ImportError: + from cgi import parse_qs + + +class MockProtocolBuffer(object): + def __init__(self, data=None): + self.data = data + + def __eq__(self, other): + return self.data == other.data + + @classmethod + def FromString(cls, string): + return cls(string) + + def SerializeToString(self): + return self.data + + +class Model(unittest.TestCase): + def setUp(self): + self.model = ProtocolBufferModel(MockProtocolBuffer) + + def test_no_body(self): + headers = {} + path_params = {} + query_params = {} + body = None + + headers, params, query, body = self.model.request( + headers, path_params, query_params, body) + + self.assertEqual(headers['accept'], 'application/x-protobuf') + self.assertTrue('content-type' not in headers) + self.assertNotEqual(query, '') + self.assertEqual(body, None) + + def test_body(self): + headers = {} + path_params = {} + query_params = {} + body = MockProtocolBuffer('data') + + headers, params, query, body = self.model.request( + headers, path_params, query_params, body) + + self.assertEqual(headers['accept'], 'application/x-protobuf') + self.assertEqual(headers['content-type'], 'application/x-protobuf') + self.assertNotEqual(query, '') + self.assertEqual(body, 'data') + + def test_good_response(self): + resp = httplib2.Response({'status': '200'}) + resp.reason = 'OK' + content = 'data' + + content = self.model.response(resp, content) + self.assertEqual(content, MockProtocolBuffer('data')) + + def test_no_content_response(self): + resp = httplib2.Response({'status': '204'}) + resp.reason = 'No Content' + content = '' + + content = self.model.response(resp, content) + self.assertEqual(content, MockProtocolBuffer()) + + +if __name__ == '__main__': + unittest.main()