blob: b853a4f682eb8501c3fb31c7e9ced8b2520130ec [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
John Asmuth864311d2014-04-24 15:46:08 -040023
Bu Sun Kim66bb32c2019-10-30 10:11:58 -070024__author__ = "jcgregorio@google.com (Joe Gregorio)"
John Asmuth864311d2014-04-24 15:46:08 -040025
Craig Citro6ae34d72014-08-18 23:10:09 -070026import json
John Asmuth864311d2014-04-24 15:46:08 -040027import logging
Bu Sun Kim07f647c2019-08-09 14:55:24 -070028import platform
Bu Sun Kimf706cfd2020-04-20 14:05:05 -070029import pkg_resources
Anthonios Partheniou9f7b4102021-07-23 12:18:25 -040030import urllib
John Asmuth864311d2014-04-24 15:46:08 -040031
Pat Ferateb240c172015-03-03 16:23:51 -080032from googleapiclient.errors import HttpError
John Asmuth864311d2014-04-24 15:46:08 -040033
Bu Sun Kimf706cfd2020-04-20 14:05:05 -070034_LIBRARY_VERSION = pkg_resources.get_distribution("google-api-python-client").version
Bu Sun Kim07f647c2019-08-09 14:55:24 -070035_PY_VERSION = platform.python_version()
John Asmuth864311d2014-04-24 15:46:08 -040036
Emmett Butler09699152016-02-08 14:26:00 -080037LOGGER = logging.getLogger(__name__)
38
John Asmuth864311d2014-04-24 15:46:08 -040039dump_request_response = False
40
41
42def _abstract():
Bu Sun Kim66bb32c2019-10-30 10:11:58 -070043 raise NotImplementedError("You need to override this function")
John Asmuth864311d2014-04-24 15:46:08 -040044
45
46class Model(object):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -070047 """Model base class.
John Asmuth864311d2014-04-24 15:46:08 -040048
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
Bu Sun Kim66bb32c2019-10-30 10:11:58 -070054 def request(self, headers, path_params, query_params, body_value):
55 """Updates outgoing requests with a serialized body.
John Asmuth864311d2014-04-24 15:46:08 -040056
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 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -070071 _abstract()
John Asmuth864311d2014-04-24 15:46:08 -040072
Bu Sun Kim66bb32c2019-10-30 10:11:58 -070073 def response(self, resp, content):
74 """Convert the response wire format into a Python object.
John Asmuth864311d2014-04-24 15:46:08 -040075
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 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -070086 _abstract()
John Asmuth864311d2014-04-24 15:46:08 -040087
88
89class BaseModel(Model):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -070090 """Base model class.
John Asmuth864311d2014-04-24 15:46:08 -040091
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
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700103 accept = None
104 content_type = None
105 no_content_response = None
106 alt_param = None
John Asmuth864311d2014-04-24 15:46:08 -0400107
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700108 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-")
Anthonios Partheniou9f7b4102021-07-23 12:18:25 -0400113 for h, v in headers.items():
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700114 LOGGER.info("%s: %s", h, v)
115 LOGGER.info("-headers-end-")
116 LOGGER.info("-path-parameters-start-")
Anthonios Partheniou9f7b4102021-07-23 12:18:25 -0400117 for h, v in path_params.items():
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700118 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--")
John Asmuth864311d2014-04-24 15:46:08 -0400123
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700124 def request(self, headers, path_params, query_params, body_value):
125 """Updates outgoing requests with a serialized body.
John Asmuth864311d2014-04-24 15:46:08 -0400126
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
Craig Citro6ae34d72014-08-18 23:10:09 -0700132 serializable by json.
John Asmuth864311d2014-04-24 15:46:08 -0400133 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 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700141 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" % (
Bu Sun Kimf706cfd2020-04-20 14:05:05 -0700154 _LIBRARY_VERSION,
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700155 _PY_VERSION,
156 )
John Asmuth864311d2014-04-24 15:46:08 -0400157
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700158 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)
John Asmuth864311d2014-04-24 15:46:08 -0400163
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700164 def _build_query(self, params):
165 """Builds a query string.
John Asmuth864311d2014-04-24 15:46:08 -0400166
167 Args:
168 params: dict, the query parameters
169
170 Returns:
171 The query parameters properly encoded into an HTTP URI query string.
172 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700173 if self.alt_param is not None:
174 params.update({"alt": self.alt_param})
175 astuples = []
Anthonios Partheniou9f7b4102021-07-23 12:18:25 -0400176 for key, value in params.items():
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700177 if type(value) == type([]):
178 for x in value:
179 x = x.encode("utf-8")
180 astuples.append((key, x))
181 else:
Anthonios Partheniou9f7b4102021-07-23 12:18:25 -0400182 if isinstance(value, str) and callable(value.encode):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700183 value = value.encode("utf-8")
184 astuples.append((key, value))
Anthonios Partheniou9f7b4102021-07-23 12:18:25 -0400185 return "?" + urllib.parse.urlencode(astuples)
John Asmuth864311d2014-04-24 15:46:08 -0400186
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700187 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--")
Anthonios Partheniou9f7b4102021-07-23 12:18:25 -0400191 for h, v in resp.items():
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700192 LOGGER.info("%s: %s", h, v)
193 if content:
194 LOGGER.info(content)
195 LOGGER.info("--response-end--")
John Asmuth864311d2014-04-24 15:46:08 -0400196
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700197 def response(self, resp, content):
198 """Convert the response wire format into a Python object.
John Asmuth864311d2014-04-24 15:46:08 -0400199
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 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700210 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:
Matt McDonaldef6420a2020-04-14 16:28:13 -0400220 LOGGER.debug("Content from bad request was: %r" % content)
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700221 raise HttpError(resp, content)
John Asmuth864311d2014-04-24 15:46:08 -0400222
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700223 def serialize(self, body_value):
224 """Perform the actual Python object serialization.
John Asmuth864311d2014-04-24 15:46:08 -0400225
226 Args:
227 body_value: object, the request body as a Python object.
228
229 Returns:
230 string, the body in serialized form.
231 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700232 _abstract()
John Asmuth864311d2014-04-24 15:46:08 -0400233
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700234 def deserialize(self, content):
235 """Perform the actual deserialization from response string to Python
John Asmuth864311d2014-04-24 15:46:08 -0400236 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 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700244 _abstract()
John Asmuth864311d2014-04-24 15:46:08 -0400245
246
247class JsonModel(BaseModel):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700248 """Model class for JSON.
John Asmuth864311d2014-04-24 15:46:08 -0400249
250 Serializes and de-serializes between JSON and the Python
251 object representation of HTTP request and response bodies.
252 """
John Asmuth864311d2014-04-24 15:46:08 -0400253
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700254 accept = "application/json"
255 content_type = "application/json"
256 alt_param = "json"
257
258 def __init__(self, data_wrapper=False):
259 """Construct a JsonModel.
John Asmuth864311d2014-04-24 15:46:08 -0400260
261 Args:
262 data_wrapper: boolean, wrap requests and responses in a data wrapper
263 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700264 self._data_wrapper = data_wrapper
John Asmuth864311d2014-04-24 15:46:08 -0400265
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700266 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)
John Asmuth864311d2014-04-24 15:46:08 -0400274
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700275 def deserialize(self, content):
276 try:
277 content = content.decode("utf-8")
278 except AttributeError:
279 pass
280 body = json.loads(content)
281 if self._data_wrapper and isinstance(body, dict) and "data" in body:
282 body = body["data"]
283 return body
John Asmuth864311d2014-04-24 15:46:08 -0400284
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700285 @property
286 def no_content_response(self):
287 return {}
John Asmuth864311d2014-04-24 15:46:08 -0400288
289
290class RawModel(JsonModel):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700291 """Model class for requests that don't return JSON.
John Asmuth864311d2014-04-24 15:46:08 -0400292
293 Serializes and de-serializes between JSON and the Python
294 object representation of HTTP request, and returns the raw bytes
295 of the response body.
296 """
John Asmuth864311d2014-04-24 15:46:08 -0400297
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700298 accept = "*/*"
299 content_type = "application/json"
300 alt_param = None
John Asmuth864311d2014-04-24 15:46:08 -0400301
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700302 def deserialize(self, content):
303 return content
304
305 @property
306 def no_content_response(self):
307 return ""
John Asmuth864311d2014-04-24 15:46:08 -0400308
309
310class MediaModel(JsonModel):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700311 """Model class for requests that return Media.
John Asmuth864311d2014-04-24 15:46:08 -0400312
313 Serializes and de-serializes between JSON and the Python
314 object representation of HTTP request, and returns the raw bytes
315 of the response body.
316 """
John Asmuth864311d2014-04-24 15:46:08 -0400317
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700318 accept = "*/*"
319 content_type = "application/json"
320 alt_param = "media"
John Asmuth864311d2014-04-24 15:46:08 -0400321
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700322 def deserialize(self, content):
323 return content
324
325 @property
326 def no_content_response(self):
327 return ""
John Asmuth864311d2014-04-24 15:46:08 -0400328
329
330class ProtocolBufferModel(BaseModel):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700331 """Model class for protocol buffers.
John Asmuth864311d2014-04-24 15:46:08 -0400332
333 Serializes and de-serializes the binary protocol buffer sent in the HTTP
334 request and response bodies.
335 """
John Asmuth864311d2014-04-24 15:46:08 -0400336
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700337 accept = "application/x-protobuf"
338 content_type = "application/x-protobuf"
339 alt_param = "proto"
340
341 def __init__(self, protocol_buffer):
342 """Constructs a ProtocolBufferModel.
John Asmuth864311d2014-04-24 15:46:08 -0400343
Jason Banich5dac8052020-01-23 13:50:42 -0800344 The serialized protocol buffer returned in an HTTP response will be
John Asmuth864311d2014-04-24 15:46:08 -0400345 de-serialized using the given protocol buffer class.
346
347 Args:
348 protocol_buffer: The protocol buffer class used to de-serialize a
349 response from the API.
350 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700351 self._protocol_buffer = protocol_buffer
John Asmuth864311d2014-04-24 15:46:08 -0400352
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700353 def serialize(self, body_value):
354 return body_value.SerializeToString()
John Asmuth864311d2014-04-24 15:46:08 -0400355
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700356 def deserialize(self, content):
357 return self._protocol_buffer.FromString(content)
John Asmuth864311d2014-04-24 15:46:08 -0400358
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700359 @property
360 def no_content_response(self):
361 return self._protocol_buffer()
John Asmuth864311d2014-04-24 15:46:08 -0400362
363
364def makepatch(original, modified):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700365 """Create a patch object.
John Asmuth864311d2014-04-24 15:46:08 -0400366
367 Some methods support PATCH, an efficient way to send updates to a resource.
368 This method allows the easy construction of patch bodies by looking at the
369 differences between a resource before and after it was modified.
370
371 Args:
372 original: object, the original deserialized resource
373 modified: object, the modified deserialized resource
374 Returns:
375 An object that contains only the changes from original to modified, in a
376 form suitable to pass to a PATCH method.
377
378 Example usage:
379 item = service.activities().get(postid=postid, userid=userid).execute()
380 original = copy.deepcopy(item)
381 item['object']['content'] = 'This is updated.'
382 service.activities.patch(postid=postid, userid=userid,
383 body=makepatch(original, item)).execute()
384 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700385 patch = {}
Anthonios Partheniou9f7b4102021-07-23 12:18:25 -0400386 for key, original_value in original.items():
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700387 modified_value = modified.get(key, None)
388 if modified_value is None:
389 # Use None to signal that the element is deleted
390 patch[key] = None
391 elif original_value != modified_value:
392 if type(original_value) == type({}):
393 # Recursively descend objects
394 patch[key] = makepatch(original_value, modified_value)
395 else:
396 # In the case of simple types or arrays we just replace
397 patch[key] = modified_value
398 else:
399 # Don't add anything to patch if there's no change
400 pass
401 for key in modified:
402 if key not in original:
403 patch[key] = modified[key]
John Asmuth864311d2014-04-24 15:46:08 -0400404
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700405 return patch