Add support for Protocol Buffers as an API serialization format
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 @@
 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 @@
   """
 
   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 @@
     _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 @@
       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 @@
       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 @@
     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 @@
         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 @@
     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 sys
 
 from apiclient.discovery import build
-from apiclient.model import LoggingJsonModel
+from apiclient.model import JsonModel
 
 
 FLAGS = gflags.FLAGS
@@ -34,7 +34,7 @@
 
   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 @@
 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 @@
     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 @@
         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 @@
                      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()