blob: 4ca91f4face7c6637273d07aa21839c0d10cd0f0 [file] [log] [blame]
Joe Gregorio3ad5e9a2010-12-09 15:01:04 -05001#!/usr/bin/python2.4
2#
3# Copyright 2010 Google Inc. All Rights Reserved.
4
Joe Gregorioec343652011-02-16 16:52:51 -05005"""Model objects for requests and responses.
Joe Gregorio3ad5e9a2010-12-09 15:01:04 -05006
7Each API may support one or more serializations, such
8as JSON, Atom, etc. The model classes are responsible
9for converting between the wire format and the Python
10object representation.
11"""
12
13__author__ = 'jcgregorio@google.com (Joe Gregorio)'
14
Joe Gregorio34044bc2011-03-07 16:58:33 -050015import gflags
Joe Gregorio3ad5e9a2010-12-09 15:01:04 -050016import logging
17import urllib
18
Joe Gregoriob843fa22010-12-13 16:26:07 -050019from anyjson import simplejson
20from errors import HttpError
Joe Gregorio3ad5e9a2010-12-09 15:01:04 -050021
Joe Gregorio34044bc2011-03-07 16:58:33 -050022FLAGS = gflags.FLAGS
23
Joe Gregorioafdf50b2011-03-08 09:41:52 -050024gflags.DEFINE_boolean('dump_request_response', False,
Joe Gregorio34044bc2011-03-07 16:58:33 -050025 'Dump all http server requests and responses.')
Joe Gregoriodeeb0202011-02-15 14:49:57 -050026
Joe Gregorioafdf50b2011-03-08 09:41:52 -050027
Joe Gregorioabda96f2011-02-11 20:19:33 -050028def _abstract():
29 raise NotImplementedError('You need to override this function')
Joe Gregorio3ad5e9a2010-12-09 15:01:04 -050030
Joe Gregorioabda96f2011-02-11 20:19:33 -050031
32class Model(object):
33 """Model base class.
34
35 All Model classes should implement this interface.
36 The Model serializes and de-serializes between a wire
37 format such as JSON and a Python object representation.
38 """
39
40 def request(self, headers, path_params, query_params, body_value):
41 """Updates outgoing requests with a deserialized body.
42
43 Args:
44 headers: dict, request headers
45 path_params: dict, parameters that appear in the request path
46 query_params: dict, parameters that appear in the query
47 body_value: object, the request body as a Python object, which must be
48 serializable.
49 Returns:
50 A tuple of (headers, path_params, query, body)
51
52 headers: dict, request headers
53 path_params: dict, parameters that appear in the request path
54 query: string, query part of the request URI
55 body: string, the body serialized in the desired wire format.
56 """
57 _abstract()
58
59 def response(self, resp, content):
60 """Convert the response wire format into a Python object.
61
62 Args:
63 resp: httplib2.Response, the HTTP response headers and status
64 content: string, the body of the HTTP response
65
66 Returns:
67 The body de-serialized as a Python object.
68
69 Raises:
70 apiclient.errors.HttpError if a non 2xx response is received.
71 """
72 _abstract()
73
74
75class JsonModel(Model):
Joe Gregorio3ad5e9a2010-12-09 15:01:04 -050076 """Model class for JSON.
77
78 Serializes and de-serializes between JSON and the Python
79 object representation of HTTP request and response bodies.
80 """
81
Joe Gregorio10894442011-02-23 14:46:55 -050082 def __init__(self, data_wrapper=False):
Joe Gregoriod433b2a2011-02-22 10:51:51 -050083 """Construct a JsonModel
84
85 Args:
86 data_wrapper: boolean, wrap requests and responses in a data wrapper
87 """
88 self._data_wrapper = data_wrapper
89
Joe Gregorio3ad5e9a2010-12-09 15:01:04 -050090 def request(self, headers, path_params, query_params, body_value):
91 """Updates outgoing requests with JSON bodies.
92
93 Args:
94 headers: dict, request headers
95 path_params: dict, parameters that appear in the request path
96 query_params: dict, parameters that appear in the query
97 body_value: object, the request body as a Python object, which must be
98 serializable by simplejson.
99 Returns:
100 A tuple of (headers, path_params, query, body)
101
102 headers: dict, request headers
103 path_params: dict, parameters that appear in the request path
104 query: string, query part of the request URI
105 body: string, the body serialized as JSON
106 """
107 query = self._build_query(query_params)
108 headers['accept'] = 'application/json'
Joe Gregorio6429bf62011-03-01 22:53:21 -0800109 headers['accept-encoding'] = 'gzip, deflate'
Joe Gregorio3ad5e9a2010-12-09 15:01:04 -0500110 if 'user-agent' in headers:
111 headers['user-agent'] += ' '
112 else:
113 headers['user-agent'] = ''
114 headers['user-agent'] += 'google-api-python-client/1.0'
Joe Gregoriod433b2a2011-02-22 10:51:51 -0500115
116 if (isinstance(body_value, dict) and 'data' not in body_value and
117 self._data_wrapper):
118 body_value = {'data': body_value}
Joe Gregorioafdf50b2011-03-08 09:41:52 -0500119 if body_value is not None:
Joe Gregorio3ad5e9a2010-12-09 15:01:04 -0500120 headers['content-type'] = 'application/json'
Joe Gregorioafdf50b2011-03-08 09:41:52 -0500121 body_value = simplejson.dumps(body_value)
122 return (headers, path_params, query, body_value)
Joe Gregorio3ad5e9a2010-12-09 15:01:04 -0500123
124 def _build_query(self, params):
125 """Builds a query string.
126
127 Args:
128 params: dict, the query parameters
129
130 Returns:
131 The query parameters properly encoded into an HTTP URI query string.
132 """
133 params.update({'alt': 'json'})
134 astuples = []
135 for key, value in params.iteritems():
Joe Gregorio61d7e962011-02-22 22:52:07 -0500136 if type(value) == type([]):
137 for x in value:
138 x = x.encode('utf-8')
139 astuples.append((key, x))
140 else:
141 if getattr(value, 'encode', False) and callable(value.encode):
142 value = value.encode('utf-8')
143 astuples.append((key, value))
Joe Gregorio3ad5e9a2010-12-09 15:01:04 -0500144 return '?' + urllib.urlencode(astuples)
145
146 def response(self, resp, content):
147 """Convert the response wire format into a Python object.
148
149 Args:
150 resp: httplib2.Response, the HTTP response headers and status
151 content: string, the body of the HTTP response
152
153 Returns:
154 The body de-serialized as a Python object.
155
156 Raises:
157 apiclient.errors.HttpError if a non 2xx response is received.
158 """
159 # Error handling is TBD, for example, do we retry
160 # for some operation/error combinations?
161 if resp.status < 300:
162 if resp.status == 204:
163 # A 204: No Content response should be treated differently
164 # to all the other success states
165 return simplejson.loads('{}')
166 body = simplejson.loads(content)
167 if isinstance(body, dict) and 'data' in body:
168 body = body['data']
169 return body
170 else:
171 logging.debug('Content from bad request was: %s' % content)
Ali Afshar2dcc6522010-12-16 10:11:53 +0100172 raise HttpError(resp, content)
Joe Gregorio34044bc2011-03-07 16:58:33 -0500173
174
175class LoggingJsonModel(JsonModel):
176 """A printable JsonModel class that supports logging response info."""
177
178 def response(self, resp, content):
179 """An overloaded response method that will output debug info if requested.
180
181 Args:
182 resp: An httplib2.Response object.
183 content: A string representing the response body.
184
185 Returns:
186 The body de-serialized as a Python object.
187 """
Joe Gregorioafdf50b2011-03-08 09:41:52 -0500188 if FLAGS.dump_request_response:
Joe Gregorio34044bc2011-03-07 16:58:33 -0500189 logging.info('--response-start--')
190 for h, v in resp.iteritems():
191 logging.info('%s: %s', h, v)
192 if content:
193 logging.info(content)
194 logging.info('--response-end--')
195 return super(LoggingJsonModel, self).response(
196 resp, content)
Joe Gregorioafdf50b2011-03-08 09:41:52 -0500197
198 def request(self, headers, path_params, query_params, body_value):
199 """An overloaded request method that will output debug info if requested.
200
201 Args:
202 headers: dict, request headers
203 path_params: dict, parameters that appear in the request path
204 query_params: dict, parameters that appear in the query
205 body_value: object, the request body as a Python object, which must be
206 serializable by simplejson.
207 Returns:
208 A tuple of (headers, path_params, query, body)
209
210 headers: dict, request headers
211 path_params: dict, parameters that appear in the request path
212 query: string, query part of the request URI
213 body: string, the body serialized as JSON
214 """
215 (headers, path_params, query, body) = super(
216 LoggingJsonModel, self).request(
217 headers, path_params, query_params, body_value)
218 if FLAGS.dump_request_response:
219 logging.info('--request-start--')
220 logging.info('-headers-start-')
221 for h, v in headers.iteritems():
222 logging.info('%s: %s', h, v)
223 logging.info('-headers-end-')
224 logging.info('-path-parameters-start-')
225 for h, v in path_params.iteritems():
226 logging.info('%s: %s', h, v)
227 logging.info('-path-parameters-end-')
228 logging.info('body: %s', body)
229 logging.info('query: %s', query)
230 logging.info('--request-end--')
231 return (headers, path_params, query, body)