blob: e8afb63d6c52e9ddbb30151428f66cc24833cfd9 [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
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-')
INADA Naokie4ea1a92015-03-04 03:45:42 +0900110 for h, v in six.iteritems(headers):
John Asmuth864311d2014-04-24 15:46:08 -0400111 logging.info('%s: %s', h, v)
112 logging.info('-headers-end-')
113 logging.info('-path-parameters-start-')
INADA Naokie4ea1a92015-03-04 03:45:42 +0900114 for h, v in six.iteritems(path_params):
John Asmuth864311d2014-04-24 15:46:08 -0400115 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
Craig Citro6ae34d72014-08-18 23:10:09 -0700129 serializable by json.
John Asmuth864311d2014-04-24 15:46:08 -0400130 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 = []
INADA Naokie4ea1a92015-03-04 03:45:42 +0900165 for key, value in six.iteritems(params):
John Asmuth864311d2014-04-24 15:46:08 -0400166 if type(value) == type([]):
167 for x in value:
168 x = x.encode('utf-8')
169 astuples.append((key, x))
170 else:
INADA Naokie4ea1a92015-03-04 03:45:42 +0900171 if isinstance(value, six.text_type) and callable(value.encode):
John Asmuth864311d2014-04-24 15:46:08 -0400172 value = value.encode('utf-8')
173 astuples.append((key, value))
Pat Ferated5b61bd2015-03-03 16:04:11 -0800174 return '?' + urlencode(astuples)
John Asmuth864311d2014-04-24 15:46:08 -0400175
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--')
INADA Naokie4ea1a92015-03-04 03:45:42 +0900180 for h, v in six.iteritems(resp):
John Asmuth864311d2014-04-24 15:46:08 -0400181 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}
Craig Citro6ae34d72014-08-18 23:10:09 -0700258 return json.dumps(body_value)
John Asmuth864311d2014-04-24 15:46:08 -0400259
260 def deserialize(self, content):
Pat Ferate9b0452c2015-03-03 17:59:56 -0800261 try:
262 content = content.decode('utf-8')
263 except AttributeError:
264 pass
Craig Citro6ae34d72014-08-18 23:10:09 -0700265 body = json.loads(content)
John Asmuth864311d2014-04-24 15:46:08 -0400266 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 = {}
INADA Naokie4ea1a92015-03-04 03:45:42 +0900368 for key, original_value in six.iteritems(original):
John Asmuth864311d2014-04-24 15:46:08 -0400369 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