blob: 138ce48f0d1132d9ff95c40eba40ba60ac631f12 [file] [log] [blame]
Joe Gregorioc5c5a372010-09-22 11:42:32 -04001# Copyright 2010 Google Inc. All Rights Reserved.
2
Joe Gregorioaf276d22010-12-09 14:26:58 -05003"""Classes to encapsulate a single HTTP request.
Joe Gregorioc5c5a372010-09-22 11:42:32 -04004
Joe Gregorioaf276d22010-12-09 14:26:58 -05005The classes implement a command pattern, with every
6object supporting an execute() method that does the
7actuall HTTP request.
Joe Gregorioc5c5a372010-09-22 11:42:32 -04008"""
9
10__author__ = 'jcgregorio@google.com (Joe Gregorio)'
Joe Gregorioaf276d22010-12-09 14:26:58 -050011__all__ = [
Joe Gregoriocb8103d2011-02-11 23:20:52 -050012 'HttpRequest', 'RequestMockBuilder', 'HttpMock'
Joe Gregoriof4153422011-03-18 22:45:18 -040013 'set_user_agent', 'tunnel_patch'
Joe Gregorioaf276d22010-12-09 14:26:58 -050014 ]
15
Joe Gregorioc6722462010-12-20 14:29:28 -050016import httplib2
Joe Gregoriocb8103d2011-02-11 23:20:52 -050017import os
18
Joe Gregorio89174d22010-12-20 14:37:36 -050019from model import JsonModel
Joe Gregorio49396552011-03-08 10:39:00 -050020from errors import HttpError
Joe Gregoriof4153422011-03-18 22:45:18 -040021from anyjson import simplejson
Joe Gregorioc5c5a372010-09-22 11:42:32 -040022
23
24class HttpRequest(object):
Joe Gregorioaf276d22010-12-09 14:26:58 -050025 """Encapsulates a single HTTP request.
Joe Gregorioc5c5a372010-09-22 11:42:32 -040026 """
27
Joe Gregoriodeeb0202011-02-15 14:49:57 -050028 def __init__(self, http, postproc, uri,
Joe Gregorio7c22ab22011-02-16 15:32:39 -050029 method='GET',
Joe Gregoriodeeb0202011-02-15 14:49:57 -050030 body=None,
31 headers=None,
Joe Gregorioabda96f2011-02-11 20:19:33 -050032 methodId=None):
Joe Gregorioaf276d22010-12-09 14:26:58 -050033 """Constructor for an HttpRequest.
34
Joe Gregorioaf276d22010-12-09 14:26:58 -050035 Args:
36 http: httplib2.Http, the transport object to use to make a request
Joe Gregorioabda96f2011-02-11 20:19:33 -050037 postproc: callable, called on the HTTP response and content to transform
38 it into a data object before returning, or raising an exception
39 on an error.
Joe Gregorioaf276d22010-12-09 14:26:58 -050040 uri: string, the absolute URI to send the request to
41 method: string, the HTTP method to use
42 body: string, the request body of the HTTP request
43 headers: dict, the HTTP request headers
Joe Gregorioaf276d22010-12-09 14:26:58 -050044 methodId: string, a unique identifier for the API method being called.
45 """
Joe Gregorioc5c5a372010-09-22 11:42:32 -040046 self.uri = uri
47 self.method = method
48 self.body = body
49 self.headers = headers or {}
50 self.http = http
51 self.postproc = postproc
52
53 def execute(self, http=None):
54 """Execute the request.
55
Joe Gregorioaf276d22010-12-09 14:26:58 -050056 Args:
57 http: httplib2.Http, an http object to be used in place of the
58 one the HttpRequest request object was constructed with.
59
60 Returns:
61 A deserialized object model of the response body as determined
62 by the postproc.
63
64 Raises:
65 apiclient.errors.HttpError if the response was not a 2xx.
66 httplib2.Error if a transport error has occured.
Joe Gregorioc5c5a372010-09-22 11:42:32 -040067 """
68 if http is None:
69 http = self.http
70 resp, content = http.request(self.uri, self.method,
71 body=self.body,
72 headers=self.headers)
Joe Gregorio49396552011-03-08 10:39:00 -050073
74 if resp.status >= 300:
75 raise HttpError(resp, content, self.uri)
Joe Gregorioc5c5a372010-09-22 11:42:32 -040076 return self.postproc(resp, content)
Joe Gregorioaf276d22010-12-09 14:26:58 -050077
78
79class HttpRequestMock(object):
80 """Mock of HttpRequest.
81
82 Do not construct directly, instead use RequestMockBuilder.
83 """
84
85 def __init__(self, resp, content, postproc):
86 """Constructor for HttpRequestMock
87
88 Args:
89 resp: httplib2.Response, the response to emulate coming from the request
90 content: string, the response body
91 postproc: callable, the post processing function usually supplied by
92 the model class. See model.JsonModel.response() as an example.
93 """
94 self.resp = resp
95 self.content = content
96 self.postproc = postproc
97 if resp is None:
Joe Gregorioc6722462010-12-20 14:29:28 -050098 self.resp = httplib2.Response({'status': 200, 'reason': 'OK'})
Joe Gregorioaf276d22010-12-09 14:26:58 -050099 if 'reason' in self.resp:
100 self.resp.reason = self.resp['reason']
101
102 def execute(self, http=None):
103 """Execute the request.
104
105 Same behavior as HttpRequest.execute(), but the response is
106 mocked and not really from an HTTP request/response.
107 """
108 return self.postproc(self.resp, self.content)
109
110
111class RequestMockBuilder(object):
112 """A simple mock of HttpRequest
113
114 Pass in a dictionary to the constructor that maps request methodIds to
115 tuples of (httplib2.Response, content) that should be returned when that
116 method is called. None may also be passed in for the httplib2.Response, in
117 which case a 200 OK response will be generated.
118
119 Example:
120 response = '{"data": {"id": "tag:google.c...'
121 requestBuilder = RequestMockBuilder(
122 {
123 'chili.activities.get': (None, response),
124 }
125 )
126 apiclient.discovery.build("buzz", "v1", requestBuilder=requestBuilder)
127
128 Methods that you do not supply a response for will return a
129 200 OK with an empty string as the response content. The methodId
130 is taken from the rpcName in the discovery document.
131
132 For more details see the project wiki.
133 """
134
135 def __init__(self, responses):
136 """Constructor for RequestMockBuilder
137
138 The constructed object should be a callable object
139 that can replace the class HttpResponse.
140
141 responses - A dictionary that maps methodIds into tuples
142 of (httplib2.Response, content). The methodId
143 comes from the 'rpcName' field in the discovery
144 document.
145 """
146 self.responses = responses
147
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500148 def __call__(self, http, postproc, uri, method='GET', body=None,
Joe Gregorioabda96f2011-02-11 20:19:33 -0500149 headers=None, methodId=None):
Joe Gregorioaf276d22010-12-09 14:26:58 -0500150 """Implements the callable interface that discovery.build() expects
151 of requestBuilder, which is to build an object compatible with
152 HttpRequest.execute(). See that method for the description of the
153 parameters and the expected response.
154 """
155 if methodId in self.responses:
156 resp, content = self.responses[methodId]
157 return HttpRequestMock(resp, content, postproc)
158 else:
Joe Gregoriod433b2a2011-02-22 10:51:51 -0500159 model = JsonModel(False)
Joe Gregorioaf276d22010-12-09 14:26:58 -0500160 return HttpRequestMock(None, '{}', model.response)
Joe Gregoriocb8103d2011-02-11 23:20:52 -0500161
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500162
Joe Gregoriocb8103d2011-02-11 23:20:52 -0500163class HttpMock(object):
164 """Mock of httplib2.Http"""
165
Joe Gregorioec343652011-02-16 16:52:51 -0500166 def __init__(self, filename, headers=None):
Joe Gregoriocb8103d2011-02-11 23:20:52 -0500167 """
168 Args:
169 filename: string, absolute filename to read response from
170 headers: dict, header to return with response
171 """
Joe Gregorioec343652011-02-16 16:52:51 -0500172 if headers is None:
173 headers = {'status': '200 OK'}
Joe Gregoriocb8103d2011-02-11 23:20:52 -0500174 f = file(filename, 'r')
175 self.data = f.read()
176 f.close()
177 self.headers = headers
178
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500179 def request(self, uri,
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500180 method='GET',
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500181 body=None,
182 headers=None,
183 redirections=1,
184 connection_type=None):
Joe Gregoriocb8103d2011-02-11 23:20:52 -0500185 return httplib2.Response(self.headers), self.data
Joe Gregorioccc79542011-02-19 00:05:26 -0500186
187
188class HttpMockSequence(object):
189 """Mock of httplib2.Http
190
191 Mocks a sequence of calls to request returning different responses for each
192 call. Create an instance initialized with the desired response headers
193 and content and then use as if an httplib2.Http instance.
194
195 http = HttpMockSequence([
196 ({'status': '401'}, ''),
197 ({'status': '200'}, '{"access_token":"1/3w","expires_in":3600}'),
198 ({'status': '200'}, 'echo_request_headers'),
199 ])
200 resp, content = http.request("http://examples.com")
201
202 There are special values you can pass in for content to trigger
203 behavours that are helpful in testing.
204
205 'echo_request_headers' means return the request headers in the response body
Joe Gregorioe9e236f2011-03-21 22:23:14 -0400206 'echo_request_headers_as_json' means return the request headers in
207 the response body
Joe Gregorioccc79542011-02-19 00:05:26 -0500208 'echo_request_body' means return the request body in the response body
209 """
210
211 def __init__(self, iterable):
212 """
213 Args:
214 iterable: iterable, a sequence of pairs of (headers, body)
215 """
216 self._iterable = iterable
217
218 def request(self, uri,
219 method='GET',
220 body=None,
221 headers=None,
222 redirections=1,
223 connection_type=None):
224 resp, content = self._iterable.pop(0)
225 if content == 'echo_request_headers':
226 content = headers
Joe Gregoriof4153422011-03-18 22:45:18 -0400227 elif content == 'echo_request_headers_as_json':
228 content = simplejson.dumps(headers)
Joe Gregorioccc79542011-02-19 00:05:26 -0500229 elif content == 'echo_request_body':
230 content = body
231 return httplib2.Response(resp), content
Joe Gregorio6bcbcea2011-03-10 15:26:05 -0500232
233
234def set_user_agent(http, user_agent):
Joe Gregoriof4153422011-03-18 22:45:18 -0400235 """Set the user-agent on every request.
236
Joe Gregorio6bcbcea2011-03-10 15:26:05 -0500237 Args:
238 http - An instance of httplib2.Http
239 or something that acts like it.
240 user_agent: string, the value for the user-agent header.
241
242 Returns:
243 A modified instance of http that was passed in.
244
245 Example:
246
247 h = httplib2.Http()
248 h = set_user_agent(h, "my-app-name/6.0")
249
250 Most of the time the user-agent will be set doing auth, this is for the rare
251 cases where you are accessing an unauthenticated endpoint.
252 """
253 request_orig = http.request
254
255 # The closure that will replace 'httplib2.Http.request'.
256 def new_request(uri, method='GET', body=None, headers=None,
257 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
258 connection_type=None):
259 """Modify the request headers to add the user-agent."""
260 if headers is None:
261 headers = {}
262 if 'user-agent' in headers:
263 headers['user-agent'] = user_agent + ' ' + headers['user-agent']
264 else:
265 headers['user-agent'] = user_agent
266 resp, content = request_orig(uri, method, body, headers,
267 redirections, connection_type)
268 return resp, content
269
270 http.request = new_request
271 return http
Joe Gregoriof4153422011-03-18 22:45:18 -0400272
273
274def tunnel_patch(http):
275 """Tunnel PATCH requests over POST.
276 Args:
277 http - An instance of httplib2.Http
278 or something that acts like it.
279
280 Returns:
281 A modified instance of http that was passed in.
282
283 Example:
284
285 h = httplib2.Http()
286 h = tunnel_patch(h, "my-app-name/6.0")
287
288 Useful if you are running on a platform that doesn't support PATCH.
289 Apply this last if you are using OAuth 1.0, as changing the method
290 will result in a different signature.
291 """
292 request_orig = http.request
293
294 # The closure that will replace 'httplib2.Http.request'.
295 def new_request(uri, method='GET', body=None, headers=None,
296 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
297 connection_type=None):
298 """Modify the request headers to add the user-agent."""
299 if headers is None:
300 headers = {}
301 if method == 'PATCH':
302 if 'authorization' in headers and 'oauth_token' in headers['authorization']:
Joe Gregorioe9e236f2011-03-21 22:23:14 -0400303 logging.warning(
304 'OAuth 1.0 request made with Credentials applied after tunnel_patch.')
Joe Gregoriof4153422011-03-18 22:45:18 -0400305 headers['x-http-method-override'] = "PATCH"
306 method = 'POST'
307 resp, content = request_orig(uri, method, body, headers,
308 redirections, connection_type)
309 return resp, content
310
311 http.request = new_request
312 return http