blob: 2481c235bf2c94f3f5a11449d0aa82bae493dd62 [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 Gregoriof4153422011-03-18 22:45:18 -040033from anyjson import simplejson
Joe Gregorioc5c5a372010-09-22 11:42:32 -040034
35
36class HttpRequest(object):
Joe Gregorioaf276d22010-12-09 14:26:58 -050037 """Encapsulates a single HTTP request.
Joe Gregorioc5c5a372010-09-22 11:42:32 -040038 """
39
Joe Gregoriodeeb0202011-02-15 14:49:57 -050040 def __init__(self, http, postproc, uri,
Joe Gregorio7c22ab22011-02-16 15:32:39 -050041 method='GET',
Joe Gregoriodeeb0202011-02-15 14:49:57 -050042 body=None,
43 headers=None,
Joe Gregorioabda96f2011-02-11 20:19:33 -050044 methodId=None):
Joe Gregorioaf276d22010-12-09 14:26:58 -050045 """Constructor for an HttpRequest.
46
Joe Gregorioaf276d22010-12-09 14:26:58 -050047 Args:
48 http: httplib2.Http, the transport object to use to make a request
Joe Gregorioabda96f2011-02-11 20:19:33 -050049 postproc: callable, called on the HTTP response and content to transform
50 it into a data object before returning, or raising an exception
51 on an error.
Joe Gregorioaf276d22010-12-09 14:26:58 -050052 uri: string, the absolute URI to send the request to
53 method: string, the HTTP method to use
54 body: string, the request body of the HTTP request
55 headers: dict, the HTTP request headers
Joe Gregorioaf276d22010-12-09 14:26:58 -050056 methodId: string, a unique identifier for the API method being called.
57 """
Joe Gregorioc5c5a372010-09-22 11:42:32 -040058 self.uri = uri
59 self.method = method
60 self.body = body
61 self.headers = headers or {}
62 self.http = http
63 self.postproc = postproc
64
65 def execute(self, http=None):
66 """Execute the request.
67
Joe Gregorioaf276d22010-12-09 14:26:58 -050068 Args:
69 http: httplib2.Http, an http object to be used in place of the
70 one the HttpRequest request object was constructed with.
71
72 Returns:
73 A deserialized object model of the response body as determined
74 by the postproc.
75
76 Raises:
77 apiclient.errors.HttpError if the response was not a 2xx.
78 httplib2.Error if a transport error has occured.
Joe Gregorioc5c5a372010-09-22 11:42:32 -040079 """
80 if http is None:
81 http = self.http
82 resp, content = http.request(self.uri, self.method,
83 body=self.body,
84 headers=self.headers)
Joe Gregorio49396552011-03-08 10:39:00 -050085
86 if resp.status >= 300:
87 raise HttpError(resp, content, self.uri)
Joe Gregorioc5c5a372010-09-22 11:42:32 -040088 return self.postproc(resp, content)
Joe Gregorioaf276d22010-12-09 14:26:58 -050089
90
91class HttpRequestMock(object):
92 """Mock of HttpRequest.
93
94 Do not construct directly, instead use RequestMockBuilder.
95 """
96
97 def __init__(self, resp, content, postproc):
98 """Constructor for HttpRequestMock
99
100 Args:
101 resp: httplib2.Response, the response to emulate coming from the request
102 content: string, the response body
103 postproc: callable, the post processing function usually supplied by
104 the model class. See model.JsonModel.response() as an example.
105 """
106 self.resp = resp
107 self.content = content
108 self.postproc = postproc
109 if resp is None:
Joe Gregorioc6722462010-12-20 14:29:28 -0500110 self.resp = httplib2.Response({'status': 200, 'reason': 'OK'})
Joe Gregorioaf276d22010-12-09 14:26:58 -0500111 if 'reason' in self.resp:
112 self.resp.reason = self.resp['reason']
113
114 def execute(self, http=None):
115 """Execute the request.
116
117 Same behavior as HttpRequest.execute(), but the response is
118 mocked and not really from an HTTP request/response.
119 """
120 return self.postproc(self.resp, self.content)
121
122
123class RequestMockBuilder(object):
124 """A simple mock of HttpRequest
125
126 Pass in a dictionary to the constructor that maps request methodIds to
127 tuples of (httplib2.Response, content) that should be returned when that
128 method is called. None may also be passed in for the httplib2.Response, in
129 which case a 200 OK response will be generated.
130
131 Example:
132 response = '{"data": {"id": "tag:google.c...'
133 requestBuilder = RequestMockBuilder(
134 {
135 'chili.activities.get': (None, response),
136 }
137 )
138 apiclient.discovery.build("buzz", "v1", requestBuilder=requestBuilder)
139
140 Methods that you do not supply a response for will return a
141 200 OK with an empty string as the response content. The methodId
142 is taken from the rpcName in the discovery document.
143
144 For more details see the project wiki.
145 """
146
147 def __init__(self, responses):
148 """Constructor for RequestMockBuilder
149
150 The constructed object should be a callable object
151 that can replace the class HttpResponse.
152
153 responses - A dictionary that maps methodIds into tuples
154 of (httplib2.Response, content). The methodId
155 comes from the 'rpcName' field in the discovery
156 document.
157 """
158 self.responses = responses
159
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500160 def __call__(self, http, postproc, uri, method='GET', body=None,
Joe Gregorioabda96f2011-02-11 20:19:33 -0500161 headers=None, methodId=None):
Joe Gregorioaf276d22010-12-09 14:26:58 -0500162 """Implements the callable interface that discovery.build() expects
163 of requestBuilder, which is to build an object compatible with
164 HttpRequest.execute(). See that method for the description of the
165 parameters and the expected response.
166 """
167 if methodId in self.responses:
168 resp, content = self.responses[methodId]
169 return HttpRequestMock(resp, content, postproc)
170 else:
Joe Gregoriod433b2a2011-02-22 10:51:51 -0500171 model = JsonModel(False)
Joe Gregorioaf276d22010-12-09 14:26:58 -0500172 return HttpRequestMock(None, '{}', model.response)
Joe Gregoriocb8103d2011-02-11 23:20:52 -0500173
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500174
Joe Gregoriocb8103d2011-02-11 23:20:52 -0500175class HttpMock(object):
176 """Mock of httplib2.Http"""
177
Joe Gregorioec343652011-02-16 16:52:51 -0500178 def __init__(self, filename, headers=None):
Joe Gregoriocb8103d2011-02-11 23:20:52 -0500179 """
180 Args:
181 filename: string, absolute filename to read response from
182 headers: dict, header to return with response
183 """
Joe Gregorioec343652011-02-16 16:52:51 -0500184 if headers is None:
185 headers = {'status': '200 OK'}
Joe Gregoriocb8103d2011-02-11 23:20:52 -0500186 f = file(filename, 'r')
187 self.data = f.read()
188 f.close()
189 self.headers = headers
190
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500191 def request(self, uri,
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500192 method='GET',
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500193 body=None,
194 headers=None,
195 redirections=1,
196 connection_type=None):
Joe Gregoriocb8103d2011-02-11 23:20:52 -0500197 return httplib2.Response(self.headers), self.data
Joe Gregorioccc79542011-02-19 00:05:26 -0500198
199
200class HttpMockSequence(object):
201 """Mock of httplib2.Http
202
203 Mocks a sequence of calls to request returning different responses for each
204 call. Create an instance initialized with the desired response headers
205 and content and then use as if an httplib2.Http instance.
206
207 http = HttpMockSequence([
208 ({'status': '401'}, ''),
209 ({'status': '200'}, '{"access_token":"1/3w","expires_in":3600}'),
210 ({'status': '200'}, 'echo_request_headers'),
211 ])
212 resp, content = http.request("http://examples.com")
213
214 There are special values you can pass in for content to trigger
215 behavours that are helpful in testing.
216
217 'echo_request_headers' means return the request headers in the response body
Joe Gregorioe9e236f2011-03-21 22:23:14 -0400218 'echo_request_headers_as_json' means return the request headers in
219 the response body
Joe Gregorioccc79542011-02-19 00:05:26 -0500220 'echo_request_body' means return the request body in the response body
221 """
222
223 def __init__(self, iterable):
224 """
225 Args:
226 iterable: iterable, a sequence of pairs of (headers, body)
227 """
228 self._iterable = iterable
229
230 def request(self, uri,
231 method='GET',
232 body=None,
233 headers=None,
234 redirections=1,
235 connection_type=None):
236 resp, content = self._iterable.pop(0)
237 if content == 'echo_request_headers':
238 content = headers
Joe Gregoriof4153422011-03-18 22:45:18 -0400239 elif content == 'echo_request_headers_as_json':
240 content = simplejson.dumps(headers)
Joe Gregorioccc79542011-02-19 00:05:26 -0500241 elif content == 'echo_request_body':
242 content = body
243 return httplib2.Response(resp), content
Joe Gregorio6bcbcea2011-03-10 15:26:05 -0500244
245
246def set_user_agent(http, user_agent):
Joe Gregoriof4153422011-03-18 22:45:18 -0400247 """Set the user-agent on every request.
248
Joe Gregorio6bcbcea2011-03-10 15:26:05 -0500249 Args:
250 http - An instance of httplib2.Http
251 or something that acts like it.
252 user_agent: string, the value for the user-agent header.
253
254 Returns:
255 A modified instance of http that was passed in.
256
257 Example:
258
259 h = httplib2.Http()
260 h = set_user_agent(h, "my-app-name/6.0")
261
262 Most of the time the user-agent will be set doing auth, this is for the rare
263 cases where you are accessing an unauthenticated endpoint.
264 """
265 request_orig = http.request
266
267 # The closure that will replace 'httplib2.Http.request'.
268 def new_request(uri, method='GET', body=None, headers=None,
269 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
270 connection_type=None):
271 """Modify the request headers to add the user-agent."""
272 if headers is None:
273 headers = {}
274 if 'user-agent' in headers:
275 headers['user-agent'] = user_agent + ' ' + headers['user-agent']
276 else:
277 headers['user-agent'] = user_agent
278 resp, content = request_orig(uri, method, body, headers,
279 redirections, connection_type)
280 return resp, content
281
282 http.request = new_request
283 return http
Joe Gregoriof4153422011-03-18 22:45:18 -0400284
285
286def tunnel_patch(http):
287 """Tunnel PATCH requests over POST.
288 Args:
289 http - An instance of httplib2.Http
290 or something that acts like it.
291
292 Returns:
293 A modified instance of http that was passed in.
294
295 Example:
296
297 h = httplib2.Http()
298 h = tunnel_patch(h, "my-app-name/6.0")
299
300 Useful if you are running on a platform that doesn't support PATCH.
301 Apply this last if you are using OAuth 1.0, as changing the method
302 will result in a different signature.
303 """
304 request_orig = http.request
305
306 # The closure that will replace 'httplib2.Http.request'.
307 def new_request(uri, method='GET', body=None, headers=None,
308 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
309 connection_type=None):
310 """Modify the request headers to add the user-agent."""
311 if headers is None:
312 headers = {}
313 if method == 'PATCH':
Joe Gregorio06d852b2011-03-25 15:03:10 -0400314 if 'oauth_token' in headers.get('authorization', ''):
Joe Gregorioe9e236f2011-03-21 22:23:14 -0400315 logging.warning(
Joe Gregorio06d852b2011-03-25 15:03:10 -0400316 'OAuth 1.0 request made with Credentials after tunnel_patch.')
Joe Gregoriof4153422011-03-18 22:45:18 -0400317 headers['x-http-method-override'] = "PATCH"
318 method = 'POST'
319 resp, content = request_orig(uri, method, body, headers,
320 redirections, connection_type)
321 return resp, content
322
323 http.request = new_request
324 return http