blob: b1f7c0e6ffc0a58c688cb4ab4d9c46ddb60f4aa0 [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
24gflags.DEFINE_boolean('dump_request', False,
25 'Dump all http server requests and responses.')
Joe Gregoriodeeb0202011-02-15 14:49:57 -050026
Joe Gregorioabda96f2011-02-11 20:19:33 -050027def _abstract():
28 raise NotImplementedError('You need to override this function')
Joe Gregorio3ad5e9a2010-12-09 15:01:04 -050029
Joe Gregorioabda96f2011-02-11 20:19:33 -050030
31class Model(object):
32 """Model base class.
33
34 All Model classes should implement this interface.
35 The Model serializes and de-serializes between a wire
36 format such as JSON and a Python object representation.
37 """
38
39 def request(self, headers, path_params, query_params, body_value):
40 """Updates outgoing requests with a deserialized body.
41
42 Args:
43 headers: dict, request headers
44 path_params: dict, parameters that appear in the request path
45 query_params: dict, parameters that appear in the query
46 body_value: object, the request body as a Python object, which must be
47 serializable.
48 Returns:
49 A tuple of (headers, path_params, query, body)
50
51 headers: dict, request headers
52 path_params: dict, parameters that appear in the request path
53 query: string, query part of the request URI
54 body: string, the body serialized in the desired wire format.
55 """
56 _abstract()
57
58 def response(self, resp, content):
59 """Convert the response wire format into a Python object.
60
61 Args:
62 resp: httplib2.Response, the HTTP response headers and status
63 content: string, the body of the HTTP response
64
65 Returns:
66 The body de-serialized as a Python object.
67
68 Raises:
69 apiclient.errors.HttpError if a non 2xx response is received.
70 """
71 _abstract()
72
73
74class JsonModel(Model):
Joe Gregorio3ad5e9a2010-12-09 15:01:04 -050075 """Model class for JSON.
76
77 Serializes and de-serializes between JSON and the Python
78 object representation of HTTP request and response bodies.
79 """
80
Joe Gregorio10894442011-02-23 14:46:55 -050081 def __init__(self, data_wrapper=False):
Joe Gregoriod433b2a2011-02-22 10:51:51 -050082 """Construct a JsonModel
83
84 Args:
85 data_wrapper: boolean, wrap requests and responses in a data wrapper
86 """
87 self._data_wrapper = data_wrapper
88
Joe Gregorio3ad5e9a2010-12-09 15:01:04 -050089 def request(self, headers, path_params, query_params, body_value):
90 """Updates outgoing requests with JSON bodies.
91
92 Args:
93 headers: dict, request headers
94 path_params: dict, parameters that appear in the request path
95 query_params: dict, parameters that appear in the query
96 body_value: object, the request body as a Python object, which must be
97 serializable by simplejson.
98 Returns:
99 A tuple of (headers, path_params, query, body)
100
101 headers: dict, request headers
102 path_params: dict, parameters that appear in the request path
103 query: string, query part of the request URI
104 body: string, the body serialized as JSON
105 """
106 query = self._build_query(query_params)
107 headers['accept'] = 'application/json'
Joe Gregorio6429bf62011-03-01 22:53:21 -0800108 headers['accept-encoding'] = 'gzip, deflate'
Joe Gregorio3ad5e9a2010-12-09 15:01:04 -0500109 if 'user-agent' in headers:
110 headers['user-agent'] += ' '
111 else:
112 headers['user-agent'] = ''
113 headers['user-agent'] += 'google-api-python-client/1.0'
Joe Gregoriod433b2a2011-02-22 10:51:51 -0500114
115 if (isinstance(body_value, dict) and 'data' not in body_value and
116 self._data_wrapper):
117 body_value = {'data': body_value}
Joe Gregorio3ad5e9a2010-12-09 15:01:04 -0500118 if body_value is None:
119 return (headers, path_params, query, None)
120 else:
121 headers['content-type'] = 'application/json'
122 return (headers, path_params, query, simplejson.dumps(body_value))
123
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 """
188 if FLAGS.dump_request:
189 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)