blob: 5f4be004f867eab02d42a0089b5982ae4b6beab6 [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
Joe Gregorio0bc70912011-05-24 15:30:49 -0400221 'echo_request_uri' means return the request uri in the response body
Joe Gregorioccc79542011-02-19 00:05:26 -0500222 """
223
224 def __init__(self, iterable):
225 """
226 Args:
227 iterable: iterable, a sequence of pairs of (headers, body)
228 """
229 self._iterable = iterable
230
231 def request(self, uri,
232 method='GET',
233 body=None,
234 headers=None,
235 redirections=1,
236 connection_type=None):
237 resp, content = self._iterable.pop(0)
238 if content == 'echo_request_headers':
239 content = headers
Joe Gregoriof4153422011-03-18 22:45:18 -0400240 elif content == 'echo_request_headers_as_json':
241 content = simplejson.dumps(headers)
Joe Gregorioccc79542011-02-19 00:05:26 -0500242 elif content == 'echo_request_body':
243 content = body
Joe Gregorio0bc70912011-05-24 15:30:49 -0400244 elif content == 'echo_request_uri':
245 content = uri
Joe Gregorioccc79542011-02-19 00:05:26 -0500246 return httplib2.Response(resp), content
Joe Gregorio6bcbcea2011-03-10 15:26:05 -0500247
248
249def set_user_agent(http, user_agent):
Joe Gregoriof4153422011-03-18 22:45:18 -0400250 """Set the user-agent on every request.
251
Joe Gregorio6bcbcea2011-03-10 15:26:05 -0500252 Args:
253 http - An instance of httplib2.Http
254 or something that acts like it.
255 user_agent: string, the value for the user-agent header.
256
257 Returns:
258 A modified instance of http that was passed in.
259
260 Example:
261
262 h = httplib2.Http()
263 h = set_user_agent(h, "my-app-name/6.0")
264
265 Most of the time the user-agent will be set doing auth, this is for the rare
266 cases where you are accessing an unauthenticated endpoint.
267 """
268 request_orig = http.request
269
270 # The closure that will replace 'httplib2.Http.request'.
271 def new_request(uri, method='GET', body=None, headers=None,
272 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
273 connection_type=None):
274 """Modify the request headers to add the user-agent."""
275 if headers is None:
276 headers = {}
277 if 'user-agent' in headers:
278 headers['user-agent'] = user_agent + ' ' + headers['user-agent']
279 else:
280 headers['user-agent'] = user_agent
281 resp, content = request_orig(uri, method, body, headers,
282 redirections, connection_type)
283 return resp, content
284
285 http.request = new_request
286 return http
Joe Gregoriof4153422011-03-18 22:45:18 -0400287
288
289def tunnel_patch(http):
290 """Tunnel PATCH requests over POST.
291 Args:
292 http - An instance of httplib2.Http
293 or something that acts like it.
294
295 Returns:
296 A modified instance of http that was passed in.
297
298 Example:
299
300 h = httplib2.Http()
301 h = tunnel_patch(h, "my-app-name/6.0")
302
303 Useful if you are running on a platform that doesn't support PATCH.
304 Apply this last if you are using OAuth 1.0, as changing the method
305 will result in a different signature.
306 """
307 request_orig = http.request
308
309 # The closure that will replace 'httplib2.Http.request'.
310 def new_request(uri, method='GET', body=None, headers=None,
311 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
312 connection_type=None):
313 """Modify the request headers to add the user-agent."""
314 if headers is None:
315 headers = {}
316 if method == 'PATCH':
Joe Gregorio06d852b2011-03-25 15:03:10 -0400317 if 'oauth_token' in headers.get('authorization', ''):
Joe Gregorioe9e236f2011-03-21 22:23:14 -0400318 logging.warning(
Joe Gregorio06d852b2011-03-25 15:03:10 -0400319 'OAuth 1.0 request made with Credentials after tunnel_patch.')
Joe Gregoriof4153422011-03-18 22:45:18 -0400320 headers['x-http-method-override'] = "PATCH"
321 method = 'POST'
322 resp, content = request_orig(uri, method, body, headers,
323 redirections, connection_type)
324 return resp, content
325
326 http.request = new_request
327 return http