blob: 2402d84dfaf832e18dc6694d9c7c14a734dc28aa [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__
INADA Naoki0bceb332014-08-20 15:27:52 +090033from .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):
261 content = content.decode('utf-8')
Craig Citro6ae34d72014-08-18 23:10:09 -0700262 body = json.loads(content)
John Asmuth864311d2014-04-24 15:46:08 -0400263 if self._data_wrapper and isinstance(body, dict) and 'data' in body:
264 body = body['data']
265 return body
266
267 @property
268 def no_content_response(self):
269 return {}
270
271
272class RawModel(JsonModel):
273 """Model class for requests that don't return JSON.
274
275 Serializes and de-serializes between JSON and the Python
276 object representation of HTTP request, and returns the raw bytes
277 of the response body.
278 """
279 accept = '*/*'
280 content_type = 'application/json'
281 alt_param = None
282
283 def deserialize(self, content):
284 return content
285
286 @property
287 def no_content_response(self):
288 return ''
289
290
291class MediaModel(JsonModel):
292 """Model class for requests that return Media.
293
294 Serializes and de-serializes between JSON and the Python
295 object representation of HTTP request, and returns the raw bytes
296 of the response body.
297 """
298 accept = '*/*'
299 content_type = 'application/json'
300 alt_param = 'media'
301
302 def deserialize(self, content):
303 return content
304
305 @property
306 def no_content_response(self):
307 return ''
308
309
310class ProtocolBufferModel(BaseModel):
311 """Model class for protocol buffers.
312
313 Serializes and de-serializes the binary protocol buffer sent in the HTTP
314 request and response bodies.
315 """
316 accept = 'application/x-protobuf'
317 content_type = 'application/x-protobuf'
318 alt_param = 'proto'
319
320 def __init__(self, protocol_buffer):
321 """Constructs a ProtocolBufferModel.
322
323 The serialzed protocol buffer returned in an HTTP response will be
324 de-serialized using the given protocol buffer class.
325
326 Args:
327 protocol_buffer: The protocol buffer class used to de-serialize a
328 response from the API.
329 """
330 self._protocol_buffer = protocol_buffer
331
332 def serialize(self, body_value):
333 return body_value.SerializeToString()
334
335 def deserialize(self, content):
336 return self._protocol_buffer.FromString(content)
337
338 @property
339 def no_content_response(self):
340 return self._protocol_buffer()
341
342
343def makepatch(original, modified):
344 """Create a patch object.
345
346 Some methods support PATCH, an efficient way to send updates to a resource.
347 This method allows the easy construction of patch bodies by looking at the
348 differences between a resource before and after it was modified.
349
350 Args:
351 original: object, the original deserialized resource
352 modified: object, the modified deserialized resource
353 Returns:
354 An object that contains only the changes from original to modified, in a
355 form suitable to pass to a PATCH method.
356
357 Example usage:
358 item = service.activities().get(postid=postid, userid=userid).execute()
359 original = copy.deepcopy(item)
360 item['object']['content'] = 'This is updated.'
361 service.activities.patch(postid=postid, userid=userid,
362 body=makepatch(original, item)).execute()
363 """
364 patch = {}
INADA Naokie4ea1a92015-03-04 03:45:42 +0900365 for key, original_value in six.iteritems(original):
John Asmuth864311d2014-04-24 15:46:08 -0400366 modified_value = modified.get(key, None)
367 if modified_value is None:
368 # Use None to signal that the element is deleted
369 patch[key] = None
370 elif original_value != modified_value:
371 if type(original_value) == type({}):
372 # Recursively descend objects
373 patch[key] = makepatch(original_value, modified_value)
374 else:
375 # In the case of simple types or arrays we just replace
376 patch[key] = modified_value
377 else:
378 # Don't add anything to patch if there's no change
379 pass
380 for key in modified:
381 if key not in original:
382 patch[key] = modified[key]
383
384 return patch