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
36dump_request_response = False
37
38
39def _abstract():
40  raise NotImplementedError('You need to override this function')
41
42
43class 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
86class 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
236class 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
275class 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
294class 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
313class 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
346def 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