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