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

# Source Code for Module apiclient.model

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