blob: f58549c49ead05eaa0022bf67faeaa220fa265a0 [file] [log] [blame]
Craig Citro751b7fb2014-09-23 11:20:38 -07001# Copyright 2014 Google Inc. All Rights Reserved.
John Asmuth864311d2014-04-24 15:46:08 -04002#
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"""
INADA Naoki0bceb332014-08-20 15:27:52 +090022from __future__ import absolute_import
INADA Naokie4ea1a92015-03-04 03:45:42 +090023import six
John Asmuth864311d2014-04-24 15:46:08 -040024
Bu Sun Kim66bb32c2019-10-30 10:11:58 -070025__author__ = "jcgregorio@google.com (Joe Gregorio)"
John Asmuth864311d2014-04-24 15:46:08 -040026
Craig Citro6ae34d72014-08-18 23:10:09 -070027import json
John Asmuth864311d2014-04-24 15:46:08 -040028import logging
Bu Sun Kim07f647c2019-08-09 14:55:24 -070029import platform
Bu Sun Kimf706cfd2020-04-20 14:05:05 -070030import pkg_resources
Pat Ferated5b61bd2015-03-03 16:04:11 -080031
32from six.moves.urllib.parse import urlencode
John Asmuth864311d2014-04-24 15:46:08 -040033
Pat Ferateb240c172015-03-03 16:23:51 -080034from googleapiclient.errors import HttpError
John Asmuth864311d2014-04-24 15:46:08 -040035
Bu Sun Kimf706cfd2020-04-20 14:05:05 -070036_LIBRARY_VERSION = pkg_resources.get_distribution("google-api-python-client").version
Bu Sun Kim07f647c2019-08-09 14:55:24 -070037_PY_VERSION = platform.python_version()
John Asmuth864311d2014-04-24 15:46:08 -040038
Emmett Butler09699152016-02-08 14:26:00 -080039LOGGER = logging.getLogger(__name__)
40
John Asmuth864311d2014-04-24 15:46:08 -040041dump_request_response = False
42
43
44def _abstract():
Bu Sun Kim66bb32c2019-10-30 10:11:58 -070045 raise NotImplementedError("You need to override this function")
John Asmuth864311d2014-04-24 15:46:08 -040046
47
48class Model(object):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -070049 """Model base class.
John Asmuth864311d2014-04-24 15:46:08 -040050
51 All Model classes should implement this interface.
52 The Model serializes and de-serializes between a wire
53 format such as JSON and a Python object representation.
54 """
55
Bu Sun Kim66bb32c2019-10-30 10:11:58 -070056 def request(self, headers, path_params, query_params, body_value):
57 """Updates outgoing requests with a serialized body.
John Asmuth864311d2014-04-24 15:46:08 -040058
59 Args:
60 headers: dict, request headers
61 path_params: dict, parameters that appear in the request path
62 query_params: dict, parameters that appear in the query
63 body_value: object, the request body as a Python object, which must be
64 serializable.
65 Returns:
66 A tuple of (headers, path_params, query, body)
67
68 headers: dict, request headers
69 path_params: dict, parameters that appear in the request path
70 query: string, query part of the request URI
71 body: string, the body serialized in the desired wire format.
72 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -070073 _abstract()
John Asmuth864311d2014-04-24 15:46:08 -040074
Bu Sun Kim66bb32c2019-10-30 10:11:58 -070075 def response(self, resp, content):
76 """Convert the response wire format into a Python object.
John Asmuth864311d2014-04-24 15:46:08 -040077
78 Args:
79 resp: httplib2.Response, the HTTP response headers and status
80 content: string, the body of the HTTP response
81
82 Returns:
83 The body de-serialized as a Python object.
84
85 Raises:
86 googleapiclient.errors.HttpError if a non 2xx response is received.
87 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -070088 _abstract()
John Asmuth864311d2014-04-24 15:46:08 -040089
90
91class BaseModel(Model):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -070092 """Base model class.
John Asmuth864311d2014-04-24 15:46:08 -040093
94 Subclasses should provide implementations for the "serialize" and
95 "deserialize" methods, as well as values for the following class attributes.
96
97 Attributes:
98 accept: The value to use for the HTTP Accept header.
99 content_type: The value to use for the HTTP Content-type header.
100 no_content_response: The value to return when deserializing a 204 "No
101 Content" response.
102 alt_param: The value to supply as the "alt" query parameter for requests.
103 """
104
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700105 accept = None
106 content_type = None
107 no_content_response = None
108 alt_param = None
John Asmuth864311d2014-04-24 15:46:08 -0400109
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700110 def _log_request(self, headers, path_params, query, body):
111 """Logs debugging information about the request if requested."""
112 if dump_request_response:
113 LOGGER.info("--request-start--")
114 LOGGER.info("-headers-start-")
115 for h, v in six.iteritems(headers):
116 LOGGER.info("%s: %s", h, v)
117 LOGGER.info("-headers-end-")
118 LOGGER.info("-path-parameters-start-")
119 for h, v in six.iteritems(path_params):
120 LOGGER.info("%s: %s", h, v)
121 LOGGER.info("-path-parameters-end-")
122 LOGGER.info("body: %s", body)
123 LOGGER.info("query: %s", query)
124 LOGGER.info("--request-end--")
John Asmuth864311d2014-04-24 15:46:08 -0400125
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700126 def request(self, headers, path_params, query_params, body_value):
127 """Updates outgoing requests with a serialized body.
John Asmuth864311d2014-04-24 15:46:08 -0400128
129 Args:
130 headers: dict, request headers
131 path_params: dict, parameters that appear in the request path
132 query_params: dict, parameters that appear in the query
133 body_value: object, the request body as a Python object, which must be
Craig Citro6ae34d72014-08-18 23:10:09 -0700134 serializable by json.
John Asmuth864311d2014-04-24 15:46:08 -0400135 Returns:
136 A tuple of (headers, path_params, query, body)
137
138 headers: dict, request headers
139 path_params: dict, parameters that appear in the request path
140 query: string, query part of the request URI
141 body: string, the body serialized as JSON
142 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700143 query = self._build_query(query_params)
144 headers["accept"] = self.accept
145 headers["accept-encoding"] = "gzip, deflate"
146 if "user-agent" in headers:
147 headers["user-agent"] += " "
148 else:
149 headers["user-agent"] = ""
150 headers["user-agent"] += "(gzip)"
151 if "x-goog-api-client" in headers:
152 headers["x-goog-api-client"] += " "
153 else:
154 headers["x-goog-api-client"] = ""
155 headers["x-goog-api-client"] += "gdcl/%s gl-python/%s" % (
Bu Sun Kimf706cfd2020-04-20 14:05:05 -0700156 _LIBRARY_VERSION,
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700157 _PY_VERSION,
158 )
John Asmuth864311d2014-04-24 15:46:08 -0400159
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700160 if body_value is not None:
161 headers["content-type"] = self.content_type
162 body_value = self.serialize(body_value)
163 self._log_request(headers, path_params, query, body_value)
164 return (headers, path_params, query, body_value)
John Asmuth864311d2014-04-24 15:46:08 -0400165
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700166 def _build_query(self, params):
167 """Builds a query string.
John Asmuth864311d2014-04-24 15:46:08 -0400168
169 Args:
170 params: dict, the query parameters
171
172 Returns:
173 The query parameters properly encoded into an HTTP URI query string.
174 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700175 if self.alt_param is not None:
176 params.update({"alt": self.alt_param})
177 astuples = []
178 for key, value in six.iteritems(params):
179 if type(value) == type([]):
180 for x in value:
181 x = x.encode("utf-8")
182 astuples.append((key, x))
183 else:
184 if isinstance(value, six.text_type) and callable(value.encode):
185 value = value.encode("utf-8")
186 astuples.append((key, value))
187 return "?" + urlencode(astuples)
John Asmuth864311d2014-04-24 15:46:08 -0400188
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700189 def _log_response(self, resp, content):
190 """Logs debugging information about the response if requested."""
191 if dump_request_response:
192 LOGGER.info("--response-start--")
193 for h, v in six.iteritems(resp):
194 LOGGER.info("%s: %s", h, v)
195 if content:
196 LOGGER.info(content)
197 LOGGER.info("--response-end--")
John Asmuth864311d2014-04-24 15:46:08 -0400198
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700199 def response(self, resp, content):
200 """Convert the response wire format into a Python object.
John Asmuth864311d2014-04-24 15:46:08 -0400201
202 Args:
203 resp: httplib2.Response, the HTTP response headers and status
204 content: string, the body of the HTTP response
205
206 Returns:
207 The body de-serialized as a Python object.
208
209 Raises:
210 googleapiclient.errors.HttpError if a non 2xx response is received.
211 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700212 self._log_response(resp, content)
213 # Error handling is TBD, for example, do we retry
214 # for some operation/error combinations?
215 if resp.status < 300:
216 if resp.status == 204:
217 # A 204: No Content response should be treated differently
218 # to all the other success states
219 return self.no_content_response
220 return self.deserialize(content)
221 else:
Matt McDonaldef6420a2020-04-14 16:28:13 -0400222 LOGGER.debug("Content from bad request was: %r" % content)
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700223 raise HttpError(resp, content)
John Asmuth864311d2014-04-24 15:46:08 -0400224
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700225 def serialize(self, body_value):
226 """Perform the actual Python object serialization.
John Asmuth864311d2014-04-24 15:46:08 -0400227
228 Args:
229 body_value: object, the request body as a Python object.
230
231 Returns:
232 string, the body in serialized form.
233 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700234 _abstract()
John Asmuth864311d2014-04-24 15:46:08 -0400235
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700236 def deserialize(self, content):
237 """Perform the actual deserialization from response string to Python
John Asmuth864311d2014-04-24 15:46:08 -0400238 object.
239
240 Args:
241 content: string, the body of the HTTP response
242
243 Returns:
244 The body de-serialized as a Python object.
245 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700246 _abstract()
John Asmuth864311d2014-04-24 15:46:08 -0400247
248
249class JsonModel(BaseModel):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700250 """Model class for JSON.
John Asmuth864311d2014-04-24 15:46:08 -0400251
252 Serializes and de-serializes between JSON and the Python
253 object representation of HTTP request and response bodies.
254 """
John Asmuth864311d2014-04-24 15:46:08 -0400255
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700256 accept = "application/json"
257 content_type = "application/json"
258 alt_param = "json"
259
260 def __init__(self, data_wrapper=False):
261 """Construct a JsonModel.
John Asmuth864311d2014-04-24 15:46:08 -0400262
263 Args:
264 data_wrapper: boolean, wrap requests and responses in a data wrapper
265 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700266 self._data_wrapper = data_wrapper
John Asmuth864311d2014-04-24 15:46:08 -0400267
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700268 def serialize(self, body_value):
269 if (
270 isinstance(body_value, dict)
271 and "data" not in body_value
272 and self._data_wrapper
273 ):
274 body_value = {"data": body_value}
275 return json.dumps(body_value)
John Asmuth864311d2014-04-24 15:46:08 -0400276
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700277 def deserialize(self, content):
278 try:
279 content = content.decode("utf-8")
280 except AttributeError:
281 pass
282 body = json.loads(content)
283 if self._data_wrapper and isinstance(body, dict) and "data" in body:
284 body = body["data"]
285 return body
John Asmuth864311d2014-04-24 15:46:08 -0400286
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700287 @property
288 def no_content_response(self):
289 return {}
John Asmuth864311d2014-04-24 15:46:08 -0400290
291
292class RawModel(JsonModel):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700293 """Model class for requests that don't return JSON.
John Asmuth864311d2014-04-24 15:46:08 -0400294
295 Serializes and de-serializes between JSON and the Python
296 object representation of HTTP request, and returns the raw bytes
297 of the response body.
298 """
John Asmuth864311d2014-04-24 15:46:08 -0400299
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700300 accept = "*/*"
301 content_type = "application/json"
302 alt_param = None
John Asmuth864311d2014-04-24 15:46:08 -0400303
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700304 def deserialize(self, content):
305 return content
306
307 @property
308 def no_content_response(self):
309 return ""
John Asmuth864311d2014-04-24 15:46:08 -0400310
311
312class MediaModel(JsonModel):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700313 """Model class for requests that return Media.
John Asmuth864311d2014-04-24 15:46:08 -0400314
315 Serializes and de-serializes between JSON and the Python
316 object representation of HTTP request, and returns the raw bytes
317 of the response body.
318 """
John Asmuth864311d2014-04-24 15:46:08 -0400319
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700320 accept = "*/*"
321 content_type = "application/json"
322 alt_param = "media"
John Asmuth864311d2014-04-24 15:46:08 -0400323
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700324 def deserialize(self, content):
325 return content
326
327 @property
328 def no_content_response(self):
329 return ""
John Asmuth864311d2014-04-24 15:46:08 -0400330
331
332class ProtocolBufferModel(BaseModel):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700333 """Model class for protocol buffers.
John Asmuth864311d2014-04-24 15:46:08 -0400334
335 Serializes and de-serializes the binary protocol buffer sent in the HTTP
336 request and response bodies.
337 """
John Asmuth864311d2014-04-24 15:46:08 -0400338
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700339 accept = "application/x-protobuf"
340 content_type = "application/x-protobuf"
341 alt_param = "proto"
342
343 def __init__(self, protocol_buffer):
344 """Constructs a ProtocolBufferModel.
John Asmuth864311d2014-04-24 15:46:08 -0400345
Jason Banich5dac8052020-01-23 13:50:42 -0800346 The serialized protocol buffer returned in an HTTP response will be
John Asmuth864311d2014-04-24 15:46:08 -0400347 de-serialized using the given protocol buffer class.
348
349 Args:
350 protocol_buffer: The protocol buffer class used to de-serialize a
351 response from the API.
352 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700353 self._protocol_buffer = protocol_buffer
John Asmuth864311d2014-04-24 15:46:08 -0400354
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700355 def serialize(self, body_value):
356 return body_value.SerializeToString()
John Asmuth864311d2014-04-24 15:46:08 -0400357
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700358 def deserialize(self, content):
359 return self._protocol_buffer.FromString(content)
John Asmuth864311d2014-04-24 15:46:08 -0400360
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700361 @property
362 def no_content_response(self):
363 return self._protocol_buffer()
John Asmuth864311d2014-04-24 15:46:08 -0400364
365
366def makepatch(original, modified):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700367 """Create a patch object.
John Asmuth864311d2014-04-24 15:46:08 -0400368
369 Some methods support PATCH, an efficient way to send updates to a resource.
370 This method allows the easy construction of patch bodies by looking at the
371 differences between a resource before and after it was modified.
372
373 Args:
374 original: object, the original deserialized resource
375 modified: object, the modified deserialized resource
376 Returns:
377 An object that contains only the changes from original to modified, in a
378 form suitable to pass to a PATCH method.
379
380 Example usage:
381 item = service.activities().get(postid=postid, userid=userid).execute()
382 original = copy.deepcopy(item)
383 item['object']['content'] = 'This is updated.'
384 service.activities.patch(postid=postid, userid=userid,
385 body=makepatch(original, item)).execute()
386 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700387 patch = {}
388 for key, original_value in six.iteritems(original):
389 modified_value = modified.get(key, None)
390 if modified_value is None:
391 # Use None to signal that the element is deleted
392 patch[key] = None
393 elif original_value != modified_value:
394 if type(original_value) == type({}):
395 # Recursively descend objects
396 patch[key] = makepatch(original_value, modified_value)
397 else:
398 # In the case of simple types or arrays we just replace
399 patch[key] = modified_value
400 else:
401 # Don't add anything to patch if there's no change
402 pass
403 for key in modified:
404 if key not in original:
405 patch[key] = modified[key]
John Asmuth864311d2014-04-24 15:46:08 -0400406
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700407 return patch