blob: 9881a15c04ec9d9703bbfc3f6b8dd9a7d9f62f0c [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 Gregorioa314b1f2013-04-05 16:25:11 -040030from apiclient import __version__
Joe Gregoriob843fa22010-12-13 16:26:07 -050031from errors import HttpError
Joe Gregorio549230c2012-01-11 10:38:05 -050032from oauth2client.anyjson import simplejson
Joe Gregorio3ad5e9a2010-12-09 15:01:04 -050033
Joe Gregorio34044bc2011-03-07 16:58:33 -050034
Joe Gregorio79daca02013-03-29 16:25:52 -040035dump_request_response = False
Joe Gregoriodeeb0202011-02-15 14:49:57 -050036
Joe Gregorioafdf50b2011-03-08 09:41:52 -050037
Joe Gregorioabda96f2011-02-11 20:19:33 -050038def _abstract():
39 raise NotImplementedError('You need to override this function')
Joe Gregorio3ad5e9a2010-12-09 15:01:04 -050040
Joe Gregorioabda96f2011-02-11 20:19:33 -050041
42class Model(object):
43 """Model base class.
44
45 All Model classes should implement this interface.
46 The Model serializes and de-serializes between a wire
47 format such as JSON and a Python object representation.
48 """
49
50 def request(self, headers, path_params, query_params, body_value):
Matt McDonald2a5f4132011-04-29 16:32:27 -040051 """Updates outgoing requests with a serialized body.
Joe Gregorioabda96f2011-02-11 20:19:33 -050052
53 Args:
54 headers: dict, request headers
55 path_params: dict, parameters that appear in the request path
56 query_params: dict, parameters that appear in the query
57 body_value: object, the request body as a Python object, which must be
58 serializable.
59 Returns:
60 A tuple of (headers, path_params, query, body)
61
62 headers: dict, request headers
63 path_params: dict, parameters that appear in the request path
64 query: string, query part of the request URI
65 body: string, the body serialized in the desired wire format.
66 """
67 _abstract()
68
69 def response(self, resp, content):
70 """Convert the response wire format into a Python object.
71
72 Args:
73 resp: httplib2.Response, the HTTP response headers and status
74 content: string, the body of the HTTP response
75
76 Returns:
77 The body de-serialized as a Python object.
78
79 Raises:
80 apiclient.errors.HttpError if a non 2xx response is received.
81 """
82 _abstract()
83
84
Matt McDonald2a5f4132011-04-29 16:32:27 -040085class BaseModel(Model):
86 """Base model class.
Joe Gregorio3ad5e9a2010-12-09 15:01:04 -050087
Matt McDonald2a5f4132011-04-29 16:32:27 -040088 Subclasses should provide implementations for the "serialize" and
89 "deserialize" methods, as well as values for the following class attributes.
90
91 Attributes:
92 accept: The value to use for the HTTP Accept header.
93 content_type: The value to use for the HTTP Content-type header.
94 no_content_response: The value to return when deserializing a 204 "No
95 Content" response.
96 alt_param: The value to supply as the "alt" query parameter for requests.
Joe Gregorio3ad5e9a2010-12-09 15:01:04 -050097 """
98
Matt McDonald2a5f4132011-04-29 16:32:27 -040099 accept = None
100 content_type = None
101 no_content_response = None
102 alt_param = None
Joe Gregoriod433b2a2011-02-22 10:51:51 -0500103
Matt McDonald2a5f4132011-04-29 16:32:27 -0400104 def _log_request(self, headers, path_params, query, body):
105 """Logs debugging information about the request if requested."""
Joe Gregorio79daca02013-03-29 16:25:52 -0400106 if dump_request_response:
Matt McDonald2a5f4132011-04-29 16:32:27 -0400107 logging.info('--request-start--')
108 logging.info('-headers-start-')
109 for h, v in headers.iteritems():
110 logging.info('%s: %s', h, v)
111 logging.info('-headers-end-')
112 logging.info('-path-parameters-start-')
113 for h, v in path_params.iteritems():
114 logging.info('%s: %s', h, v)
115 logging.info('-path-parameters-end-')
116 logging.info('body: %s', body)
117 logging.info('query: %s', query)
118 logging.info('--request-end--')
Joe Gregoriod433b2a2011-02-22 10:51:51 -0500119
Joe Gregorio3ad5e9a2010-12-09 15:01:04 -0500120 def request(self, headers, path_params, query_params, body_value):
Matt McDonald2a5f4132011-04-29 16:32:27 -0400121 """Updates outgoing requests with a serialized body.
Joe Gregorio3ad5e9a2010-12-09 15:01:04 -0500122
123 Args:
124 headers: dict, request headers
125 path_params: dict, parameters that appear in the request path
126 query_params: dict, parameters that appear in the query
127 body_value: object, the request body as a Python object, which must be
128 serializable by simplejson.
129 Returns:
130 A tuple of (headers, path_params, query, body)
131
132 headers: dict, request headers
133 path_params: dict, parameters that appear in the request path
134 query: string, query part of the request URI
135 body: string, the body serialized as JSON
136 """
137 query = self._build_query(query_params)
Matt McDonald2a5f4132011-04-29 16:32:27 -0400138 headers['accept'] = self.accept
Joe Gregorio6429bf62011-03-01 22:53:21 -0800139 headers['accept-encoding'] = 'gzip, deflate'
Joe Gregorio3ad5e9a2010-12-09 15:01:04 -0500140 if 'user-agent' in headers:
141 headers['user-agent'] += ' '
142 else:
143 headers['user-agent'] = ''
Joe Gregorioc02f5632013-05-13 11:28:56 -0400144 headers['user-agent'] += 'google-api-python-client/%s (gzip)' % __version__
Joe Gregoriod433b2a2011-02-22 10:51:51 -0500145
Joe Gregorioafdf50b2011-03-08 09:41:52 -0500146 if body_value is not None:
Matt McDonald2a5f4132011-04-29 16:32:27 -0400147 headers['content-type'] = self.content_type
148 body_value = self.serialize(body_value)
149 self._log_request(headers, path_params, query, body_value)
Joe Gregorioafdf50b2011-03-08 09:41:52 -0500150 return (headers, path_params, query, body_value)
Joe Gregorio3ad5e9a2010-12-09 15:01:04 -0500151
152 def _build_query(self, params):
153 """Builds a query string.
154
155 Args:
156 params: dict, the query parameters
157
158 Returns:
159 The query parameters properly encoded into an HTTP URI query string.
160 """
Joe Gregorioe08a1662011-12-07 09:48:22 -0500161 if self.alt_param is not None:
162 params.update({'alt': self.alt_param})
Joe Gregorio3ad5e9a2010-12-09 15:01:04 -0500163 astuples = []
164 for key, value in params.iteritems():
Joe Gregorio61d7e962011-02-22 22:52:07 -0500165 if type(value) == type([]):
166 for x in value:
167 x = x.encode('utf-8')
168 astuples.append((key, x))
169 else:
170 if getattr(value, 'encode', False) and callable(value.encode):
171 value = value.encode('utf-8')
172 astuples.append((key, value))
Joe Gregorio3ad5e9a2010-12-09 15:01:04 -0500173 return '?' + urllib.urlencode(astuples)
174
Matt McDonald2a5f4132011-04-29 16:32:27 -0400175 def _log_response(self, resp, content):
176 """Logs debugging information about the response if requested."""
Joe Gregorio79daca02013-03-29 16:25:52 -0400177 if dump_request_response:
Matt McDonald2a5f4132011-04-29 16:32:27 -0400178 logging.info('--response-start--')
179 for h, v in resp.iteritems():
180 logging.info('%s: %s', h, v)
181 if content:
182 logging.info(content)
183 logging.info('--response-end--')
184
Joe Gregorio3ad5e9a2010-12-09 15:01:04 -0500185 def response(self, resp, content):
186 """Convert the response wire format into a Python object.
187
188 Args:
189 resp: httplib2.Response, the HTTP response headers and status
190 content: string, the body of the HTTP response
191
192 Returns:
193 The body de-serialized as a Python object.
194
195 Raises:
196 apiclient.errors.HttpError if a non 2xx response is received.
197 """
Joe Gregorio79daca02013-03-29 16:25:52 -0400198 content = content.decode('utf-8')
Matt McDonald2a5f4132011-04-29 16:32:27 -0400199 self._log_response(resp, content)
Joe Gregorio3ad5e9a2010-12-09 15:01:04 -0500200 # 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
Matt McDonald2a5f4132011-04-29 16:32:27 -0400206 return self.no_content_response
207 return self.deserialize(content)
Joe Gregorio3ad5e9a2010-12-09 15:01:04 -0500208 else:
209 logging.debug('Content from bad request was: %s' % content)
Ali Afshar2dcc6522010-12-16 10:11:53 +0100210 raise HttpError(resp, content)
Joe Gregorio34044bc2011-03-07 16:58:33 -0500211
Matt McDonald2a5f4132011-04-29 16:32:27 -0400212 def serialize(self, body_value):
213 """Perform the actual Python object serialization.
Joe Gregorio34044bc2011-03-07 16:58:33 -0500214
215 Args:
Matt McDonald2a5f4132011-04-29 16:32:27 -0400216 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):
Joe Gregorio562b7312011-09-15 09:06:38 -0400224 """Perform the actual deserialization from response string to Python
225 object.
Matt McDonald2a5f4132011-04-29 16:32:27 -0400226
227 Args:
228 content: string, the body of the HTTP response
Joe Gregorio34044bc2011-03-07 16:58:33 -0500229
230 Returns:
231 The body de-serialized as a Python object.
232 """
Matt McDonald2a5f4132011-04-29 16:32:27 -0400233 _abstract()
Joe Gregorioafdf50b2011-03-08 09:41:52 -0500234
Matt McDonald2a5f4132011-04-29 16:32:27 -0400235
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.
Joe Gregorioafdf50b2011-03-08 09:41:52 -0500248
249 Args:
Matt McDonald2a5f4132011-04-29 16:32:27 -0400250 data_wrapper: boolean, wrap requests and responses in a data wrapper
Joe Gregorioafdf50b2011-03-08 09:41:52 -0500251 """
Matt McDonald2a5f4132011-04-29 16:32:27 -0400252 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}
258 return simplejson.dumps(body_value)
259
260 def deserialize(self, content):
261 body = simplejson.loads(content)
Ali Afshar81fde8e2012-10-23 11:14:28 -0700262 if self._data_wrapper and isinstance(body, dict) and 'data' in body:
Matt McDonald2a5f4132011-04-29 16:32:27 -0400263 body = body['data']
264 return body
265
266 @property
267 def no_content_response(self):
268 return {}
269
270
Joe Gregorioe08a1662011-12-07 09:48:22 -0500271class RawModel(JsonModel):
272 """Model class for requests that don't return JSON.
273
274 Serializes and de-serializes between JSON and the Python
275 object representation of HTTP request, and returns the raw bytes
276 of the response body.
277 """
278 accept = '*/*'
279 content_type = 'application/json'
280 alt_param = None
281
282 def deserialize(self, content):
283 return content
284
285 @property
286 def no_content_response(self):
287 return ''
288
289
Joe Gregorio708388c2012-06-15 13:43:04 -0400290class MediaModel(JsonModel):
291 """Model class for requests that return Media.
292
293 Serializes and de-serializes between JSON and the Python
294 object representation of HTTP request, and returns the raw bytes
295 of the response body.
296 """
297 accept = '*/*'
298 content_type = 'application/json'
299 alt_param = 'media'
300
301 def deserialize(self, content):
302 return content
303
304 @property
305 def no_content_response(self):
306 return ''
307
308
Matt McDonald2a5f4132011-04-29 16:32:27 -0400309class ProtocolBufferModel(BaseModel):
310 """Model class for protocol buffers.
311
312 Serializes and de-serializes the binary protocol buffer sent in the HTTP
313 request and response bodies.
314 """
315 accept = 'application/x-protobuf'
316 content_type = 'application/x-protobuf'
317 alt_param = 'proto'
318
319 def __init__(self, protocol_buffer):
320 """Constructs a ProtocolBufferModel.
321
322 The serialzed protocol buffer returned in an HTTP response will be
323 de-serialized using the given protocol buffer class.
324
325 Args:
Joe Gregorio562b7312011-09-15 09:06:38 -0400326 protocol_buffer: The protocol buffer class used to de-serialize a
327 response from the API.
Matt McDonald2a5f4132011-04-29 16:32:27 -0400328 """
329 self._protocol_buffer = protocol_buffer
330
331 def serialize(self, body_value):
332 return body_value.SerializeToString()
333
334 def deserialize(self, content):
335 return self._protocol_buffer.FromString(content)
336
337 @property
338 def no_content_response(self):
339 return self._protocol_buffer()
Joe Gregorioe98c2322011-05-26 15:40:48 -0400340
341
342def makepatch(original, modified):
343 """Create a patch object.
344
345 Some methods support PATCH, an efficient way to send updates to a resource.
346 This method allows the easy construction of patch bodies by looking at the
347 differences between a resource before and after it was modified.
348
349 Args:
350 original: object, the original deserialized resource
351 modified: object, the modified deserialized resource
352 Returns:
353 An object that contains only the changes from original to modified, in a
354 form suitable to pass to a PATCH method.
355
356 Example usage:
357 item = service.activities().get(postid=postid, userid=userid).execute()
358 original = copy.deepcopy(item)
359 item['object']['content'] = 'This is updated.'
360 service.activities.patch(postid=postid, userid=userid,
361 body=makepatch(original, item)).execute()
362 """
363 patch = {}
364 for key, original_value in original.iteritems():
365 modified_value = modified.get(key, None)
366 if modified_value is None:
367 # Use None to signal that the element is deleted
368 patch[key] = None
369 elif original_value != modified_value:
370 if type(original_value) == type({}):
371 # Recursively descend objects
372 patch[key] = makepatch(original_value, modified_value)
373 else:
374 # In the case of simple types or arrays we just replace
375 patch[key] = modified_value
376 else:
377 # Don't add anything to patch if there's no change
378 pass
379 for key in modified:
380 if key not in original:
381 patch[key] = modified[key]
382
383 return patch