blob: 206f3e68ee7dbd3f54ea779a1530d7f61f211da4 [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 Gregoriof4153422011-03-18 22:45:18 -0400206 'echo_request_headers_as_json' means return the request headers in the response body
Joe Gregorioccc79542011-02-19 00:05:26 -0500207 'echo_request_body' means return the request body in the response body
208 """
209
210 def __init__(self, iterable):
211 """
212 Args:
213 iterable: iterable, a sequence of pairs of (headers, body)
214 """
215 self._iterable = iterable
216
217 def request(self, uri,
218 method='GET',
219 body=None,
220 headers=None,
221 redirections=1,
222 connection_type=None):
223 resp, content = self._iterable.pop(0)
224 if content == 'echo_request_headers':
225 content = headers
Joe Gregoriof4153422011-03-18 22:45:18 -0400226 elif content == 'echo_request_headers_as_json':
227 content = simplejson.dumps(headers)
Joe Gregorioccc79542011-02-19 00:05:26 -0500228 elif content == 'echo_request_body':
229 content = body
230 return httplib2.Response(resp), content
Joe Gregorio6bcbcea2011-03-10 15:26:05 -0500231
232
233def set_user_agent(http, user_agent):
Joe Gregoriof4153422011-03-18 22:45:18 -0400234 """Set the user-agent on every request.
235
Joe Gregorio6bcbcea2011-03-10 15:26:05 -0500236 Args:
237 http - An instance of httplib2.Http
238 or something that acts like it.
239 user_agent: string, the value for the user-agent header.
240
241 Returns:
242 A modified instance of http that was passed in.
243
244 Example:
245
246 h = httplib2.Http()
247 h = set_user_agent(h, "my-app-name/6.0")
248
249 Most of the time the user-agent will be set doing auth, this is for the rare
250 cases where you are accessing an unauthenticated endpoint.
251 """
252 request_orig = http.request
253
254 # The closure that will replace 'httplib2.Http.request'.
255 def new_request(uri, method='GET', body=None, headers=None,
256 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
257 connection_type=None):
258 """Modify the request headers to add the user-agent."""
259 if headers is None:
260 headers = {}
261 if 'user-agent' in headers:
262 headers['user-agent'] = user_agent + ' ' + headers['user-agent']
263 else:
264 headers['user-agent'] = user_agent
265 resp, content = request_orig(uri, method, body, headers,
266 redirections, connection_type)
267 return resp, content
268
269 http.request = new_request
270 return http
Joe Gregoriof4153422011-03-18 22:45:18 -0400271
272
273def tunnel_patch(http):
274 """Tunnel PATCH requests over POST.
275 Args:
276 http - An instance of httplib2.Http
277 or something that acts like it.
278
279 Returns:
280 A modified instance of http that was passed in.
281
282 Example:
283
284 h = httplib2.Http()
285 h = tunnel_patch(h, "my-app-name/6.0")
286
287 Useful if you are running on a platform that doesn't support PATCH.
288 Apply this last if you are using OAuth 1.0, as changing the method
289 will result in a different signature.
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 method == 'PATCH':
301 if 'authorization' in headers and 'oauth_token' in headers['authorization']:
302 logging.warning('OAuth 1.0 request made with Credentials applied after tunnel_patch.')
303 headers['x-http-method-override'] = "PATCH"
304 method = 'POST'
305 resp, content = request_orig(uri, method, body, headers,
306 redirections, connection_type)
307 return resp, content
308
309 http.request = new_request
310 return http