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