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