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