Add support for Protocol Buffers as an API serialization format
This commit is contained in:
@@ -34,9 +34,7 @@ from errors import HttpError
|
|||||||
FLAGS = gflags.FLAGS
|
FLAGS = gflags.FLAGS
|
||||||
|
|
||||||
gflags.DEFINE_boolean('dump_request_response', False,
|
gflags.DEFINE_boolean('dump_request_response', False,
|
||||||
'Dump all http server requests and responses. '
|
'Dump all http server requests and responses. '
|
||||||
'Must use apiclient.model.LoggingJsonModel as '
|
|
||||||
'the model.'
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -53,7 +51,7 @@ class Model(object):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def request(self, headers, path_params, query_params, body_value):
|
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:
|
Args:
|
||||||
headers: dict, request headers
|
headers: dict, request headers
|
||||||
@@ -87,23 +85,43 @@ class Model(object):
|
|||||||
_abstract()
|
_abstract()
|
||||||
|
|
||||||
|
|
||||||
class JsonModel(Model):
|
class BaseModel(Model):
|
||||||
"""Model class for JSON.
|
"""Base model class.
|
||||||
|
|
||||||
Serializes and de-serializes between JSON and the Python
|
Subclasses should provide implementations for the "serialize" and
|
||||||
object representation of HTTP request and response bodies.
|
"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):
|
accept = None
|
||||||
"""Construct a JsonModel
|
content_type = None
|
||||||
|
no_content_response = None
|
||||||
|
alt_param = None
|
||||||
|
|
||||||
Args:
|
def _log_request(self, headers, path_params, query, body):
|
||||||
data_wrapper: boolean, wrap requests and responses in a data wrapper
|
"""Logs debugging information about the request if requested."""
|
||||||
"""
|
if FLAGS.dump_request_response:
|
||||||
self._data_wrapper = data_wrapper
|
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):
|
def request(self, headers, path_params, query_params, body_value):
|
||||||
"""Updates outgoing requests with JSON bodies.
|
"""Updates outgoing requests with a serialized body.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
headers: dict, request headers
|
headers: dict, request headers
|
||||||
@@ -120,7 +138,7 @@ class JsonModel(Model):
|
|||||||
body: string, the body serialized as JSON
|
body: string, the body serialized as JSON
|
||||||
"""
|
"""
|
||||||
query = self._build_query(query_params)
|
query = self._build_query(query_params)
|
||||||
headers['accept'] = 'application/json'
|
headers['accept'] = self.accept
|
||||||
headers['accept-encoding'] = 'gzip, deflate'
|
headers['accept-encoding'] = 'gzip, deflate'
|
||||||
if 'user-agent' in headers:
|
if 'user-agent' in headers:
|
||||||
headers['user-agent'] += ' '
|
headers['user-agent'] += ' '
|
||||||
@@ -128,12 +146,10 @@ class JsonModel(Model):
|
|||||||
headers['user-agent'] = ''
|
headers['user-agent'] = ''
|
||||||
headers['user-agent'] += 'google-api-python-client/1.0'
|
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:
|
if body_value is not None:
|
||||||
headers['content-type'] = 'application/json'
|
headers['content-type'] = self.content_type
|
||||||
body_value = simplejson.dumps(body_value)
|
body_value = self.serialize(body_value)
|
||||||
|
self._log_request(headers, path_params, query, body_value)
|
||||||
return (headers, path_params, query, body_value)
|
return (headers, path_params, query, body_value)
|
||||||
|
|
||||||
def _build_query(self, params):
|
def _build_query(self, params):
|
||||||
@@ -145,7 +161,7 @@ class JsonModel(Model):
|
|||||||
Returns:
|
Returns:
|
||||||
The query parameters properly encoded into an HTTP URI query string.
|
The query parameters properly encoded into an HTTP URI query string.
|
||||||
"""
|
"""
|
||||||
params.update({'alt': 'json'})
|
params.update({'alt': self.alt_param})
|
||||||
astuples = []
|
astuples = []
|
||||||
for key, value in params.iteritems():
|
for key, value in params.iteritems():
|
||||||
if type(value) == type([]):
|
if type(value) == type([]):
|
||||||
@@ -158,6 +174,16 @@ class JsonModel(Model):
|
|||||||
astuples.append((key, value))
|
astuples.append((key, value))
|
||||||
return '?' + urllib.urlencode(astuples)
|
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):
|
def response(self, resp, content):
|
||||||
"""Convert the response wire format into a Python object.
|
"""Convert the response wire format into a Python object.
|
||||||
|
|
||||||
@@ -171,76 +197,105 @@ class JsonModel(Model):
|
|||||||
Raises:
|
Raises:
|
||||||
apiclient.errors.HttpError if a non 2xx response is received.
|
apiclient.errors.HttpError if a non 2xx response is received.
|
||||||
"""
|
"""
|
||||||
|
self._log_response(resp, content)
|
||||||
# Error handling is TBD, for example, do we retry
|
# Error handling is TBD, for example, do we retry
|
||||||
# for some operation/error combinations?
|
# for some operation/error combinations?
|
||||||
if resp.status < 300:
|
if resp.status < 300:
|
||||||
if resp.status == 204:
|
if resp.status == 204:
|
||||||
# A 204: No Content response should be treated differently
|
# A 204: No Content response should be treated differently
|
||||||
# to all the other success states
|
# to all the other success states
|
||||||
return simplejson.loads('{}')
|
return self.no_content_response
|
||||||
body = simplejson.loads(content)
|
return self.deserialize(content)
|
||||||
if isinstance(body, dict) and 'data' in body:
|
|
||||||
body = body['data']
|
|
||||||
return body
|
|
||||||
else:
|
else:
|
||||||
logging.debug('Content from bad request was: %s' % content)
|
logging.debug('Content from bad request was: %s' % content)
|
||||||
raise HttpError(resp, content)
|
raise HttpError(resp, content)
|
||||||
|
|
||||||
|
def serialize(self, body_value):
|
||||||
class LoggingJsonModel(JsonModel):
|
"""Perform the actual Python object serialization.
|
||||||
"""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.
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
resp: An httplib2.Response object.
|
body_value: object, the request body as a Python object.
|
||||||
content: A string representing the response body.
|
|
||||||
|
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:
|
Returns:
|
||||||
The body de-serialized as a Python object.
|
The body de-serialized as a Python object.
|
||||||
"""
|
"""
|
||||||
if FLAGS.dump_request_response:
|
_abstract()
|
||||||
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)
|
|
||||||
|
|
||||||
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:
|
Args:
|
||||||
headers: dict, request headers
|
data_wrapper: boolean, wrap requests and responses in a data wrapper
|
||||||
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
|
|
||||||
"""
|
"""
|
||||||
(headers, path_params, query, body) = super(
|
self._data_wrapper = data_wrapper
|
||||||
LoggingJsonModel, self).request(
|
|
||||||
headers, path_params, query_params, body_value)
|
def serialize(self, body_value):
|
||||||
if FLAGS.dump_request_response:
|
if (isinstance(body_value, dict) and 'data' not in body_value and
|
||||||
logging.info('--request-start--')
|
self._data_wrapper):
|
||||||
logging.info('-headers-start-')
|
body_value = {'data': body_value}
|
||||||
for h, v in headers.iteritems():
|
return simplejson.dumps(body_value)
|
||||||
logging.info('%s: %s', h, v)
|
|
||||||
logging.info('-headers-end-')
|
def deserialize(self, content):
|
||||||
logging.info('-path-parameters-start-')
|
body = simplejson.loads(content)
|
||||||
for h, v in path_params.iteritems():
|
if isinstance(body, dict) and 'data' in body:
|
||||||
logging.info('%s: %s', h, v)
|
body = body['data']
|
||||||
logging.info('-path-parameters-end-')
|
return body
|
||||||
logging.info('body: %s', body)
|
|
||||||
logging.info('query: %s', query)
|
@property
|
||||||
logging.info('--request-end--')
|
def no_content_response(self):
|
||||||
return (headers, path_params, query, body)
|
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()
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import pprint
|
|||||||
import sys
|
import sys
|
||||||
|
|
||||||
from apiclient.discovery import build
|
from apiclient.discovery import build
|
||||||
from apiclient.model import LoggingJsonModel
|
from apiclient.model import JsonModel
|
||||||
|
|
||||||
|
|
||||||
FLAGS = gflags.FLAGS
|
FLAGS = gflags.FLAGS
|
||||||
@@ -34,7 +34,7 @@ def main(argv):
|
|||||||
|
|
||||||
service = build('translate', 'v2',
|
service = build('translate', 'v2',
|
||||||
developerKey='AIzaSyAQIKv_gwnob-YNrXV2stnY86GSGY81Zr0',
|
developerKey='AIzaSyAQIKv_gwnob-YNrXV2stnY86GSGY81Zr0',
|
||||||
model=LoggingJsonModel())
|
model=JsonModel())
|
||||||
print service.translations().list(
|
print service.translations().list(
|
||||||
source='en',
|
source='en',
|
||||||
target='fr',
|
target='fr',
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ import apiclient.model
|
|||||||
from apiclient.anyjson import simplejson
|
from apiclient.anyjson import simplejson
|
||||||
from apiclient.errors import HttpError
|
from apiclient.errors import HttpError
|
||||||
from apiclient.model import JsonModel
|
from apiclient.model import JsonModel
|
||||||
from apiclient.model import LoggingJsonModel
|
|
||||||
|
|
||||||
FLAGS = gflags.FLAGS
|
FLAGS = gflags.FLAGS
|
||||||
|
|
||||||
@@ -186,10 +185,16 @@ class Model(unittest.TestCase):
|
|||||||
content = model.response(resp, content)
|
content = model.response(resp, content)
|
||||||
self.assertEqual(content, 'data goes here')
|
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):
|
class MockLogging(object):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.info_record = []
|
self.info_record = []
|
||||||
@@ -206,10 +211,11 @@ class LoggingModel(unittest.TestCase):
|
|||||||
self.status = items['status']
|
self.status = items['status']
|
||||||
for key, value in items.iteritems():
|
for key, value in items.iteritems():
|
||||||
self[key] = value
|
self[key] = value
|
||||||
|
old_logging = apiclient.model.logging
|
||||||
apiclient.model.logging = MockLogging()
|
apiclient.model.logging = MockLogging()
|
||||||
apiclient.model.FLAGS = copy.deepcopy(FLAGS)
|
apiclient.model.FLAGS = copy.deepcopy(FLAGS)
|
||||||
apiclient.model.FLAGS.dump_request_response = True
|
apiclient.model.FLAGS.dump_request_response = True
|
||||||
model = LoggingJsonModel()
|
model = JsonModel()
|
||||||
request_body = {
|
request_body = {
|
||||||
'field1': 'value1',
|
'field1': 'value1',
|
||||||
'field2': 'value2'
|
'field2': 'value2'
|
||||||
@@ -234,7 +240,7 @@ class LoggingModel(unittest.TestCase):
|
|||||||
request_body)
|
request_body)
|
||||||
self.assertEqual(apiclient.model.logging.info_record[-1],
|
self.assertEqual(apiclient.model.logging.info_record[-1],
|
||||||
'--response-end--')
|
'--response-end--')
|
||||||
|
apiclient.model.logging = old_logging
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|||||||
106
tests/test_protobuf_model.py
Normal file
106
tests/test_protobuf_model.py
Normal file
@@ -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()
|
||||||
Reference in New Issue
Block a user