blob: f4627bdb020d26449a5d0550abff48abce1e97de [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 Gregorioaf276d22010-12-09 14:26:58 -050013 ]
14
Joe Gregorioc6722462010-12-20 14:29:28 -050015import httplib2
Joe Gregoriocb8103d2011-02-11 23:20:52 -050016import os
17
Joe Gregorio89174d22010-12-20 14:37:36 -050018from model import JsonModel
Joe Gregorio49396552011-03-08 10:39:00 -050019from errors import HttpError
Joe Gregorioc5c5a372010-09-22 11:42:32 -040020
21
22class HttpRequest(object):
Joe Gregorioaf276d22010-12-09 14:26:58 -050023 """Encapsulates a single HTTP request.
Joe Gregorioc5c5a372010-09-22 11:42:32 -040024 """
25
Joe Gregoriodeeb0202011-02-15 14:49:57 -050026 def __init__(self, http, postproc, uri,
Joe Gregorio7c22ab22011-02-16 15:32:39 -050027 method='GET',
Joe Gregoriodeeb0202011-02-15 14:49:57 -050028 body=None,
29 headers=None,
Joe Gregorioabda96f2011-02-11 20:19:33 -050030 methodId=None):
Joe Gregorioaf276d22010-12-09 14:26:58 -050031 """Constructor for an HttpRequest.
32
Joe Gregorioaf276d22010-12-09 14:26:58 -050033 Args:
34 http: httplib2.Http, the transport object to use to make a request
Joe Gregorioabda96f2011-02-11 20:19:33 -050035 postproc: callable, called on the HTTP response and content to transform
36 it into a data object before returning, or raising an exception
37 on an error.
Joe Gregorioaf276d22010-12-09 14:26:58 -050038 uri: string, the absolute URI to send the request to
39 method: string, the HTTP method to use
40 body: string, the request body of the HTTP request
41 headers: dict, the HTTP request headers
Joe Gregorioaf276d22010-12-09 14:26:58 -050042 methodId: string, a unique identifier for the API method being called.
43 """
Joe Gregorioc5c5a372010-09-22 11:42:32 -040044 self.uri = uri
45 self.method = method
46 self.body = body
47 self.headers = headers or {}
48 self.http = http
49 self.postproc = postproc
50
51 def execute(self, http=None):
52 """Execute the request.
53
Joe Gregorioaf276d22010-12-09 14:26:58 -050054 Args:
55 http: httplib2.Http, an http object to be used in place of the
56 one the HttpRequest request object was constructed with.
57
58 Returns:
59 A deserialized object model of the response body as determined
60 by the postproc.
61
62 Raises:
63 apiclient.errors.HttpError if the response was not a 2xx.
64 httplib2.Error if a transport error has occured.
Joe Gregorioc5c5a372010-09-22 11:42:32 -040065 """
66 if http is None:
67 http = self.http
68 resp, content = http.request(self.uri, self.method,
69 body=self.body,
70 headers=self.headers)
Joe Gregorio49396552011-03-08 10:39:00 -050071
72 if resp.status >= 300:
73 raise HttpError(resp, content, self.uri)
Joe Gregorioc5c5a372010-09-22 11:42:32 -040074 return self.postproc(resp, content)
Joe Gregorioaf276d22010-12-09 14:26:58 -050075
76
77class HttpRequestMock(object):
78 """Mock of HttpRequest.
79
80 Do not construct directly, instead use RequestMockBuilder.
81 """
82
83 def __init__(self, resp, content, postproc):
84 """Constructor for HttpRequestMock
85
86 Args:
87 resp: httplib2.Response, the response to emulate coming from the request
88 content: string, the response body
89 postproc: callable, the post processing function usually supplied by
90 the model class. See model.JsonModel.response() as an example.
91 """
92 self.resp = resp
93 self.content = content
94 self.postproc = postproc
95 if resp is None:
Joe Gregorioc6722462010-12-20 14:29:28 -050096 self.resp = httplib2.Response({'status': 200, 'reason': 'OK'})
Joe Gregorioaf276d22010-12-09 14:26:58 -050097 if 'reason' in self.resp:
98 self.resp.reason = self.resp['reason']
99
100 def execute(self, http=None):
101 """Execute the request.
102
103 Same behavior as HttpRequest.execute(), but the response is
104 mocked and not really from an HTTP request/response.
105 """
106 return self.postproc(self.resp, self.content)
107
108
109class RequestMockBuilder(object):
110 """A simple mock of HttpRequest
111
112 Pass in a dictionary to the constructor that maps request methodIds to
113 tuples of (httplib2.Response, content) that should be returned when that
114 method is called. None may also be passed in for the httplib2.Response, in
115 which case a 200 OK response will be generated.
116
117 Example:
118 response = '{"data": {"id": "tag:google.c...'
119 requestBuilder = RequestMockBuilder(
120 {
121 'chili.activities.get': (None, response),
122 }
123 )
124 apiclient.discovery.build("buzz", "v1", requestBuilder=requestBuilder)
125
126 Methods that you do not supply a response for will return a
127 200 OK with an empty string as the response content. The methodId
128 is taken from the rpcName in the discovery document.
129
130 For more details see the project wiki.
131 """
132
133 def __init__(self, responses):
134 """Constructor for RequestMockBuilder
135
136 The constructed object should be a callable object
137 that can replace the class HttpResponse.
138
139 responses - A dictionary that maps methodIds into tuples
140 of (httplib2.Response, content). The methodId
141 comes from the 'rpcName' field in the discovery
142 document.
143 """
144 self.responses = responses
145
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500146 def __call__(self, http, postproc, uri, method='GET', body=None,
Joe Gregorioabda96f2011-02-11 20:19:33 -0500147 headers=None, methodId=None):
Joe Gregorioaf276d22010-12-09 14:26:58 -0500148 """Implements the callable interface that discovery.build() expects
149 of requestBuilder, which is to build an object compatible with
150 HttpRequest.execute(). See that method for the description of the
151 parameters and the expected response.
152 """
153 if methodId in self.responses:
154 resp, content = self.responses[methodId]
155 return HttpRequestMock(resp, content, postproc)
156 else:
Joe Gregoriod433b2a2011-02-22 10:51:51 -0500157 model = JsonModel(False)
Joe Gregorioaf276d22010-12-09 14:26:58 -0500158 return HttpRequestMock(None, '{}', model.response)
Joe Gregoriocb8103d2011-02-11 23:20:52 -0500159
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500160
Joe Gregoriocb8103d2011-02-11 23:20:52 -0500161class HttpMock(object):
162 """Mock of httplib2.Http"""
163
Joe Gregorioec343652011-02-16 16:52:51 -0500164 def __init__(self, filename, headers=None):
Joe Gregoriocb8103d2011-02-11 23:20:52 -0500165 """
166 Args:
167 filename: string, absolute filename to read response from
168 headers: dict, header to return with response
169 """
Joe Gregorioec343652011-02-16 16:52:51 -0500170 if headers is None:
171 headers = {'status': '200 OK'}
Joe Gregoriocb8103d2011-02-11 23:20:52 -0500172 f = file(filename, 'r')
173 self.data = f.read()
174 f.close()
175 self.headers = headers
176
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500177 def request(self, uri,
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500178 method='GET',
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500179 body=None,
180 headers=None,
181 redirections=1,
182 connection_type=None):
Joe Gregoriocb8103d2011-02-11 23:20:52 -0500183 return httplib2.Response(self.headers), self.data
Joe Gregorioccc79542011-02-19 00:05:26 -0500184
185
186class HttpMockSequence(object):
187 """Mock of httplib2.Http
188
189 Mocks a sequence of calls to request returning different responses for each
190 call. Create an instance initialized with the desired response headers
191 and content and then use as if an httplib2.Http instance.
192
193 http = HttpMockSequence([
194 ({'status': '401'}, ''),
195 ({'status': '200'}, '{"access_token":"1/3w","expires_in":3600}'),
196 ({'status': '200'}, 'echo_request_headers'),
197 ])
198 resp, content = http.request("http://examples.com")
199
200 There are special values you can pass in for content to trigger
201 behavours that are helpful in testing.
202
203 'echo_request_headers' means return the request headers in the response body
204 'echo_request_body' means return the request body in the response body
205 """
206
207 def __init__(self, iterable):
208 """
209 Args:
210 iterable: iterable, a sequence of pairs of (headers, body)
211 """
212 self._iterable = iterable
213
214 def request(self, uri,
215 method='GET',
216 body=None,
217 headers=None,
218 redirections=1,
219 connection_type=None):
220 resp, content = self._iterable.pop(0)
221 if content == 'echo_request_headers':
222 content = headers
223 elif content == 'echo_request_body':
224 content = body
225 return httplib2.Response(resp), content
Joe Gregorio6bcbcea2011-03-10 15:26:05 -0500226
227
228def set_user_agent(http, user_agent):
229 """
230 Args:
231 http - An instance of httplib2.Http
232 or something that acts like it.
233 user_agent: string, the value for the user-agent header.
234
235 Returns:
236 A modified instance of http that was passed in.
237
238 Example:
239
240 h = httplib2.Http()
241 h = set_user_agent(h, "my-app-name/6.0")
242
243 Most of the time the user-agent will be set doing auth, this is for the rare
244 cases where you are accessing an unauthenticated endpoint.
245 """
246 request_orig = http.request
247
248 # The closure that will replace 'httplib2.Http.request'.
249 def new_request(uri, method='GET', body=None, headers=None,
250 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
251 connection_type=None):
252 """Modify the request headers to add the user-agent."""
253 if headers is None:
254 headers = {}
255 if 'user-agent' in headers:
256 headers['user-agent'] = user_agent + ' ' + headers['user-agent']
257 else:
258 headers['user-agent'] = user_agent
259 resp, content = request_orig(uri, method, body, headers,
260 redirections, connection_type)
261 return resp, content
262
263 http.request = new_request
264 return http