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__ = "[email protected] (Joe Gregorio)" 25 26import json 27import logging 28import platform 29import urllib 30 31from googleapiclient import version as googleapiclient_version 32from googleapiclient.errors import HttpError 33 34_LIBRARY_VERSION = googleapiclient_version.__version__ 35_PY_VERSION = platform.python_version() 36 37LOGGER = logging.getLogger(__name__) 38 39dump_request_response = False 40 41 42def _abstract(): 43 raise NotImplementedError("You need to override this function") 44 45 46class Model(object): 47 """Model base class. 48 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 54 def request(self, headers, path_params, query_params, body_value): 55 """Updates outgoing requests with a serialized body. 56 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 """ 71 _abstract() 72 73 def response(self, resp, content): 74 """Convert the response wire format into a Python object. 75 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 """ 86 _abstract() 87 88 89class BaseModel(Model): 90 """Base model class. 91 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 103 accept = None 104 content_type = None 105 no_content_response = None 106 alt_param = None 107 108 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-") 113 for h, v in headers.items(): 114 LOGGER.info("%s: %s", h, v) 115 LOGGER.info("-headers-end-") 116 LOGGER.info("-path-parameters-start-") 117 for h, v in path_params.items(): 118 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--") 123 124 def request(self, headers, path_params, query_params, body_value): 125 """Updates outgoing requests with a serialized body. 126 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 132 serializable by json. 133 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 """ 141 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" % ( 154 _LIBRARY_VERSION, 155 _PY_VERSION, 156 ) 157 158 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) 163 164 def _build_query(self, params): 165 """Builds a query string. 166 167 Args: 168 params: dict, the query parameters 169 170 Returns: 171 The query parameters properly encoded into an HTTP URI query string. 172 """ 173 if self.alt_param is not None: 174 params.update({"alt": self.alt_param}) 175 astuples = [] 176 for key, value in params.items(): 177 if type(value) == type([]): 178 for x in value: 179 x = x.encode("utf-8") 180 astuples.append((key, x)) 181 else: 182 if isinstance(value, str) and callable(value.encode): 183 value = value.encode("utf-8") 184 astuples.append((key, value)) 185 return "?" + urllib.parse.urlencode(astuples) 186 187 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--") 191 for h, v in resp.items(): 192 LOGGER.info("%s: %s", h, v) 193 if content: 194 LOGGER.info(content) 195 LOGGER.info("--response-end--") 196 197 def response(self, resp, content): 198 """Convert the response wire format into a Python object. 199 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 """ 210 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: 220 LOGGER.debug("Content from bad request was: %r" % content) 221 raise HttpError(resp, content) 222 223 def serialize(self, body_value): 224 """Perform the actual Python object serialization. 225 226 Args: 227 body_value: object, the request body as a Python object. 228 229 Returns: 230 string, the body in serialized form. 231 """ 232 _abstract() 233 234 def deserialize(self, content): 235 """Perform the actual deserialization from response string to Python 236 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 """ 244 _abstract() 245 246 247class JsonModel(BaseModel): 248 """Model class for JSON. 249 250 Serializes and de-serializes between JSON and the Python 251 object representation of HTTP request and response bodies. 252 """ 253 254 accept = "application/json" 255 content_type = "application/json" 256 alt_param = "json" 257 258 def __init__(self, data_wrapper=False): 259 """Construct a JsonModel. 260 261 Args: 262 data_wrapper: boolean, wrap requests and responses in a data wrapper 263 """ 264 self._data_wrapper = data_wrapper 265 266 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) 274 275 def deserialize(self, content): 276 try: 277 content = content.decode("utf-8") 278 except AttributeError: 279 pass 280 try: 281 body = json.loads(content) 282 except json.decoder.JSONDecodeError: 283 body = content 284 else: 285 if self._data_wrapper and "data" in body: 286 body = body["data"] 287 return body 288 289 @property 290 def no_content_response(self): 291 return {} 292 293 294class RawModel(JsonModel): 295 """Model class for requests that don't return JSON. 296 297 Serializes and de-serializes between JSON and the Python 298 object representation of HTTP request, and returns the raw bytes 299 of the response body. 300 """ 301 302 accept = "*/*" 303 content_type = "application/json" 304 alt_param = None 305 306 def deserialize(self, content): 307 return content 308 309 @property 310 def no_content_response(self): 311 return "" 312 313 314class MediaModel(JsonModel): 315 """Model class for requests that return Media. 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 = "media" 325 326 def deserialize(self, content): 327 return content 328 329 @property 330 def no_content_response(self): 331 return "" 332 333 334class ProtocolBufferModel(BaseModel): 335 """Model class for protocol buffers. 336 337 Serializes and de-serializes the binary protocol buffer sent in the HTTP 338 request and response bodies. 339 """ 340 341 accept = "application/x-protobuf" 342 content_type = "application/x-protobuf" 343 alt_param = "proto" 344 345 def __init__(self, protocol_buffer): 346 """Constructs a ProtocolBufferModel. 347 348 The serialized protocol buffer returned in an HTTP response will be 349 de-serialized using the given protocol buffer class. 350 351 Args: 352 protocol_buffer: The protocol buffer class used to de-serialize a 353 response from the API. 354 """ 355 self._protocol_buffer = protocol_buffer 356 357 def serialize(self, body_value): 358 return body_value.SerializeToString() 359 360 def deserialize(self, content): 361 return self._protocol_buffer.FromString(content) 362 363 @property 364 def no_content_response(self): 365 return self._protocol_buffer() 366 367 368def makepatch(original, modified): 369 """Create a patch object. 370 371 Some methods support PATCH, an efficient way to send updates to a resource. 372 This method allows the easy construction of patch bodies by looking at the 373 differences between a resource before and after it was modified. 374 375 Args: 376 original: object, the original deserialized resource 377 modified: object, the modified deserialized resource 378 Returns: 379 An object that contains only the changes from original to modified, in a 380 form suitable to pass to a PATCH method. 381 382 Example usage: 383 item = service.activities().get(postid=postid, userid=userid).execute() 384 original = copy.deepcopy(item) 385 item['object']['content'] = 'This is updated.' 386 service.activities.patch(postid=postid, userid=userid, 387 body=makepatch(original, item)).execute() 388 """ 389 patch = {} 390 for key, original_value in original.items(): 391 modified_value = modified.get(key, None) 392 if modified_value is None: 393 # Use None to signal that the element is deleted 394 patch[key] = None 395 elif original_value != modified_value: 396 if type(original_value) == type({}): 397 # Recursively descend objects 398 patch[key] = makepatch(original_value, modified_value) 399 else: 400 # In the case of simple types or arrays we just replace 401 patch[key] = modified_value 402 else: 403 # Don't add anything to patch if there's no change 404 pass 405 for key in modified: 406 if key not in original: 407 patch[key] = modified[key] 408 409 return patch 410