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