Package googleapiclient :: Module model |
[hide private]
[frames] | no frames]

# Source Code for Module googleapiclient.model

      1  # Copyright 2014 Google Inc. All Rights Reserved.
      2  #
      3  # Licensed under the Apache License, Version 2.0 (the "License");
      4  # you may not use this file except in compliance with the License.
      5  # You may obtain a copy of the License at
      6  #
      7  #      http://www.apache.org/licenses/LICENSE-2.0
      8  #
      9  # Unless required by applicable law or agreed to in writing, software
     10  # distributed under the License is distributed on an "AS IS" BASIS,
     11  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     12  # See the License for the specific language governing permissions and
     13  # limitations under the License.
     14
     15  """Model objects for requests and responses.
     16
     17  Each API may support one or more serializations, such
     18  as JSON, Atom, etc. The model classes are responsible
     19  for converting between the wire format and the Python
     20  object representation.
     21  """
     22  from __future__ import absolute_import
     23  import six
     24
     25  __author__ = "jcgregorio@google.com (Joe Gregorio)"
     26
     27  import json
     28  import logging
     29  import platform
     30  import pkg_resources
     31
     32  from six.moves.urllib.parse import urlencode
     33
     34  from googleapiclient.errors import HttpError
     35
     36  _LIBRARY_VERSION = pkg_resources.get_distribution("google-api-python-client").version
     37  _PY_VERSION = platform.python_version()
     38
     39  LOGGER = logging.getLogger(__name__)
     40
     41  dump_request_response = False
     42
     43
     44 -def _abstract():
     45      raise NotImplementedError("You need to override this function")
     46
     47
     48 -class Model(object):
     49      """Model base class.
     50
     51    All Model classes should implement this interface.
     52    The Model serializes and de-serializes between a wire
     53    format such as JSON and a Python object representation.
     54    """
     55
     56 -    def request(self, headers, path_params, query_params, body_value):
     57          """Updates outgoing requests with a serialized body.
     58
     59      Args:
     60        headers: dict, request headers
     61        path_params: dict, parameters that appear in the request path
     62        query_params: dict, parameters that appear in the query
     63        body_value: object, the request body as a Python object, which must be
     64                    serializable.
     65      Returns:
     66        A tuple of (headers, path_params, query, body)
     67
     68        headers: dict, request headers
     69        path_params: dict, parameters that appear in the request path
     70        query: string, query part of the request URI
     71        body: string, the body serialized in the desired wire format.
     72      """
     73          _abstract()
     74
     75 -    def response(self, resp, content):
     76          """Convert the response wire format into a Python object.
     77
     78      Args:
     79        resp: httplib2.Response, the HTTP response headers and status
     80        content: string, the body of the HTTP response
     81
     82      Returns:
     83        The body de-serialized as a Python object.
     84
     85      Raises:
     86        googleapiclient.errors.HttpError if a non 2xx response is received.
     87      """
     88          _abstract()
     89
     90
     91 -class BaseModel(Model):
     92      """Base model class.
     93
     94    Subclasses should provide implementations for the "serialize" and
     95    "deserialize" methods, as well as values for the following class attributes.
     96
     97    Attributes:
     98      accept: The value to use for the HTTP Accept header.
     99      content_type: The value to use for the HTTP Content-type header.
    100      no_content_response: The value to return when deserializing a 204 "No
    101          Content" response.
    102      alt_param: The value to supply as the "alt" query parameter for requests.
    103    """
    104
    105      accept = None
    106      content_type = None
    107      no_content_response = None
    108      alt_param = None
    109
    110 -    def _log_request(self, headers, path_params, query, body):
    111          """Logs debugging information about the request if requested."""
    112          if dump_request_response:
    113              LOGGER.info("--request-start--")
    114              LOGGER.info("-headers-start-")
    115              for h, v in six.iteritems(headers):
    116                  LOGGER.info("%s: %s", h, v)
    117              LOGGER.info("-headers-end-")
    118              LOGGER.info("-path-parameters-start-")
    119              for h, v in six.iteritems(path_params):
    120                  LOGGER.info("%s: %s", h, v)
    121              LOGGER.info("-path-parameters-end-")
    122              LOGGER.info("body: %s", body)
    123              LOGGER.info("query: %s", query)
    124              LOGGER.info("--request-end--")
    125
    126 -    def request(self, headers, path_params, query_params, body_value):
    127          """Updates outgoing requests with a serialized body.
    128
    129      Args:
    130        headers: dict, request headers
    131        path_params: dict, parameters that appear in the request path
    132        query_params: dict, parameters that appear in the query
    133        body_value: object, the request body as a Python object, which must be
    134                    serializable by json.
    135      Returns:
    136        A tuple of (headers, path_params, query, body)
    137
    138        headers: dict, request headers
    139        path_params: dict, parameters that appear in the request path
    140        query: string, query part of the request URI
    141        body: string, the body serialized as JSON
    142      """
    143          query = self._build_query(query_params)
    144          headers["accept"] = self.accept
    145          headers["accept-encoding"] = "gzip, deflate"
    146          if "user-agent" in headers:
    147              headers["user-agent"] += " "
    148          else:
    149              headers["user-agent"] = ""
    150          headers["user-agent"] += "(gzip)"
    151          if "x-goog-api-client" in headers:
    152              headers["x-goog-api-client"] += " "
    153          else:
    154              headers["x-goog-api-client"] = ""
    155          headers["x-goog-api-client"] += "gdcl/%s gl-python/%s" % (
    156              _LIBRARY_VERSION,
    157              _PY_VERSION,
    158          )
    159
    160          if body_value is not None:
    161              headers["content-type"] = self.content_type
    162              body_value = self.serialize(body_value)
    163          self._log_request(headers, path_params, query, body_value)
    164          return (headers, path_params, query, body_value)
    165
    166 -    def _build_query(self, params):
    167          """Builds a query string.
    168
    169      Args:
    170        params: dict, the query parameters
    171
    172      Returns:
    173        The query parameters properly encoded into an HTTP URI query string.
    174      """
    175          if self.alt_param is not None:
    176              params.update({"alt": self.alt_param})
    177          astuples = []
    178          for key, value in six.iteritems(params):
    179              if type(value) == type([]):
    180                  for x in value:
    181                      x = x.encode("utf-8")
    182                      astuples.append((key, x))
    183              else:
    184                  if isinstance(value, six.text_type) and callable(value.encode):
    185                      value = value.encode("utf-8")
    186                  astuples.append((key, value))
    187          return "?" + urlencode(astuples)
    188
    189 -    def _log_response(self, resp, content):
    190          """Logs debugging information about the response if requested."""
    191          if dump_request_response:
    192              LOGGER.info("--response-start--")
    193              for h, v in six.iteritems(resp):
    194                  LOGGER.info("%s: %s", h, v)
    195              if content:
    196                  LOGGER.info(content)
    197              LOGGER.info("--response-end--")
    198
    199 -    def response(self, resp, content):
    200          """Convert the response wire format into a Python object.
    201
    202      Args:
    203        resp: httplib2.Response, the HTTP response headers and status
    204        content: string, the body of the HTTP response
    205
    206      Returns:
    207        The body de-serialized as a Python object.
    208
    209      Raises:
    210        googleapiclient.errors.HttpError if a non 2xx response is received.
    211      """
    212          self._log_response(resp, content)
    213          # Error handling is TBD, for example, do we retry
    214          # for some operation/error combinations?
    215          if resp.status < 300:
    216              if resp.status == 204:
    217                  # A 204: No Content response should be treated differently
    218                  # to all the other success states
    219                  return self.no_content_response
    220              return self.deserialize(content)
    221          else:
    222              LOGGER.debug("Content from bad request was: %r" % content)
    223              raise HttpError(resp, content)
    224
    225 -    def serialize(self, body_value):
    226          """Perform the actual Python object serialization.
    227
    228      Args:
    229        body_value: object, the request body as a Python object.
    230
    231      Returns:
    232        string, the body in serialized form.
    233      """
    234          _abstract()
    235
    236 -    def deserialize(self, content):
    237          """Perform the actual deserialization from response string to Python
    238      object.
    239
    240      Args:
    241        content: string, the body of the HTTP response
    242
    243      Returns:
    244        The body de-serialized as a Python object.
    245      """
    246          _abstract()
    247
    248
    249 -class JsonModel(BaseModel):
    250      """Model class for JSON.
    251
    252    Serializes and de-serializes between JSON and the Python
    253    object representation of HTTP request and response bodies.
    254    """
    255
    256      accept = "application/json"
    257      content_type = "application/json"
    258      alt_param = "json"
    259
    260 -    def __init__(self, data_wrapper=False):
    261          """Construct a JsonModel.
    262
    263      Args:
    264        data_wrapper: boolean, wrap requests and responses in a data wrapper
    265      """
    266          self._data_wrapper = data_wrapper
    267
    268 -    def serialize(self, body_value):
    269          if (
    270              isinstance(body_value, dict)
    271              and "data" not in body_value
    272              and self._data_wrapper
    273          ):
    274              body_value = {"data": body_value}
    275          return json.dumps(body_value)
    276
    277 -    def deserialize(self, content):
    278          try:
    279              content = content.decode("utf-8")
    280          except AttributeError:
    281              pass
    282          body = json.loads(content)
    283          if self._data_wrapper and isinstance(body, dict) and "data" in body:
    284              body = body["data"]
    285          return body
    286
    287      @property
    288 -    def no_content_response(self):
    289          return {}
    290
    291
    292 -class RawModel(JsonModel):
    293      """Model class for requests that don't return JSON.
    294
    295    Serializes and de-serializes between JSON and the Python
    296    object representation of HTTP request, and returns the raw bytes
    297    of the response body.
    298    """
    299
    300      accept = "*/*"
    301      content_type = "application/json"
    302      alt_param = None
    303
    304 -    def deserialize(self, content):
    305          return content
    306
    307      @property
    308 -    def no_content_response(self):
    309          return ""
    310
    311
    312 -class MediaModel(JsonModel):
    313      """Model class for requests that return Media.
    314
    315    Serializes and de-serializes between JSON and the Python
    316    object representation of HTTP request, and returns the raw bytes
    317    of the response body.
    318    """
    319
    320      accept = "*/*"
    321      content_type = "application/json"
    322      alt_param = "media"
    323
    324 -    def deserialize(self, content):
    325          return content
    326
    327      @property
    328 -    def no_content_response(self):
    329          return ""
    330
    331
    332 -class ProtocolBufferModel(BaseModel):
    333      """Model class for protocol buffers.
    334
    335    Serializes and de-serializes the binary protocol buffer sent in the HTTP
    336    request and response bodies.
    337    """
    338
    339      accept = "application/x-protobuf"
    340      content_type = "application/x-protobuf"
    341      alt_param = "proto"
    342
    343 -    def __init__(self, protocol_buffer):
    344          """Constructs a ProtocolBufferModel.
    345
    346      The serialized protocol buffer returned in an HTTP response will be
    347      de-serialized using the given protocol buffer class.
    348
    349      Args:
    350        protocol_buffer: The protocol buffer class used to de-serialize a
    351        response from the API.
    352      """
    353          self._protocol_buffer = protocol_buffer
    354
    355 -    def serialize(self, body_value):
    356          return body_value.SerializeToString()
    357
    358 -    def deserialize(self, content):
    359          return self._protocol_buffer.FromString(content)
    360
    361      @property
    362 -    def no_content_response(self):
    363          return self._protocol_buffer()
    364
    365
    366 -def makepatch(original, modified):
    367      """Create a patch object.
    368
    369    Some methods support PATCH, an efficient way to send updates to a resource.
    370    This method allows the easy construction of patch bodies by looking at the
    371    differences between a resource before and after it was modified.
    372
    373    Args:
    374      original: object, the original deserialized resource
    375      modified: object, the modified deserialized resource
    376    Returns:
    377      An object that contains only the changes from original to modified, in a
    378      form suitable to pass to a PATCH method.
    379
    380    Example usage:
    381      item = service.activities().get(postid=postid, userid=userid).execute()
    382      original = copy.deepcopy(item)
    383      item['object']['content'] = 'This is updated.'
    384      service.activities.patch(postid=postid, userid=userid,
    385        body=makepatch(original, item)).execute()
    386    """
    387      patch = {}
    388      for key, original_value in six.iteritems(original):
    389          modified_value = modified.get(key, None)
    390          if modified_value is None:
    391              # Use None to signal that the element is deleted
    392              patch[key] = None
    393          elif original_value != modified_value:
    394              if type(original_value) == type({}):
    395                  # Recursively descend objects
    396                  patch[key] = makepatch(original_value, modified_value)
    397              else:
    398                  # In the case of simple types or arrays we just replace
    399                  patch[key] = modified_value
    400          else:
    401              # Don't add anything to patch if there's no change
    402              pass
    403      for key in modified:
    404          if key not in original:
    405              patch[key] = modified[key]
    406
    407      return patch
    408