codekingpro/portable-devtools
114k
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 at6#7# http://www.apache.org/licenses/LICENSE-2.08#9# Unless required by applicable law or agreed to in writing, software10# 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 and13# limitations under the License.14 15"""Model objects for requests and responses.16 17Each API may support one or more serializations, such18as JSON, Atom, etc. The model classes are responsible19for converting between the wire format and the Python20object representation.21"""22from __future__ import absolute_import23 24__author__ = "jcgregorio@google.com (Joe Gregorio)"25 26import json27import logging28import platform29import urllib30 31from googleapiclient import version as googleapiclient_version32from googleapiclient.errors import HttpError33 34_LIBRARY_VERSION = googleapiclient_version.__version__35_PY_VERSION = platform.python_version()36 37LOGGER = logging.getLogger(__name__)38 39dump_request_response = False40 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 wire51 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 headers59 path_params: dict, parameters that appear in the request path60 query_params: dict, parameters that appear in the query61 body_value: object, the request body as a Python object, which must be62 serializable.63 Returns:64 A tuple of (headers, path_params, query, body)65 66 headers: dict, request headers67 path_params: dict, parameters that appear in the request path68 query: string, query part of the request URI69 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 status78 content: string, the body of the HTTP response79 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" and93 "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 "No99 Content" response.100 alt_param: The value to supply as the "alt" query parameter for requests.101 """102 103 accept = None104 content_type = None105 no_content_response = None106 alt_param = None107 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 headers129 path_params: dict, parameters that appear in the request path130 query_params: dict, parameters that appear in the query131 body_value: object, the request body as a Python object, which must be132 serializable by json.133 Returns:134 A tuple of (headers, path_params, query, body)135 136 headers: dict, request headers137 path_params: dict, parameters that appear in the request path138 query: string, query part of the request URI139 body: string, the body serialized as JSON140 """141 query = self._build_query(query_params)142 headers["accept"] = self.accept143 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_type160 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 parameters169 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 status202 content: string, the body of the HTTP response203 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 retry212 # for some operation/error combinations?213 if resp.status < 300:214 if resp.status == 204:215 # A 204: No Content response should be treated differently216 # to all the other success states217 return self.no_content_response218 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 Python236 object.237 238 Args:239 content: string, the body of the HTTP response240 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 Python251 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 wrapper263 """264 self._data_wrapper = data_wrapper265 266 def serialize(self, body_value):267 if (268 isinstance(body_value, dict)269 and "data" not in body_value270 and self._data_wrapper271 ):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 pass280 try:281 body = json.loads(content)282 except json.decoder.JSONDecodeError:283 body = content284 else:285 if self._data_wrapper and "data" in body:286 body = body["data"]287 return body288 289 @property290 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 Python298 object representation of HTTP request, and returns the raw bytes299 of the response body.300 """301 302 accept = "*/*"303 content_type = "application/json"304 alt_param = None305 306 def deserialize(self, content):307 return content308 309 @property310 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 Python318 object representation of HTTP request, and returns the raw bytes319 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 content328 329 @property330 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 HTTP338 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 be349 de-serialized using the given protocol buffer class.350 351 Args:352 protocol_buffer: The protocol buffer class used to de-serialize a353 response from the API.354 """355 self._protocol_buffer = protocol_buffer356 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 @property364 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 the373 differences between a resource before and after it was modified.374 375 Args:376 original: object, the original deserialized resource377 modified: object, the modified deserialized resource378 Returns:379 An object that contains only the changes from original to modified, in a380 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 deleted394 patch[key] = None395 elif original_value != modified_value:396 if type(original_value) == type({}):397 # Recursively descend objects398 patch[key] = makepatch(original_value, modified_value)399 else:400 # In the case of simple types or arrays we just replace401 patch[key] = modified_value402 else:403 # Don't add anything to patch if there's no change404 pass405 for key in modified:406 if key not in original:407 patch[key] = modified[key]408 409 return patch410 