blob: 554056e3ef20edd170f54a3f720682c2ac074c59 [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
Pat Ferated5b61bd2015-03-03 16:04:11 -080030
31from six.moves.urllib.parse import urlencode
John Asmuth864311d2014-04-24 15:46:08 -040032
33from googleapiclient import __version__
Pat Ferateb240c172015-03-03 16:23:51 -080034from googleapiclient.errors import HttpError
John Asmuth864311d2014-04-24 15:46:08 -040035
Bu Sun Kim07f647c2019-08-09 14:55:24 -070036_PY_VERSION = platform.python_version()
John Asmuth864311d2014-04-24 15:46:08 -040037
Emmett Butler09699152016-02-08 14:26:00 -080038LOGGER = logging.getLogger(__name__)
39
John Asmuth864311d2014-04-24 15:46:08 -040040dump_request_response = False
41
42
43def _abstract():
Bu Sun Kim66bb32c2019-10-30 10:11:58 -070044 raise NotImplementedError("You need to override this function")
John Asmuth864311d2014-04-24 15:46:08 -040045
46
47class Model(object):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -070048 """Model base class.
John Asmuth864311d2014-04-24 15:46:08 -040049
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
Bu Sun Kim66bb32c2019-10-30 10:11:58 -070055 def request(self, headers, path_params, query_params, body_value):
56 """Updates outgoing requests with a serialized body.
John Asmuth864311d2014-04-24 15:46:08 -040057
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 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -070072 _abstract()
John Asmuth864311d2014-04-24 15:46:08 -040073
Bu Sun Kim66bb32c2019-10-30 10:11:58 -070074 def response(self, resp, content):
75 """Convert the response wire format into a Python object.
John Asmuth864311d2014-04-24 15:46:08 -040076
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 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -070087 _abstract()
John Asmuth864311d2014-04-24 15:46:08 -040088
89
90class BaseModel(Model):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -070091 """Base model class.
John Asmuth864311d2014-04-24 15:46:08 -040092
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
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700104 accept = None
105 content_type = None
106 no_content_response = None
107 alt_param = None
John Asmuth864311d2014-04-24 15:46:08 -0400108
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700109 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--")
John Asmuth864311d2014-04-24 15:46:08 -0400124
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700125 def request(self, headers, path_params, query_params, body_value):
126 """Updates outgoing requests with a serialized body.
John Asmuth864311d2014-04-24 15:46:08 -0400127
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
Craig Citro6ae34d72014-08-18 23:10:09 -0700133 serializable by json.
John Asmuth864311d2014-04-24 15:46:08 -0400134 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 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700142 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 )
John Asmuth864311d2014-04-24 15:46:08 -0400158
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700159 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)
John Asmuth864311d2014-04-24 15:46:08 -0400164
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700165 def _build_query(self, params):
166 """Builds a query string.
John Asmuth864311d2014-04-24 15:46:08 -0400167
168 Args:
169 params: dict, the query parameters
170
171 Returns:
172 The query parameters properly encoded into an HTTP URI query string.
173 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700174 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)
John Asmuth864311d2014-04-24 15:46:08 -0400187
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700188 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--")
John Asmuth864311d2014-04-24 15:46:08 -0400197
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700198 def response(self, resp, content):
199 """Convert the response wire format into a Python object.
John Asmuth864311d2014-04-24 15:46:08 -0400200
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 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700211 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)
John Asmuth864311d2014-04-24 15:46:08 -0400223
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700224 def serialize(self, body_value):
225 """Perform the actual Python object serialization.
John Asmuth864311d2014-04-24 15:46:08 -0400226
227 Args:
228 body_value: object, the request body as a Python object.
229
230 Returns:
231 string, the body in serialized form.
232 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700233 _abstract()
John Asmuth864311d2014-04-24 15:46:08 -0400234
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700235 def deserialize(self, content):
236 """Perform the actual deserialization from response string to Python
John Asmuth864311d2014-04-24 15:46:08 -0400237 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 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700245 _abstract()
John Asmuth864311d2014-04-24 15:46:08 -0400246
247
248class JsonModel(BaseModel):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700249 """Model class for JSON.
John Asmuth864311d2014-04-24 15:46:08 -0400250
251 Serializes and de-serializes between JSON and the Python
252 object representation of HTTP request and response bodies.
253 """
John Asmuth864311d2014-04-24 15:46:08 -0400254
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700255 accept = "application/json"
256 content_type = "application/json"
257 alt_param = "json"
258
259 def __init__(self, data_wrapper=False):
260 """Construct a JsonModel.
John Asmuth864311d2014-04-24 15:46:08 -0400261
262 Args:
263 data_wrapper: boolean, wrap requests and responses in a data wrapper
264 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700265 self._data_wrapper = data_wrapper
John Asmuth864311d2014-04-24 15:46:08 -0400266
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700267 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)
John Asmuth864311d2014-04-24 15:46:08 -0400275
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700276 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
John Asmuth864311d2014-04-24 15:46:08 -0400285
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700286 @property
287 def no_content_response(self):
288 return {}
John Asmuth864311d2014-04-24 15:46:08 -0400289
290
291class RawModel(JsonModel):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700292 """Model class for requests that don't return JSON.
John Asmuth864311d2014-04-24 15:46:08 -0400293
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 """
John Asmuth864311d2014-04-24 15:46:08 -0400298
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700299 accept = "*/*"
300 content_type = "application/json"
301 alt_param = None
John Asmuth864311d2014-04-24 15:46:08 -0400302
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700303 def deserialize(self, content):
304 return content
305
306 @property
307 def no_content_response(self):
308 return ""
John Asmuth864311d2014-04-24 15:46:08 -0400309
310
311class MediaModel(JsonModel):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700312 """Model class for requests that return Media.
John Asmuth864311d2014-04-24 15:46:08 -0400313
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 """
John Asmuth864311d2014-04-24 15:46:08 -0400318
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700319 accept = "*/*"
320 content_type = "application/json"
321 alt_param = "media"
John Asmuth864311d2014-04-24 15:46:08 -0400322
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700323 def deserialize(self, content):
324 return content
325
326 @property
327 def no_content_response(self):
328 return ""
John Asmuth864311d2014-04-24 15:46:08 -0400329
330
331class ProtocolBufferModel(BaseModel):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700332 """Model class for protocol buffers.
John Asmuth864311d2014-04-24 15:46:08 -0400333
334 Serializes and de-serializes the binary protocol buffer sent in the HTTP
335 request and response bodies.
336 """
John Asmuth864311d2014-04-24 15:46:08 -0400337
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700338 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.
John Asmuth864311d2014-04-24 15:46:08 -0400344
Jason Banich5dac8052020-01-23 13:50:42 -0800345 The serialized protocol buffer returned in an HTTP response will be
John Asmuth864311d2014-04-24 15:46:08 -0400346 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 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700352 self._protocol_buffer = protocol_buffer
John Asmuth864311d2014-04-24 15:46:08 -0400353
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700354 def serialize(self, body_value):
355 return body_value.SerializeToString()
John Asmuth864311d2014-04-24 15:46:08 -0400356
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700357 def deserialize(self, content):
358 return self._protocol_buffer.FromString(content)
John Asmuth864311d2014-04-24 15:46:08 -0400359
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700360 @property
361 def no_content_response(self):
362 return self._protocol_buffer()
John Asmuth864311d2014-04-24 15:46:08 -0400363
364
365def makepatch(original, modified):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700366 """Create a patch object.
John Asmuth864311d2014-04-24 15:46:08 -0400367
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 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700386 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]
John Asmuth864311d2014-04-24 15:46:08 -0400405
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700406 return patch