blob: d2a3a2f9bd68dc9290c660d48c16e857041e8d51 [file] [log] [blame]
Joe Gregorio20a5aa92011-04-01 17:44:25 -04001# Copyright (C) 2010 Google Inc.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
Joe Gregorioc5c5a372010-09-22 11:42:32 -040014
Joe Gregorioaf276d22010-12-09 14:26:58 -050015"""Classes to encapsulate a single HTTP request.
Joe Gregorioc5c5a372010-09-22 11:42:32 -040016
Joe Gregorioaf276d22010-12-09 14:26:58 -050017The classes implement a command pattern, with every
18object supporting an execute() method that does the
19actuall HTTP request.
Joe Gregorioc5c5a372010-09-22 11:42:32 -040020"""
21
22__author__ = 'jcgregorio@google.com (Joe Gregorio)'
Joe Gregorioaf276d22010-12-09 14:26:58 -050023__all__ = [
Joe Gregoriocb8103d2011-02-11 23:20:52 -050024 'HttpRequest', 'RequestMockBuilder', 'HttpMock'
Joe Gregoriof4153422011-03-18 22:45:18 -040025 'set_user_agent', 'tunnel_patch'
Joe Gregorioaf276d22010-12-09 14:26:58 -050026 ]
27
Joe Gregorioc6722462010-12-20 14:29:28 -050028import httplib2
Joe Gregoriocb8103d2011-02-11 23:20:52 -050029import os
30
Joe Gregorio89174d22010-12-20 14:37:36 -050031from model import JsonModel
Joe Gregorio49396552011-03-08 10:39:00 -050032from errors import HttpError
Joe Gregorioa388ce32011-09-09 17:19:13 -040033from errors import UnexpectedBodyError
34from errors import UnexpectedMethodError
Joe Gregoriof4153422011-03-18 22:45:18 -040035from anyjson import simplejson
Joe Gregorioc5c5a372010-09-22 11:42:32 -040036
37
38class HttpRequest(object):
Joe Gregorioaf276d22010-12-09 14:26:58 -050039 """Encapsulates a single HTTP request.
Joe Gregorioc5c5a372010-09-22 11:42:32 -040040 """
41
Joe Gregoriodeeb0202011-02-15 14:49:57 -050042 def __init__(self, http, postproc, uri,
Joe Gregorio7c22ab22011-02-16 15:32:39 -050043 method='GET',
Joe Gregoriodeeb0202011-02-15 14:49:57 -050044 body=None,
45 headers=None,
Joe Gregorioabda96f2011-02-11 20:19:33 -050046 methodId=None):
Joe Gregorioaf276d22010-12-09 14:26:58 -050047 """Constructor for an HttpRequest.
48
Joe Gregorioaf276d22010-12-09 14:26:58 -050049 Args:
50 http: httplib2.Http, the transport object to use to make a request
Joe Gregorioabda96f2011-02-11 20:19:33 -050051 postproc: callable, called on the HTTP response and content to transform
52 it into a data object before returning, or raising an exception
53 on an error.
Joe Gregorioaf276d22010-12-09 14:26:58 -050054 uri: string, the absolute URI to send the request to
55 method: string, the HTTP method to use
56 body: string, the request body of the HTTP request
57 headers: dict, the HTTP request headers
Joe Gregorioaf276d22010-12-09 14:26:58 -050058 methodId: string, a unique identifier for the API method being called.
59 """
Joe Gregorioc5c5a372010-09-22 11:42:32 -040060 self.uri = uri
61 self.method = method
62 self.body = body
63 self.headers = headers or {}
64 self.http = http
65 self.postproc = postproc
66
67 def execute(self, http=None):
68 """Execute the request.
69
Joe Gregorioaf276d22010-12-09 14:26:58 -050070 Args:
71 http: httplib2.Http, an http object to be used in place of the
72 one the HttpRequest request object was constructed with.
73
74 Returns:
75 A deserialized object model of the response body as determined
76 by the postproc.
77
78 Raises:
79 apiclient.errors.HttpError if the response was not a 2xx.
80 httplib2.Error if a transport error has occured.
Joe Gregorioc5c5a372010-09-22 11:42:32 -040081 """
82 if http is None:
83 http = self.http
84 resp, content = http.request(self.uri, self.method,
85 body=self.body,
86 headers=self.headers)
Joe Gregorio49396552011-03-08 10:39:00 -050087
88 if resp.status >= 300:
89 raise HttpError(resp, content, self.uri)
Joe Gregorioc5c5a372010-09-22 11:42:32 -040090 return self.postproc(resp, content)
Joe Gregorioaf276d22010-12-09 14:26:58 -050091
92
93class HttpRequestMock(object):
94 """Mock of HttpRequest.
95
96 Do not construct directly, instead use RequestMockBuilder.
97 """
98
99 def __init__(self, resp, content, postproc):
100 """Constructor for HttpRequestMock
101
102 Args:
103 resp: httplib2.Response, the response to emulate coming from the request
104 content: string, the response body
105 postproc: callable, the post processing function usually supplied by
106 the model class. See model.JsonModel.response() as an example.
107 """
108 self.resp = resp
109 self.content = content
110 self.postproc = postproc
111 if resp is None:
Joe Gregorioc6722462010-12-20 14:29:28 -0500112 self.resp = httplib2.Response({'status': 200, 'reason': 'OK'})
Joe Gregorioaf276d22010-12-09 14:26:58 -0500113 if 'reason' in self.resp:
114 self.resp.reason = self.resp['reason']
115
116 def execute(self, http=None):
117 """Execute the request.
118
119 Same behavior as HttpRequest.execute(), but the response is
120 mocked and not really from an HTTP request/response.
121 """
122 return self.postproc(self.resp, self.content)
123
124
125class RequestMockBuilder(object):
126 """A simple mock of HttpRequest
127
128 Pass in a dictionary to the constructor that maps request methodIds to
Joe Gregorioa388ce32011-09-09 17:19:13 -0400129 tuples of (httplib2.Response, content, opt_expected_body) that should be
130 returned when that method is called. None may also be passed in for the
131 httplib2.Response, in which case a 200 OK response will be generated.
132 If an opt_expected_body (str or dict) is provided, it will be compared to
133 the body and UnexpectedBodyError will be raised on inequality.
Joe Gregorioaf276d22010-12-09 14:26:58 -0500134
135 Example:
136 response = '{"data": {"id": "tag:google.c...'
137 requestBuilder = RequestMockBuilder(
138 {
Joe Gregorioc4fc0952011-11-09 12:21:11 -0500139 'plus.activities.get': (None, response),
Joe Gregorioaf276d22010-12-09 14:26:58 -0500140 }
141 )
Joe Gregorioc4fc0952011-11-09 12:21:11 -0500142 apiclient.discovery.build("plus", "v1", requestBuilder=requestBuilder)
Joe Gregorioaf276d22010-12-09 14:26:58 -0500143
144 Methods that you do not supply a response for will return a
Joe Gregorioa388ce32011-09-09 17:19:13 -0400145 200 OK with an empty string as the response content or raise an excpetion if
146 check_unexpected is set to True. The methodId is taken from the rpcName
147 in the discovery document.
Joe Gregorioaf276d22010-12-09 14:26:58 -0500148
149 For more details see the project wiki.
150 """
151
Joe Gregorioa388ce32011-09-09 17:19:13 -0400152 def __init__(self, responses, check_unexpected=False):
Joe Gregorioaf276d22010-12-09 14:26:58 -0500153 """Constructor for RequestMockBuilder
154
155 The constructed object should be a callable object
156 that can replace the class HttpResponse.
157
158 responses - A dictionary that maps methodIds into tuples
159 of (httplib2.Response, content). The methodId
160 comes from the 'rpcName' field in the discovery
161 document.
Joe Gregorioa388ce32011-09-09 17:19:13 -0400162 check_unexpected - A boolean setting whether or not UnexpectedMethodError
163 should be raised on unsupplied method.
Joe Gregorioaf276d22010-12-09 14:26:58 -0500164 """
165 self.responses = responses
Joe Gregorioa388ce32011-09-09 17:19:13 -0400166 self.check_unexpected = check_unexpected
Joe Gregorioaf276d22010-12-09 14:26:58 -0500167
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500168 def __call__(self, http, postproc, uri, method='GET', body=None,
Joe Gregorioabda96f2011-02-11 20:19:33 -0500169 headers=None, methodId=None):
Joe Gregorioaf276d22010-12-09 14:26:58 -0500170 """Implements the callable interface that discovery.build() expects
171 of requestBuilder, which is to build an object compatible with
172 HttpRequest.execute(). See that method for the description of the
173 parameters and the expected response.
174 """
175 if methodId in self.responses:
Joe Gregorioa388ce32011-09-09 17:19:13 -0400176 response = self.responses[methodId]
177 resp, content = response[:2]
178 if len(response) > 2:
179 # Test the body against the supplied expected_body.
180 expected_body = response[2]
181 if bool(expected_body) != bool(body):
182 # Not expecting a body and provided one
183 # or expecting a body and not provided one.
184 raise UnexpectedBodyError(expected_body, body)
185 if isinstance(expected_body, str):
186 expected_body = simplejson.loads(expected_body)
187 body = simplejson.loads(body)
188 if body != expected_body:
189 raise UnexpectedBodyError(expected_body, body)
Joe Gregorioaf276d22010-12-09 14:26:58 -0500190 return HttpRequestMock(resp, content, postproc)
Joe Gregorioa388ce32011-09-09 17:19:13 -0400191 elif self.check_unexpected:
192 raise UnexpectedMethodError(methodId)
Joe Gregorioaf276d22010-12-09 14:26:58 -0500193 else:
Joe Gregoriod433b2a2011-02-22 10:51:51 -0500194 model = JsonModel(False)
Joe Gregorioaf276d22010-12-09 14:26:58 -0500195 return HttpRequestMock(None, '{}', model.response)
Joe Gregoriocb8103d2011-02-11 23:20:52 -0500196
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500197
Joe Gregoriocb8103d2011-02-11 23:20:52 -0500198class HttpMock(object):
199 """Mock of httplib2.Http"""
200
Joe Gregorioec343652011-02-16 16:52:51 -0500201 def __init__(self, filename, headers=None):
Joe Gregoriocb8103d2011-02-11 23:20:52 -0500202 """
203 Args:
204 filename: string, absolute filename to read response from
205 headers: dict, header to return with response
206 """
Joe Gregorioec343652011-02-16 16:52:51 -0500207 if headers is None:
208 headers = {'status': '200 OK'}
Joe Gregoriocb8103d2011-02-11 23:20:52 -0500209 f = file(filename, 'r')
210 self.data = f.read()
211 f.close()
212 self.headers = headers
213
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500214 def request(self, uri,
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500215 method='GET',
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500216 body=None,
217 headers=None,
218 redirections=1,
219 connection_type=None):
Joe Gregoriocb8103d2011-02-11 23:20:52 -0500220 return httplib2.Response(self.headers), self.data
Joe Gregorioccc79542011-02-19 00:05:26 -0500221
222
223class HttpMockSequence(object):
224 """Mock of httplib2.Http
225
226 Mocks a sequence of calls to request returning different responses for each
227 call. Create an instance initialized with the desired response headers
228 and content and then use as if an httplib2.Http instance.
229
230 http = HttpMockSequence([
231 ({'status': '401'}, ''),
232 ({'status': '200'}, '{"access_token":"1/3w","expires_in":3600}'),
233 ({'status': '200'}, 'echo_request_headers'),
234 ])
235 resp, content = http.request("http://examples.com")
236
237 There are special values you can pass in for content to trigger
238 behavours that are helpful in testing.
239
240 'echo_request_headers' means return the request headers in the response body
Joe Gregorioe9e236f2011-03-21 22:23:14 -0400241 'echo_request_headers_as_json' means return the request headers in
242 the response body
Joe Gregorioccc79542011-02-19 00:05:26 -0500243 'echo_request_body' means return the request body in the response body
Joe Gregorio0bc70912011-05-24 15:30:49 -0400244 'echo_request_uri' means return the request uri in the response body
Joe Gregorioccc79542011-02-19 00:05:26 -0500245 """
246
247 def __init__(self, iterable):
248 """
249 Args:
250 iterable: iterable, a sequence of pairs of (headers, body)
251 """
252 self._iterable = iterable
253
254 def request(self, uri,
255 method='GET',
256 body=None,
257 headers=None,
258 redirections=1,
259 connection_type=None):
260 resp, content = self._iterable.pop(0)
261 if content == 'echo_request_headers':
262 content = headers
Joe Gregoriof4153422011-03-18 22:45:18 -0400263 elif content == 'echo_request_headers_as_json':
264 content = simplejson.dumps(headers)
Joe Gregorioccc79542011-02-19 00:05:26 -0500265 elif content == 'echo_request_body':
266 content = body
Joe Gregorio0bc70912011-05-24 15:30:49 -0400267 elif content == 'echo_request_uri':
268 content = uri
Joe Gregorioccc79542011-02-19 00:05:26 -0500269 return httplib2.Response(resp), content
Joe Gregorio6bcbcea2011-03-10 15:26:05 -0500270
271
272def set_user_agent(http, user_agent):
Joe Gregoriof4153422011-03-18 22:45:18 -0400273 """Set the user-agent on every request.
274
Joe Gregorio6bcbcea2011-03-10 15:26:05 -0500275 Args:
276 http - An instance of httplib2.Http
277 or something that acts like it.
278 user_agent: string, the value for the user-agent header.
279
280 Returns:
281 A modified instance of http that was passed in.
282
283 Example:
284
285 h = httplib2.Http()
286 h = set_user_agent(h, "my-app-name/6.0")
287
288 Most of the time the user-agent will be set doing auth, this is for the rare
289 cases where you are accessing an unauthenticated endpoint.
290 """
291 request_orig = http.request
292
293 # The closure that will replace 'httplib2.Http.request'.
294 def new_request(uri, method='GET', body=None, headers=None,
295 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
296 connection_type=None):
297 """Modify the request headers to add the user-agent."""
298 if headers is None:
299 headers = {}
300 if 'user-agent' in headers:
301 headers['user-agent'] = user_agent + ' ' + headers['user-agent']
302 else:
303 headers['user-agent'] = user_agent
304 resp, content = request_orig(uri, method, body, headers,
305 redirections, connection_type)
306 return resp, content
307
308 http.request = new_request
309 return http
Joe Gregoriof4153422011-03-18 22:45:18 -0400310
311
312def tunnel_patch(http):
313 """Tunnel PATCH requests over POST.
314 Args:
315 http - An instance of httplib2.Http
316 or something that acts like it.
317
318 Returns:
319 A modified instance of http that was passed in.
320
321 Example:
322
323 h = httplib2.Http()
324 h = tunnel_patch(h, "my-app-name/6.0")
325
326 Useful if you are running on a platform that doesn't support PATCH.
327 Apply this last if you are using OAuth 1.0, as changing the method
328 will result in a different signature.
329 """
330 request_orig = http.request
331
332 # The closure that will replace 'httplib2.Http.request'.
333 def new_request(uri, method='GET', body=None, headers=None,
334 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
335 connection_type=None):
336 """Modify the request headers to add the user-agent."""
337 if headers is None:
338 headers = {}
339 if method == 'PATCH':
Joe Gregorio06d852b2011-03-25 15:03:10 -0400340 if 'oauth_token' in headers.get('authorization', ''):
Joe Gregorioe9e236f2011-03-21 22:23:14 -0400341 logging.warning(
Joe Gregorio06d852b2011-03-25 15:03:10 -0400342 'OAuth 1.0 request made with Credentials after tunnel_patch.')
Joe Gregoriof4153422011-03-18 22:45:18 -0400343 headers['x-http-method-override'] = "PATCH"
344 method = 'POST'
345 resp, content = request_orig(uri, method, body, headers,
346 redirections, connection_type)
347 return resp, content
348
349 http.request = new_request
350 return http