• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
17Each API may support one or more serializations, such
18as JSON, Atom, etc. The model classes are responsible
19for converting between the wire format and the Python
20object representation.
21"""
22from __future__ import absolute_import
23import six
24
25__author__ = 'jcgregorio@google.com (Joe Gregorio)'
26
27import json
28import logging
29
30from six.moves.urllib.parse import urlencode
31
32from googleapiclient import __version__
33from googleapiclient.errors import HttpError
34
35
36LOGGER = logging.getLogger(__name__)
37
38dump_request_response = False
39
40
41def _abstract():
42  raise NotImplementedError('You need to override this function')
43
44
45class 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
88class 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
238class 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
277class 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
296class 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
315class 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
348def 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