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