blob: 7bc68586b0fa18f1b55d42a3285f2920e00484c1 [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,
Joe Gregorio205e73a2011-03-12 09:55:31 -050037 'Dump all http server requests and responses. '
38 'Must use apiclient.model.LoggingJsonModel as '
39 'the model.'
40 )
Joe Gregoriodeeb0202011-02-15 14:49:57 -050041
Joe Gregorioafdf50b2011-03-08 09:41:52 -050042
Joe Gregorioabda96f2011-02-11 20:19:33 -050043def _abstract():
44 raise NotImplementedError('You need to override this function')
Joe Gregorio3ad5e9a2010-12-09 15:01:04 -050045
Joe Gregorioabda96f2011-02-11 20:19:33 -050046
47class Model(object):
48 """Model base class.
49
50 All Model classes should implement this interface.
51 The Model serializes and de-serializes between a wire
52 format such as JSON and a Python object representation.
53 """
54
55 def request(self, headers, path_params, query_params, body_value):
56 """Updates outgoing requests with a deserialized body.
57
58 Args:
59 headers: dict, request headers
60 path_params: dict, parameters that appear in the request path
61 query_params: dict, parameters that appear in the query
62 body_value: object, the request body as a Python object, which must be
63 serializable.
64 Returns:
65 A tuple of (headers, path_params, query, body)
66
67 headers: dict, request headers
68 path_params: dict, parameters that appear in the request path
69 query: string, query part of the request URI
70 body: string, the body serialized in the desired wire format.
71 """
72 _abstract()
73
74 def response(self, resp, content):
75 """Convert the response wire format into a Python object.
76
77 Args:
78 resp: httplib2.Response, the HTTP response headers and status
79 content: string, the body of the HTTP response
80
81 Returns:
82 The body de-serialized as a Python object.
83
84 Raises:
85 apiclient.errors.HttpError if a non 2xx response is received.
86 """
87 _abstract()
88
89
90class JsonModel(Model):
Joe Gregorio3ad5e9a2010-12-09 15:01:04 -050091 """Model class for JSON.
92
93 Serializes and de-serializes between JSON and the Python
94 object representation of HTTP request and response bodies.
95 """
96
Joe Gregorio10894442011-02-23 14:46:55 -050097 def __init__(self, data_wrapper=False):
Joe Gregoriod433b2a2011-02-22 10:51:51 -050098 """Construct a JsonModel
99
100 Args:
101 data_wrapper: boolean, wrap requests and responses in a data wrapper
102 """
103 self._data_wrapper = data_wrapper
104
Joe Gregorio3ad5e9a2010-12-09 15:01:04 -0500105 def request(self, headers, path_params, query_params, body_value):
106 """Updates outgoing requests with JSON bodies.
107
108 Args:
109 headers: dict, request headers
110 path_params: dict, parameters that appear in the request path
111 query_params: dict, parameters that appear in the query
112 body_value: object, the request body as a Python object, which must be
113 serializable by simplejson.
114 Returns:
115 A tuple of (headers, path_params, query, body)
116
117 headers: dict, request headers
118 path_params: dict, parameters that appear in the request path
119 query: string, query part of the request URI
120 body: string, the body serialized as JSON
121 """
122 query = self._build_query(query_params)
123 headers['accept'] = 'application/json'
Joe Gregorio6429bf62011-03-01 22:53:21 -0800124 headers['accept-encoding'] = 'gzip, deflate'
Joe Gregorio3ad5e9a2010-12-09 15:01:04 -0500125 if 'user-agent' in headers:
126 headers['user-agent'] += ' '
127 else:
128 headers['user-agent'] = ''
129 headers['user-agent'] += 'google-api-python-client/1.0'
Joe Gregoriod433b2a2011-02-22 10:51:51 -0500130
131 if (isinstance(body_value, dict) and 'data' not in body_value and
132 self._data_wrapper):
133 body_value = {'data': body_value}
Joe Gregorioafdf50b2011-03-08 09:41:52 -0500134 if body_value is not None:
Joe Gregorio3ad5e9a2010-12-09 15:01:04 -0500135 headers['content-type'] = 'application/json'
Joe Gregorioafdf50b2011-03-08 09:41:52 -0500136 body_value = simplejson.dumps(body_value)
137 return (headers, path_params, query, body_value)
Joe Gregorio3ad5e9a2010-12-09 15:01:04 -0500138
139 def _build_query(self, params):
140 """Builds a query string.
141
142 Args:
143 params: dict, the query parameters
144
145 Returns:
146 The query parameters properly encoded into an HTTP URI query string.
147 """
148 params.update({'alt': 'json'})
149 astuples = []
150 for key, value in params.iteritems():
Joe Gregorio61d7e962011-02-22 22:52:07 -0500151 if type(value) == type([]):
152 for x in value:
153 x = x.encode('utf-8')
154 astuples.append((key, x))
155 else:
156 if getattr(value, 'encode', False) and callable(value.encode):
157 value = value.encode('utf-8')
158 astuples.append((key, value))
Joe Gregorio3ad5e9a2010-12-09 15:01:04 -0500159 return '?' + urllib.urlencode(astuples)
160
161 def response(self, resp, content):
162 """Convert the response wire format into a Python object.
163
164 Args:
165 resp: httplib2.Response, the HTTP response headers and status
166 content: string, the body of the HTTP response
167
168 Returns:
169 The body de-serialized as a Python object.
170
171 Raises:
172 apiclient.errors.HttpError if a non 2xx response is received.
173 """
174 # Error handling is TBD, for example, do we retry
175 # for some operation/error combinations?
176 if resp.status < 300:
177 if resp.status == 204:
178 # A 204: No Content response should be treated differently
179 # to all the other success states
180 return simplejson.loads('{}')
181 body = simplejson.loads(content)
182 if isinstance(body, dict) and 'data' in body:
183 body = body['data']
184 return body
185 else:
186 logging.debug('Content from bad request was: %s' % content)
Ali Afshar2dcc6522010-12-16 10:11:53 +0100187 raise HttpError(resp, content)
Joe Gregorio34044bc2011-03-07 16:58:33 -0500188
189
190class LoggingJsonModel(JsonModel):
191 """A printable JsonModel class that supports logging response info."""
192
193 def response(self, resp, content):
194 """An overloaded response method that will output debug info if requested.
195
196 Args:
197 resp: An httplib2.Response object.
198 content: A string representing the response body.
199
200 Returns:
201 The body de-serialized as a Python object.
202 """
Joe Gregorioafdf50b2011-03-08 09:41:52 -0500203 if FLAGS.dump_request_response:
Joe Gregorio34044bc2011-03-07 16:58:33 -0500204 logging.info('--response-start--')
205 for h, v in resp.iteritems():
206 logging.info('%s: %s', h, v)
207 if content:
208 logging.info(content)
209 logging.info('--response-end--')
210 return super(LoggingJsonModel, self).response(
211 resp, content)
Joe Gregorioafdf50b2011-03-08 09:41:52 -0500212
213 def request(self, headers, path_params, query_params, body_value):
214 """An overloaded request method that will output debug info if requested.
215
216 Args:
217 headers: dict, request headers
218 path_params: dict, parameters that appear in the request path
219 query_params: dict, parameters that appear in the query
220 body_value: object, the request body as a Python object, which must be
221 serializable by simplejson.
222 Returns:
223 A tuple of (headers, path_params, query, body)
224
225 headers: dict, request headers
226 path_params: dict, parameters that appear in the request path
227 query: string, query part of the request URI
228 body: string, the body serialized as JSON
229 """
230 (headers, path_params, query, body) = super(
231 LoggingJsonModel, self).request(
232 headers, path_params, query_params, body_value)
233 if FLAGS.dump_request_response:
234 logging.info('--request-start--')
235 logging.info('-headers-start-')
236 for h, v in headers.iteritems():
237 logging.info('%s: %s', h, v)
238 logging.info('-headers-end-')
239 logging.info('-path-parameters-start-')
240 for h, v in path_params.iteritems():
241 logging.info('%s: %s', h, v)
242 logging.info('-path-parameters-end-')
243 logging.info('body: %s', body)
244 logging.info('query: %s', query)
245 logging.info('--request-end--')
246 return (headers, path_params, query, body)