blob: ef91bbda4c7daf4c8cd97e8442182b3a61f0cf6b [file] [log] [blame]
Joe Gregorio3ad5e9a2010-12-09 15:01:04 -05001#!/usr/bin/python2.4
2#
Joe Gregorio20a5aa92011-04-01 17:44:25 -04003# Copyright (C) 2010 Google Inc.
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
Joe Gregorio3ad5e9a2010-12-09 15:01:04 -050016
Joe Gregorioec343652011-02-16 16:52:51 -050017"""Model objects for requests and responses.
Joe Gregorio3ad5e9a2010-12-09 15:01:04 -050018
19Each API may support one or more serializations, such
20as JSON, Atom, etc. The model classes are responsible
21for converting between the wire format and the Python
22object representation.
23"""
24
25__author__ = 'jcgregorio@google.com (Joe Gregorio)'
26
27import logging
28import urllib
29
Joe Gregoriob843fa22010-12-13 16:26:07 -050030from errors import HttpError
Joe Gregorio549230c2012-01-11 10:38:05 -050031from oauth2client.anyjson import simplejson
Joe Gregorio3ad5e9a2010-12-09 15:01:04 -050032
Joe Gregorio34044bc2011-03-07 16:58:33 -050033
Joe Gregorio79daca02013-03-29 16:25:52 -040034dump_request_response = False
Joe Gregoriodeeb0202011-02-15 14:49:57 -050035
Joe Gregorioafdf50b2011-03-08 09:41:52 -050036
Joe Gregorioabda96f2011-02-11 20:19:33 -050037def _abstract():
38 raise NotImplementedError('You need to override this function')
Joe Gregorio3ad5e9a2010-12-09 15:01:04 -050039
Joe Gregorioabda96f2011-02-11 20:19:33 -050040
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):
Matt McDonald2a5f4132011-04-29 16:32:27 -040050 """Updates outgoing requests with a serialized body.
Joe Gregorioabda96f2011-02-11 20:19:33 -050051
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 apiclient.errors.HttpError if a non 2xx response is received.
80 """
81 _abstract()
82
83
Matt McDonald2a5f4132011-04-29 16:32:27 -040084class BaseModel(Model):
85 """Base model class.
Joe Gregorio3ad5e9a2010-12-09 15:01:04 -050086
Matt McDonald2a5f4132011-04-29 16:32:27 -040087 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.
Joe Gregorio3ad5e9a2010-12-09 15:01:04 -050096 """
97
Matt McDonald2a5f4132011-04-29 16:32:27 -040098 accept = None
99 content_type = None
100 no_content_response = None
101 alt_param = None
Joe Gregoriod433b2a2011-02-22 10:51:51 -0500102
Matt McDonald2a5f4132011-04-29 16:32:27 -0400103 def _log_request(self, headers, path_params, query, body):
104 """Logs debugging information about the request if requested."""
Joe Gregorio79daca02013-03-29 16:25:52 -0400105 if dump_request_response:
Matt McDonald2a5f4132011-04-29 16:32:27 -0400106 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--')
Joe Gregoriod433b2a2011-02-22 10:51:51 -0500118
Joe Gregorio3ad5e9a2010-12-09 15:01:04 -0500119 def request(self, headers, path_params, query_params, body_value):
Matt McDonald2a5f4132011-04-29 16:32:27 -0400120 """Updates outgoing requests with a serialized body.
Joe Gregorio3ad5e9a2010-12-09 15:01:04 -0500121
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
127 serializable by simplejson.
128 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)
Matt McDonald2a5f4132011-04-29 16:32:27 -0400137 headers['accept'] = self.accept
Joe Gregorio6429bf62011-03-01 22:53:21 -0800138 headers['accept-encoding'] = 'gzip, deflate'
Joe Gregorio3ad5e9a2010-12-09 15:01:04 -0500139 if 'user-agent' in headers:
140 headers['user-agent'] += ' '
141 else:
142 headers['user-agent'] = ''
143 headers['user-agent'] += 'google-api-python-client/1.0'
Joe Gregoriod433b2a2011-02-22 10:51:51 -0500144
Joe Gregorioafdf50b2011-03-08 09:41:52 -0500145 if body_value is not None:
Matt McDonald2a5f4132011-04-29 16:32:27 -0400146 headers['content-type'] = self.content_type
147 body_value = self.serialize(body_value)
148 self._log_request(headers, path_params, query, body_value)
Joe Gregorioafdf50b2011-03-08 09:41:52 -0500149 return (headers, path_params, query, body_value)
Joe Gregorio3ad5e9a2010-12-09 15:01:04 -0500150
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 """
Joe Gregorioe08a1662011-12-07 09:48:22 -0500160 if self.alt_param is not None:
161 params.update({'alt': self.alt_param})
Joe Gregorio3ad5e9a2010-12-09 15:01:04 -0500162 astuples = []
163 for key, value in params.iteritems():
Joe Gregorio61d7e962011-02-22 22:52:07 -0500164 if type(value) == type([]):
165 for x in value:
166 x = x.encode('utf-8')
167 astuples.append((key, x))
168 else:
169 if getattr(value, 'encode', False) and callable(value.encode):
170 value = value.encode('utf-8')
171 astuples.append((key, value))
Joe Gregorio3ad5e9a2010-12-09 15:01:04 -0500172 return '?' + urllib.urlencode(astuples)
173
Matt McDonald2a5f4132011-04-29 16:32:27 -0400174 def _log_response(self, resp, content):
175 """Logs debugging information about the response if requested."""
Joe Gregorio79daca02013-03-29 16:25:52 -0400176 if dump_request_response:
Matt McDonald2a5f4132011-04-29 16:32:27 -0400177 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
Joe Gregorio3ad5e9a2010-12-09 15:01:04 -0500184 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 apiclient.errors.HttpError if a non 2xx response is received.
196 """
Joe Gregorio79daca02013-03-29 16:25:52 -0400197 content = content.decode('utf-8')
Matt McDonald2a5f4132011-04-29 16:32:27 -0400198 self._log_response(resp, content)
Joe Gregorio3ad5e9a2010-12-09 15:01:04 -0500199 # Error handling is TBD, for example, do we retry
200 # for some operation/error combinations?
201 if resp.status < 300:
202 if resp.status == 204:
203 # A 204: No Content response should be treated differently
204 # to all the other success states
Matt McDonald2a5f4132011-04-29 16:32:27 -0400205 return self.no_content_response
206 return self.deserialize(content)
Joe Gregorio3ad5e9a2010-12-09 15:01:04 -0500207 else:
208 logging.debug('Content from bad request was: %s' % content)
Ali Afshar2dcc6522010-12-16 10:11:53 +0100209 raise HttpError(resp, content)
Joe Gregorio34044bc2011-03-07 16:58:33 -0500210
Matt McDonald2a5f4132011-04-29 16:32:27 -0400211 def serialize(self, body_value):
212 """Perform the actual Python object serialization.
Joe Gregorio34044bc2011-03-07 16:58:33 -0500213
214 Args:
Matt McDonald2a5f4132011-04-29 16:32:27 -0400215 body_value: object, the request body as a Python object.
216
217 Returns:
218 string, the body in serialized form.
219 """
220 _abstract()
221
222 def deserialize(self, content):
Joe Gregorio562b7312011-09-15 09:06:38 -0400223 """Perform the actual deserialization from response string to Python
224 object.
Matt McDonald2a5f4132011-04-29 16:32:27 -0400225
226 Args:
227 content: string, the body of the HTTP response
Joe Gregorio34044bc2011-03-07 16:58:33 -0500228
229 Returns:
230 The body de-serialized as a Python object.
231 """
Matt McDonald2a5f4132011-04-29 16:32:27 -0400232 _abstract()
Joe Gregorioafdf50b2011-03-08 09:41:52 -0500233
Matt McDonald2a5f4132011-04-29 16:32:27 -0400234
235class JsonModel(BaseModel):
236 """Model class for JSON.
237
238 Serializes and de-serializes between JSON and the Python
239 object representation of HTTP request and response bodies.
240 """
241 accept = 'application/json'
242 content_type = 'application/json'
243 alt_param = 'json'
244
245 def __init__(self, data_wrapper=False):
246 """Construct a JsonModel.
Joe Gregorioafdf50b2011-03-08 09:41:52 -0500247
248 Args:
Matt McDonald2a5f4132011-04-29 16:32:27 -0400249 data_wrapper: boolean, wrap requests and responses in a data wrapper
Joe Gregorioafdf50b2011-03-08 09:41:52 -0500250 """
Matt McDonald2a5f4132011-04-29 16:32:27 -0400251 self._data_wrapper = data_wrapper
252
253 def serialize(self, body_value):
254 if (isinstance(body_value, dict) and 'data' not in body_value and
255 self._data_wrapper):
256 body_value = {'data': body_value}
257 return simplejson.dumps(body_value)
258
259 def deserialize(self, content):
260 body = simplejson.loads(content)
Ali Afshar81fde8e2012-10-23 11:14:28 -0700261 if self._data_wrapper and isinstance(body, dict) and 'data' in body:
Matt McDonald2a5f4132011-04-29 16:32:27 -0400262 body = body['data']
263 return body
264
265 @property
266 def no_content_response(self):
267 return {}
268
269
Joe Gregorioe08a1662011-12-07 09:48:22 -0500270class 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
Joe Gregorio708388c2012-06-15 13:43:04 -0400289class 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
Matt McDonald2a5f4132011-04-29 16:32:27 -0400308class 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:
Joe Gregorio562b7312011-09-15 09:06:38 -0400325 protocol_buffer: The protocol buffer class used to de-serialize a
326 response from the API.
Matt McDonald2a5f4132011-04-29 16:32:27 -0400327 """
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()
Joe Gregorioe98c2322011-05-26 15:40:48 -0400339
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