407 lines
13 KiB
Python
407 lines
13 KiB
Python
# Copyright 2014 Google Inc. All Rights Reserved.
|
|
#
|
|
# 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.
|
|
|
|
"""Model objects for requests and responses.
|
|
|
|
Each API may support one or more serializations, such
|
|
as JSON, Atom, etc. The model classes are responsible
|
|
for converting between the wire format and the Python
|
|
object representation.
|
|
"""
|
|
from __future__ import absolute_import
|
|
import six
|
|
|
|
__author__ = "jcgregorio@google.com (Joe Gregorio)"
|
|
|
|
import json
|
|
import logging
|
|
import platform
|
|
import pkg_resources
|
|
|
|
from six.moves.urllib.parse import urlencode
|
|
|
|
from googleapiclient.errors import HttpError
|
|
|
|
_LIBRARY_VERSION = pkg_resources.get_distribution("google-api-python-client").version
|
|
_PY_VERSION = platform.python_version()
|
|
|
|
LOGGER = logging.getLogger(__name__)
|
|
|
|
dump_request_response = False
|
|
|
|
|
|
def _abstract():
|
|
raise NotImplementedError("You need to override this function")
|
|
|
|
|
|
class Model(object):
|
|
"""Model base class.
|
|
|
|
All Model classes should implement this interface.
|
|
The Model serializes and de-serializes between a wire
|
|
format such as JSON and a Python object representation.
|
|
"""
|
|
|
|
def request(self, headers, path_params, query_params, body_value):
|
|
"""Updates outgoing requests with a serialized body.
|
|
|
|
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.
|
|
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 in the desired wire format.
|
|
"""
|
|
_abstract()
|
|
|
|
def response(self, resp, content):
|
|
"""Convert the response wire format into a Python object.
|
|
|
|
Args:
|
|
resp: httplib2.Response, the HTTP response headers and status
|
|
content: string, the body of the HTTP response
|
|
|
|
Returns:
|
|
The body de-serialized as a Python object.
|
|
|
|
Raises:
|
|
googleapiclient.errors.HttpError if a non 2xx response is received.
|
|
"""
|
|
_abstract()
|
|
|
|
|
|
class BaseModel(Model):
|
|
"""Base model class.
|
|
|
|
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.
|
|
"""
|
|
|
|
accept = None
|
|
content_type = None
|
|
no_content_response = None
|
|
alt_param = None
|
|
|
|
def _log_request(self, headers, path_params, query, body):
|
|
"""Logs debugging information about the request if requested."""
|
|
if dump_request_response:
|
|
LOGGER.info("--request-start--")
|
|
LOGGER.info("-headers-start-")
|
|
for h, v in six.iteritems(headers):
|
|
LOGGER.info("%s: %s", h, v)
|
|
LOGGER.info("-headers-end-")
|
|
LOGGER.info("-path-parameters-start-")
|
|
for h, v in six.iteritems(path_params):
|
|
LOGGER.info("%s: %s", h, v)
|
|
LOGGER.info("-path-parameters-end-")
|
|
LOGGER.info("body: %s", body)
|
|
LOGGER.info("query: %s", query)
|
|
LOGGER.info("--request-end--")
|
|
|
|
def request(self, headers, path_params, query_params, body_value):
|
|
"""Updates outgoing requests with a serialized body.
|
|
|
|
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 json.
|
|
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
|
|
"""
|
|
query = self._build_query(query_params)
|
|
headers["accept"] = self.accept
|
|
headers["accept-encoding"] = "gzip, deflate"
|
|
if "user-agent" in headers:
|
|
headers["user-agent"] += " "
|
|
else:
|
|
headers["user-agent"] = ""
|
|
headers["user-agent"] += "(gzip)"
|
|
if "x-goog-api-client" in headers:
|
|
headers["x-goog-api-client"] += " "
|
|
else:
|
|
headers["x-goog-api-client"] = ""
|
|
headers["x-goog-api-client"] += "gdcl/%s gl-python/%s" % (
|
|
_LIBRARY_VERSION,
|
|
_PY_VERSION,
|
|
)
|
|
|
|
if body_value is not None:
|
|
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):
|
|
"""Builds a query string.
|
|
|
|
Args:
|
|
params: dict, the query parameters
|
|
|
|
Returns:
|
|
The query parameters properly encoded into an HTTP URI query string.
|
|
"""
|
|
if self.alt_param is not None:
|
|
params.update({"alt": self.alt_param})
|
|
astuples = []
|
|
for key, value in six.iteritems(params):
|
|
if type(value) == type([]):
|
|
for x in value:
|
|
x = x.encode("utf-8")
|
|
astuples.append((key, x))
|
|
else:
|
|
if isinstance(value, six.text_type) and callable(value.encode):
|
|
value = value.encode("utf-8")
|
|
astuples.append((key, value))
|
|
return "?" + urlencode(astuples)
|
|
|
|
def _log_response(self, resp, content):
|
|
"""Logs debugging information about the response if requested."""
|
|
if dump_request_response:
|
|
LOGGER.info("--response-start--")
|
|
for h, v in six.iteritems(resp):
|
|
LOGGER.info("%s: %s", h, v)
|
|
if content:
|
|
LOGGER.info(content)
|
|
LOGGER.info("--response-end--")
|
|
|
|
def response(self, resp, content):
|
|
"""Convert the response wire format into a Python object.
|
|
|
|
Args:
|
|
resp: httplib2.Response, the HTTP response headers and status
|
|
content: string, the body of the HTTP response
|
|
|
|
Returns:
|
|
The body de-serialized as a Python object.
|
|
|
|
Raises:
|
|
googleapiclient.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 self.no_content_response
|
|
return self.deserialize(content)
|
|
else:
|
|
LOGGER.debug("Content from bad request was: %r" % content)
|
|
raise HttpError(resp, content)
|
|
|
|
def serialize(self, body_value):
|
|
"""Perform the actual Python object serialization.
|
|
|
|
Args:
|
|
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.
|
|
"""
|
|
_abstract()
|
|
|
|
|
|
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:
|
|
data_wrapper: boolean, wrap requests and responses in a data wrapper
|
|
"""
|
|
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 json.dumps(body_value)
|
|
|
|
def deserialize(self, content):
|
|
try:
|
|
content = content.decode("utf-8")
|
|
except AttributeError:
|
|
pass
|
|
body = json.loads(content)
|
|
if self._data_wrapper and isinstance(body, dict) and "data" in body:
|
|
body = body["data"]
|
|
return body
|
|
|
|
@property
|
|
def no_content_response(self):
|
|
return {}
|
|
|
|
|
|
class RawModel(JsonModel):
|
|
"""Model class for requests that don't return JSON.
|
|
|
|
Serializes and de-serializes between JSON and the Python
|
|
object representation of HTTP request, and returns the raw bytes
|
|
of the response body.
|
|
"""
|
|
|
|
accept = "*/*"
|
|
content_type = "application/json"
|
|
alt_param = None
|
|
|
|
def deserialize(self, content):
|
|
return content
|
|
|
|
@property
|
|
def no_content_response(self):
|
|
return ""
|
|
|
|
|
|
class MediaModel(JsonModel):
|
|
"""Model class for requests that return Media.
|
|
|
|
Serializes and de-serializes between JSON and the Python
|
|
object representation of HTTP request, and returns the raw bytes
|
|
of the response body.
|
|
"""
|
|
|
|
accept = "*/*"
|
|
content_type = "application/json"
|
|
alt_param = "media"
|
|
|
|
def deserialize(self, content):
|
|
return content
|
|
|
|
@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 serialized 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()
|
|
|
|
|
|
def makepatch(original, modified):
|
|
"""Create a patch object.
|
|
|
|
Some methods support PATCH, an efficient way to send updates to a resource.
|
|
This method allows the easy construction of patch bodies by looking at the
|
|
differences between a resource before and after it was modified.
|
|
|
|
Args:
|
|
original: object, the original deserialized resource
|
|
modified: object, the modified deserialized resource
|
|
Returns:
|
|
An object that contains only the changes from original to modified, in a
|
|
form suitable to pass to a PATCH method.
|
|
|
|
Example usage:
|
|
item = service.activities().get(postid=postid, userid=userid).execute()
|
|
original = copy.deepcopy(item)
|
|
item['object']['content'] = 'This is updated.'
|
|
service.activities.patch(postid=postid, userid=userid,
|
|
body=makepatch(original, item)).execute()
|
|
"""
|
|
patch = {}
|
|
for key, original_value in six.iteritems(original):
|
|
modified_value = modified.get(key, None)
|
|
if modified_value is None:
|
|
# Use None to signal that the element is deleted
|
|
patch[key] = None
|
|
elif original_value != modified_value:
|
|
if type(original_value) == type({}):
|
|
# Recursively descend objects
|
|
patch[key] = makepatch(original_value, modified_value)
|
|
else:
|
|
# In the case of simple types or arrays we just replace
|
|
patch[key] = modified_value
|
|
else:
|
|
# Don't add anything to patch if there's no change
|
|
pass
|
|
for key in modified:
|
|
if key not in original:
|
|
patch[key] = modified[key]
|
|
|
|
return patch
|