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