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