blob: b8271f99b470ad0a658921fd1025349689a7ed23 [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
Joe Gregorio34044bc2011-03-07 16:58:33 -050027import gflags
Joe Gregorio3ad5e9a2010-12-09 15:01:04 -050028import logging
29import urllib
30
Joe Gregoriob843fa22010-12-13 16:26:07 -050031from anyjson import simplejson
32from errors import HttpError
Joe Gregorio3ad5e9a2010-12-09 15:01:04 -050033
Joe Gregorio34044bc2011-03-07 16:58:33 -050034FLAGS = gflags.FLAGS
35
Joe Gregorioafdf50b2011-03-08 09:41:52 -050036gflags.DEFINE_boolean('dump_request_response', False,
Matt McDonald2a5f4132011-04-29 16:32:27 -040037 'Dump all http server requests and responses. '
Joe Gregorio205e73a2011-03-12 09:55:31 -050038 )
Joe Gregoriodeeb0202011-02-15 14:49:57 -050039
Joe Gregorioafdf50b2011-03-08 09:41:52 -050040
Joe Gregorioabda96f2011-02-11 20:19:33 -050041def _abstract():
42 raise NotImplementedError('You need to override this function')
Joe Gregorio3ad5e9a2010-12-09 15:01:04 -050043
Joe Gregorioabda96f2011-02-11 20:19:33 -050044
45class Model(object):
46 """Model base class.
47
48 All Model classes should implement this interface.
49 The Model serializes and de-serializes between a wire
50 format such as JSON and a Python object representation.
51 """
52
53 def request(self, headers, path_params, query_params, body_value):
Matt McDonald2a5f4132011-04-29 16:32:27 -040054 """Updates outgoing requests with a serialized body.
Joe Gregorioabda96f2011-02-11 20:19:33 -050055
56 Args:
57 headers: dict, request headers
58 path_params: dict, parameters that appear in the request path
59 query_params: dict, parameters that appear in the query
60 body_value: object, the request body as a Python object, which must be
61 serializable.
62 Returns:
63 A tuple of (headers, path_params, query, body)
64
65 headers: dict, request headers
66 path_params: dict, parameters that appear in the request path
67 query: string, query part of the request URI
68 body: string, the body serialized in the desired wire format.
69 """
70 _abstract()
71
72 def response(self, resp, content):
73 """Convert the response wire format into a Python object.
74
75 Args:
76 resp: httplib2.Response, the HTTP response headers and status
77 content: string, the body of the HTTP response
78
79 Returns:
80 The body de-serialized as a Python object.
81
82 Raises:
83 apiclient.errors.HttpError if a non 2xx response is received.
84 """
85 _abstract()
86
87
Matt McDonald2a5f4132011-04-29 16:32:27 -040088class BaseModel(Model):
89 """Base model class.
Joe Gregorio3ad5e9a2010-12-09 15:01:04 -050090
Matt McDonald2a5f4132011-04-29 16:32:27 -040091 Subclasses should provide implementations for the "serialize" and
92 "deserialize" methods, as well as values for the following class attributes.
93
94 Attributes:
95 accept: The value to use for the HTTP Accept header.
96 content_type: The value to use for the HTTP Content-type header.
97 no_content_response: The value to return when deserializing a 204 "No
98 Content" response.
99 alt_param: The value to supply as the "alt" query parameter for requests.
Joe Gregorio3ad5e9a2010-12-09 15:01:04 -0500100 """
101
Matt McDonald2a5f4132011-04-29 16:32:27 -0400102 accept = None
103 content_type = None
104 no_content_response = None
105 alt_param = None
Joe Gregoriod433b2a2011-02-22 10:51:51 -0500106
Matt McDonald2a5f4132011-04-29 16:32:27 -0400107 def _log_request(self, headers, path_params, query, body):
108 """Logs debugging information about the request if requested."""
109 if FLAGS.dump_request_response:
110 logging.info('--request-start--')
111 logging.info('-headers-start-')
112 for h, v in headers.iteritems():
113 logging.info('%s: %s', h, v)
114 logging.info('-headers-end-')
115 logging.info('-path-parameters-start-')
116 for h, v in path_params.iteritems():
117 logging.info('%s: %s', h, v)
118 logging.info('-path-parameters-end-')
119 logging.info('body: %s', body)
120 logging.info('query: %s', query)
121 logging.info('--request-end--')
Joe Gregoriod433b2a2011-02-22 10:51:51 -0500122
Joe Gregorio3ad5e9a2010-12-09 15:01:04 -0500123 def request(self, headers, path_params, query_params, body_value):
Matt McDonald2a5f4132011-04-29 16:32:27 -0400124 """Updates outgoing requests with a serialized body.
Joe Gregorio3ad5e9a2010-12-09 15:01:04 -0500125
126 Args:
127 headers: dict, request headers
128 path_params: dict, parameters that appear in the request path
129 query_params: dict, parameters that appear in the query
130 body_value: object, the request body as a Python object, which must be
131 serializable by simplejson.
132 Returns:
133 A tuple of (headers, path_params, query, body)
134
135 headers: dict, request headers
136 path_params: dict, parameters that appear in the request path
137 query: string, query part of the request URI
138 body: string, the body serialized as JSON
139 """
140 query = self._build_query(query_params)
Matt McDonald2a5f4132011-04-29 16:32:27 -0400141 headers['accept'] = self.accept
Joe Gregorio6429bf62011-03-01 22:53:21 -0800142 headers['accept-encoding'] = 'gzip, deflate'
Joe Gregorio3ad5e9a2010-12-09 15:01:04 -0500143 if 'user-agent' in headers:
144 headers['user-agent'] += ' '
145 else:
146 headers['user-agent'] = ''
147 headers['user-agent'] += 'google-api-python-client/1.0'
Joe Gregoriod433b2a2011-02-22 10:51:51 -0500148
Joe Gregorioafdf50b2011-03-08 09:41:52 -0500149 if body_value is not None:
Matt McDonald2a5f4132011-04-29 16:32:27 -0400150 headers['content-type'] = self.content_type
151 body_value = self.serialize(body_value)
152 self._log_request(headers, path_params, query, body_value)
Joe Gregorioafdf50b2011-03-08 09:41:52 -0500153 return (headers, path_params, query, body_value)
Joe Gregorio3ad5e9a2010-12-09 15:01:04 -0500154
155 def _build_query(self, params):
156 """Builds a query string.
157
158 Args:
159 params: dict, the query parameters
160
161 Returns:
162 The query parameters properly encoded into an HTTP URI query string.
163 """
Matt McDonald2a5f4132011-04-29 16:32:27 -0400164 params.update({'alt': self.alt_param})
Joe Gregorio3ad5e9a2010-12-09 15:01:04 -0500165 astuples = []
166 for key, value in params.iteritems():
Joe Gregorio61d7e962011-02-22 22:52:07 -0500167 if type(value) == type([]):
168 for x in value:
169 x = x.encode('utf-8')
170 astuples.append((key, x))
171 else:
172 if getattr(value, 'encode', False) and callable(value.encode):
173 value = value.encode('utf-8')
174 astuples.append((key, value))
Joe Gregorio3ad5e9a2010-12-09 15:01:04 -0500175 return '?' + urllib.urlencode(astuples)
176
Matt McDonald2a5f4132011-04-29 16:32:27 -0400177 def _log_response(self, resp, content):
178 """Logs debugging information about the response if requested."""
179 if FLAGS.dump_request_response:
180 logging.info('--response-start--')
181 for h, v in resp.iteritems():
182 logging.info('%s: %s', h, v)
183 if content:
184 logging.info(content)
185 logging.info('--response-end--')
186
Joe Gregorio3ad5e9a2010-12-09 15:01:04 -0500187 def response(self, resp, content):
188 """Convert the response wire format into a Python object.
189
190 Args:
191 resp: httplib2.Response, the HTTP response headers and status
192 content: string, the body of the HTTP response
193
194 Returns:
195 The body de-serialized as a Python object.
196
197 Raises:
198 apiclient.errors.HttpError if a non 2xx response is received.
199 """
Matt McDonald2a5f4132011-04-29 16:32:27 -0400200 self._log_response(resp, content)
Joe Gregorio3ad5e9a2010-12-09 15:01:04 -0500201 # Error handling is TBD, for example, do we retry
202 # for some operation/error combinations?
203 if resp.status < 300:
204 if resp.status == 204:
205 # A 204: No Content response should be treated differently
206 # to all the other success states
Matt McDonald2a5f4132011-04-29 16:32:27 -0400207 return self.no_content_response
208 return self.deserialize(content)
Joe Gregorio3ad5e9a2010-12-09 15:01:04 -0500209 else:
210 logging.debug('Content from bad request was: %s' % content)
Ali Afshar2dcc6522010-12-16 10:11:53 +0100211 raise HttpError(resp, content)
Joe Gregorio34044bc2011-03-07 16:58:33 -0500212
Matt McDonald2a5f4132011-04-29 16:32:27 -0400213 def serialize(self, body_value):
214 """Perform the actual Python object serialization.
Joe Gregorio34044bc2011-03-07 16:58:33 -0500215
216 Args:
Matt McDonald2a5f4132011-04-29 16:32:27 -0400217 body_value: object, the request body as a Python object.
218
219 Returns:
220 string, the body in serialized form.
221 """
222 _abstract()
223
224 def deserialize(self, content):
Joe Gregorio562b7312011-09-15 09:06:38 -0400225 """Perform the actual deserialization from response string to Python
226 object.
Matt McDonald2a5f4132011-04-29 16:32:27 -0400227
228 Args:
229 content: string, the body of the HTTP response
Joe Gregorio34044bc2011-03-07 16:58:33 -0500230
231 Returns:
232 The body de-serialized as a Python object.
233 """
Matt McDonald2a5f4132011-04-29 16:32:27 -0400234 _abstract()
Joe Gregorioafdf50b2011-03-08 09:41:52 -0500235
Matt McDonald2a5f4132011-04-29 16:32:27 -0400236
237class JsonModel(BaseModel):
238 """Model class for JSON.
239
240 Serializes and de-serializes between JSON and the Python
241 object representation of HTTP request and response bodies.
242 """
243 accept = 'application/json'
244 content_type = 'application/json'
245 alt_param = 'json'
246
247 def __init__(self, data_wrapper=False):
248 """Construct a JsonModel.
Joe Gregorioafdf50b2011-03-08 09:41:52 -0500249
250 Args:
Matt McDonald2a5f4132011-04-29 16:32:27 -0400251 data_wrapper: boolean, wrap requests and responses in a data wrapper
Joe Gregorioafdf50b2011-03-08 09:41:52 -0500252 """
Matt McDonald2a5f4132011-04-29 16:32:27 -0400253 self._data_wrapper = data_wrapper
254
255 def serialize(self, body_value):
256 if (isinstance(body_value, dict) and 'data' not in body_value and
257 self._data_wrapper):
258 body_value = {'data': body_value}
259 return simplejson.dumps(body_value)
260
261 def deserialize(self, content):
262 body = simplejson.loads(content)
263 if 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 ProtocolBufferModel(BaseModel):
273 """Model class for protocol buffers.
274
275 Serializes and de-serializes the binary protocol buffer sent in the HTTP
276 request and response bodies.
277 """
278 accept = 'application/x-protobuf'
279 content_type = 'application/x-protobuf'
280 alt_param = 'proto'
281
282 def __init__(self, protocol_buffer):
283 """Constructs a ProtocolBufferModel.
284
285 The serialzed protocol buffer returned in an HTTP response will be
286 de-serialized using the given protocol buffer class.
287
288 Args:
Joe Gregorio562b7312011-09-15 09:06:38 -0400289 protocol_buffer: The protocol buffer class used to de-serialize a
290 response from the API.
Matt McDonald2a5f4132011-04-29 16:32:27 -0400291 """
292 self._protocol_buffer = protocol_buffer
293
294 def serialize(self, body_value):
295 return body_value.SerializeToString()
296
297 def deserialize(self, content):
298 return self._protocol_buffer.FromString(content)
299
300 @property
301 def no_content_response(self):
302 return self._protocol_buffer()
Joe Gregorioe98c2322011-05-26 15:40:48 -0400303
304
305def makepatch(original, modified):
306 """Create a patch object.
307
308 Some methods support PATCH, an efficient way to send updates to a resource.
309 This method allows the easy construction of patch bodies by looking at the
310 differences between a resource before and after it was modified.
311
312 Args:
313 original: object, the original deserialized resource
314 modified: object, the modified deserialized resource
315 Returns:
316 An object that contains only the changes from original to modified, in a
317 form suitable to pass to a PATCH method.
318
319 Example usage:
320 item = service.activities().get(postid=postid, userid=userid).execute()
321 original = copy.deepcopy(item)
322 item['object']['content'] = 'This is updated.'
323 service.activities.patch(postid=postid, userid=userid,
324 body=makepatch(original, item)).execute()
325 """
326 patch = {}
327 for key, original_value in original.iteritems():
328 modified_value = modified.get(key, None)
329 if modified_value is None:
330 # Use None to signal that the element is deleted
331 patch[key] = None
332 elif original_value != modified_value:
333 if type(original_value) == type({}):
334 # Recursively descend objects
335 patch[key] = makepatch(original_value, modified_value)
336 else:
337 # In the case of simple types or arrays we just replace
338 patch[key] = modified_value
339 else:
340 # Don't add anything to patch if there's no change
341 pass
342 for key in modified:
343 if key not in original:
344 patch[key] = modified[key]
345
346 return patch