googleapiclient.model

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