blob: 1e5b8627f7b9da82ec9fbfea14340900931d1c7b [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 Gregorio205e73a2011-03-12 09:55:31 -050025 'Dump all http server requests and responses. '
26 'Must use apiclient.model.LoggingJsonModel as '
27 'the model.'
28 )
Joe Gregoriodeeb0202011-02-15 14:49:57 -050029
Joe Gregorioafdf50b2011-03-08 09:41:52 -050030
Joe Gregorioabda96f2011-02-11 20:19:33 -050031def _abstract():
32 raise NotImplementedError('You need to override this function')
Joe Gregorio3ad5e9a2010-12-09 15:01:04 -050033
Joe Gregorioabda96f2011-02-11 20:19:33 -050034
35class Model(object):
36 """Model base class.
37
38 All Model classes should implement this interface.
39 The Model serializes and de-serializes between a wire
40 format such as JSON and a Python object representation.
41 """
42
43 def request(self, headers, path_params, query_params, body_value):
44 """Updates outgoing requests with a deserialized body.
45
46 Args:
47 headers: dict, request headers
48 path_params: dict, parameters that appear in the request path
49 query_params: dict, parameters that appear in the query
50 body_value: object, the request body as a Python object, which must be
51 serializable.
52 Returns:
53 A tuple of (headers, path_params, query, body)
54
55 headers: dict, request headers
56 path_params: dict, parameters that appear in the request path
57 query: string, query part of the request URI
58 body: string, the body serialized in the desired wire format.
59 """
60 _abstract()
61
62 def response(self, resp, content):
63 """Convert the response wire format into a Python object.
64
65 Args:
66 resp: httplib2.Response, the HTTP response headers and status
67 content: string, the body of the HTTP response
68
69 Returns:
70 The body de-serialized as a Python object.
71
72 Raises:
73 apiclient.errors.HttpError if a non 2xx response is received.
74 """
75 _abstract()
76
77
78class JsonModel(Model):
Joe Gregorio3ad5e9a2010-12-09 15:01:04 -050079 """Model class for JSON.
80
81 Serializes and de-serializes between JSON and the Python
82 object representation of HTTP request and response bodies.
83 """
84
Joe Gregorio10894442011-02-23 14:46:55 -050085 def __init__(self, data_wrapper=False):
Joe Gregoriod433b2a2011-02-22 10:51:51 -050086 """Construct a JsonModel
87
88 Args:
89 data_wrapper: boolean, wrap requests and responses in a data wrapper
90 """
91 self._data_wrapper = data_wrapper
92
Joe Gregorio3ad5e9a2010-12-09 15:01:04 -050093 def request(self, headers, path_params, query_params, body_value):
94 """Updates outgoing requests with JSON bodies.
95
96 Args:
97 headers: dict, request headers
98 path_params: dict, parameters that appear in the request path
99 query_params: dict, parameters that appear in the query
100 body_value: object, the request body as a Python object, which must be
101 serializable by simplejson.
102 Returns:
103 A tuple of (headers, path_params, query, body)
104
105 headers: dict, request headers
106 path_params: dict, parameters that appear in the request path
107 query: string, query part of the request URI
108 body: string, the body serialized as JSON
109 """
110 query = self._build_query(query_params)
111 headers['accept'] = 'application/json'
Joe Gregorio6429bf62011-03-01 22:53:21 -0800112 headers['accept-encoding'] = 'gzip, deflate'
Joe Gregorio3ad5e9a2010-12-09 15:01:04 -0500113 if 'user-agent' in headers:
114 headers['user-agent'] += ' '
115 else:
116 headers['user-agent'] = ''
117 headers['user-agent'] += 'google-api-python-client/1.0'
Joe Gregoriod433b2a2011-02-22 10:51:51 -0500118
119 if (isinstance(body_value, dict) and 'data' not in body_value and
120 self._data_wrapper):
121 body_value = {'data': body_value}
Joe Gregorioafdf50b2011-03-08 09:41:52 -0500122 if body_value is not None:
Joe Gregorio3ad5e9a2010-12-09 15:01:04 -0500123 headers['content-type'] = 'application/json'
Joe Gregorioafdf50b2011-03-08 09:41:52 -0500124 body_value = simplejson.dumps(body_value)
125 return (headers, path_params, query, body_value)
Joe Gregorio3ad5e9a2010-12-09 15:01:04 -0500126
127 def _build_query(self, params):
128 """Builds a query string.
129
130 Args:
131 params: dict, the query parameters
132
133 Returns:
134 The query parameters properly encoded into an HTTP URI query string.
135 """
136 params.update({'alt': 'json'})
137 astuples = []
138 for key, value in params.iteritems():
Joe Gregorio61d7e962011-02-22 22:52:07 -0500139 if type(value) == type([]):
140 for x in value:
141 x = x.encode('utf-8')
142 astuples.append((key, x))
143 else:
144 if getattr(value, 'encode', False) and callable(value.encode):
145 value = value.encode('utf-8')
146 astuples.append((key, value))
Joe Gregorio3ad5e9a2010-12-09 15:01:04 -0500147 return '?' + urllib.urlencode(astuples)
148
149 def response(self, resp, content):
150 """Convert the response wire format into a Python object.
151
152 Args:
153 resp: httplib2.Response, the HTTP response headers and status
154 content: string, the body of the HTTP response
155
156 Returns:
157 The body de-serialized as a Python object.
158
159 Raises:
160 apiclient.errors.HttpError if a non 2xx response is received.
161 """
162 # Error handling is TBD, for example, do we retry
163 # for some operation/error combinations?
164 if resp.status < 300:
165 if resp.status == 204:
166 # A 204: No Content response should be treated differently
167 # to all the other success states
168 return simplejson.loads('{}')
169 body = simplejson.loads(content)
170 if isinstance(body, dict) and 'data' in body:
171 body = body['data']
172 return body
173 else:
174 logging.debug('Content from bad request was: %s' % content)
Ali Afshar2dcc6522010-12-16 10:11:53 +0100175 raise HttpError(resp, content)
Joe Gregorio34044bc2011-03-07 16:58:33 -0500176
177
178class LoggingJsonModel(JsonModel):
179 """A printable JsonModel class that supports logging response info."""
180
181 def response(self, resp, content):
182 """An overloaded response method that will output debug info if requested.
183
184 Args:
185 resp: An httplib2.Response object.
186 content: A string representing the response body.
187
188 Returns:
189 The body de-serialized as a Python object.
190 """
Joe Gregorioafdf50b2011-03-08 09:41:52 -0500191 if FLAGS.dump_request_response:
Joe Gregorio34044bc2011-03-07 16:58:33 -0500192 logging.info('--response-start--')
193 for h, v in resp.iteritems():
194 logging.info('%s: %s', h, v)
195 if content:
196 logging.info(content)
197 logging.info('--response-end--')
198 return super(LoggingJsonModel, self).response(
199 resp, content)
Joe Gregorioafdf50b2011-03-08 09:41:52 -0500200
201 def request(self, headers, path_params, query_params, body_value):
202 """An overloaded request method that will output debug info if requested.
203
204 Args:
205 headers: dict, request headers
206 path_params: dict, parameters that appear in the request path
207 query_params: dict, parameters that appear in the query
208 body_value: object, the request body as a Python object, which must be
209 serializable by simplejson.
210 Returns:
211 A tuple of (headers, path_params, query, body)
212
213 headers: dict, request headers
214 path_params: dict, parameters that appear in the request path
215 query: string, query part of the request URI
216 body: string, the body serialized as JSON
217 """
218 (headers, path_params, query, body) = super(
219 LoggingJsonModel, self).request(
220 headers, path_params, query_params, body_value)
221 if FLAGS.dump_request_response:
222 logging.info('--request-start--')
223 logging.info('-headers-start-')
224 for h, v in headers.iteritems():
225 logging.info('%s: %s', h, v)
226 logging.info('-headers-end-')
227 logging.info('-path-parameters-start-')
228 for h, v in path_params.iteritems():
229 logging.info('%s: %s', h, v)
230 logging.info('-path-parameters-end-')
231 logging.info('body: %s', body)
232 logging.info('query: %s', query)
233 logging.info('--request-end--')
234 return (headers, path_params, query, body)