blob: 0b45a446f6d5636d66d05baadfd7fd6d3449dfb0 [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 Gregoriod0bd3882011-11-22 09:49:47 -050028import copy
Joe Gregorioc6722462010-12-20 14:29:28 -050029import httplib2
Joe Gregoriocb8103d2011-02-11 23:20:52 -050030import os
Joe Gregoriod0bd3882011-11-22 09:49:47 -050031import mimeparse
32import mimetypes
Joe Gregoriocb8103d2011-02-11 23:20:52 -050033
Joe Gregorio89174d22010-12-20 14:37:36 -050034from model import JsonModel
Joe Gregorio49396552011-03-08 10:39:00 -050035from errors import HttpError
Joe Gregoriod0bd3882011-11-22 09:49:47 -050036from errors import ResumableUploadError
Joe Gregorioa388ce32011-09-09 17:19:13 -040037from errors import UnexpectedBodyError
38from errors import UnexpectedMethodError
Joe Gregoriof4153422011-03-18 22:45:18 -040039from anyjson import simplejson
Joe Gregorioc5c5a372010-09-22 11:42:32 -040040
41
Joe Gregoriod0bd3882011-11-22 09:49:47 -050042class MediaUploadProgress(object):
43 """Status of a resumable upload."""
44
45 def __init__(self, resumable_progress, total_size):
46 """Constructor.
47
48 Args:
49 resumable_progress: int, bytes sent so far.
50 total_size: int, total bytes in complete upload.
51 """
52 self.resumable_progress = resumable_progress
53 self.total_size = total_size
54
55 def progress(self):
56 """Percent of upload completed, as a float."""
57 return float(self.resumable_progress)/float(self.total_size)
58
59
60class MediaUpload(object):
61 """Describes a media object to upload.
62
63 Base class that defines the interface of MediaUpload subclasses.
64 """
65
66 def getbytes(self, begin, end):
67 raise NotImplementedError()
68
69 def size(self):
70 raise NotImplementedError()
71
72 def chunksize(self):
73 raise NotImplementedError()
74
75 def mimetype(self):
76 return 'application/octet-stream'
77
78 def resumable(self):
79 return False
80
81 def _to_json(self, strip=None):
82 """Utility function for creating a JSON representation of a MediaUpload.
83
84 Args:
85 strip: array, An array of names of members to not include in the JSON.
86
87 Returns:
88 string, a JSON representation of this instance, suitable to pass to
89 from_json().
90 """
91 t = type(self)
92 d = copy.copy(self.__dict__)
93 if strip is not None:
94 for member in strip:
95 del d[member]
96 d['_class'] = t.__name__
97 d['_module'] = t.__module__
98 return simplejson.dumps(d)
99
100 def to_json(self):
101 """Create a JSON representation of an instance of MediaUpload.
102
103 Returns:
104 string, a JSON representation of this instance, suitable to pass to
105 from_json().
106 """
107 return self._to_json()
108
109 @classmethod
110 def new_from_json(cls, s):
111 """Utility class method to instantiate a MediaUpload subclass from a JSON
112 representation produced by to_json().
113
114 Args:
115 s: string, JSON from to_json().
116
117 Returns:
118 An instance of the subclass of MediaUpload that was serialized with
119 to_json().
120 """
121 data = simplejson.loads(s)
122 # Find and call the right classmethod from_json() to restore the object.
123 module = data['_module']
124 m = __import__(module, fromlist=module.split('.')[:-1])
125 kls = getattr(m, data['_class'])
126 from_json = getattr(kls, 'from_json')
127 return from_json(s)
128
129class MediaFileUpload(MediaUpload):
130 """A MediaUpload for a file.
131
132 Construct a MediaFileUpload and pass as the media_body parameter of the
133 method. For example, if we had a service that allowed uploading images:
134
135
136 media = MediaFileUpload('smiley.png', mimetype='image/png', chunksize=1000,
137 resumable=True)
138 service.objects().insert(
139 bucket=buckets['items'][0]['id'],
140 name='smiley.png',
141 media_body=media).execute()
142 """
143
144 def __init__(self, filename, mimetype=None, chunksize=10000, resumable=False):
145 """Constructor.
146
147 Args:
148 filename: string, Name of the file.
149 mimetype: string, Mime-type of the file. If None then a mime-type will be
150 guessed from the file extension.
151 chunksize: int, File will be uploaded in chunks of this many bytes. Only
152 used if resumable=True.
153 resumable: bool, True if this is a resumable upload. False means upload in
154 a single request.
155 """
156 self._filename = filename
157 self._size = os.path.getsize(filename)
158 self._fd = None
159 if mimetype is None:
160 (mimetype, encoding) = mimetypes.guess_type(filename)
161 self._mimetype = mimetype
162 self._chunksize = chunksize
163 self._resumable = resumable
164
165 def mimetype(self):
166 return self._mimetype
167
168 def size(self):
169 return self._size
170
171 def chunksize(self):
172 return self._chunksize
173
174 def resumable(self):
175 return self._resumable
176
177 def getbytes(self, begin, length):
178 """Get bytes from the media.
179
180 Args:
181 begin: int, offset from beginning of file.
182 length: int, number of bytes to read, starting at begin.
183
184 Returns:
185 A string of bytes read. May be shorted than length if EOF was reached
186 first.
187 """
188 if self._fd is None:
189 self._fd = open(self._filename, 'rb')
190 self._fd.seek(begin)
191 return self._fd.read(length)
192
193 def to_json(self):
194 """Creating a JSON representation of an instance of Credentials.
195
196 Returns:
197 string, a JSON representation of this instance, suitable to pass to
198 from_json().
199 """
200 return self._to_json(['_fd'])
201
202 @staticmethod
203 def from_json(s):
204 d = simplejson.loads(s)
205 return MediaFileUpload(
206 d['_filename'], d['_mimetype'], d['_chunksize'], d['_resumable'])
207
208
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400209class HttpRequest(object):
Joe Gregorioaf276d22010-12-09 14:26:58 -0500210 """Encapsulates a single HTTP request.
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400211 """
212
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500213 def __init__(self, http, postproc, uri,
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500214 method='GET',
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500215 body=None,
216 headers=None,
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500217 methodId=None,
218 resumable=None):
Joe Gregorioaf276d22010-12-09 14:26:58 -0500219 """Constructor for an HttpRequest.
220
Joe Gregorioaf276d22010-12-09 14:26:58 -0500221 Args:
222 http: httplib2.Http, the transport object to use to make a request
Joe Gregorioabda96f2011-02-11 20:19:33 -0500223 postproc: callable, called on the HTTP response and content to transform
224 it into a data object before returning, or raising an exception
225 on an error.
Joe Gregorioaf276d22010-12-09 14:26:58 -0500226 uri: string, the absolute URI to send the request to
227 method: string, the HTTP method to use
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500228 body: string, the request body of the HTTP request,
Joe Gregorioaf276d22010-12-09 14:26:58 -0500229 headers: dict, the HTTP request headers
Joe Gregorioaf276d22010-12-09 14:26:58 -0500230 methodId: string, a unique identifier for the API method being called.
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500231 resumable: MediaUpload, None if this is not a resumbale request.
Joe Gregorioaf276d22010-12-09 14:26:58 -0500232 """
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400233 self.uri = uri
234 self.method = method
235 self.body = body
236 self.headers = headers or {}
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500237 self.methodId = methodId
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400238 self.http = http
239 self.postproc = postproc
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500240 self.resumable = resumable
241
242 major, minor, params = mimeparse.parse_mime_type(
243 headers.get('content-type', 'application/json'))
244 self.multipart_boundary = params.get('boundary', '').strip('"')
245
246 # If this was a multipart resumable, the size of the non-media part.
247 self.multipart_size = 0
248
249 # The resumable URI to send chunks to.
250 self.resumable_uri = None
251
252 # The bytes that have been uploaded.
253 self.resumable_progress = 0
254
255 if resumable is not None:
256 if self.body is not None:
257 self.multipart_size = len(self.body)
258 else:
259 self.multipart_size = 0
260 self.total_size = self.resumable.size() + self.multipart_size + len(self.multipart_boundary)
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400261
262 def execute(self, http=None):
263 """Execute the request.
264
Joe Gregorioaf276d22010-12-09 14:26:58 -0500265 Args:
266 http: httplib2.Http, an http object to be used in place of the
267 one the HttpRequest request object was constructed with.
268
269 Returns:
270 A deserialized object model of the response body as determined
271 by the postproc.
272
273 Raises:
274 apiclient.errors.HttpError if the response was not a 2xx.
275 httplib2.Error if a transport error has occured.
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400276 """
277 if http is None:
278 http = self.http
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500279 if self.resumable:
280 body = None
281 while body is None:
282 _, body = self.next_chunk(http)
283 return body
284 else:
285 resp, content = http.request(self.uri, self.method,
286 body=self.body,
287 headers=self.headers)
Joe Gregorio49396552011-03-08 10:39:00 -0500288
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500289 if resp.status >= 300:
290 raise HttpError(resp, content, self.uri)
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400291 return self.postproc(resp, content)
Joe Gregorioaf276d22010-12-09 14:26:58 -0500292
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500293 def next_chunk(self, http=None):
294 """Execute the next step of a resumable upload.
295
296 Can only be used if the method being executed supports media uploads and the
297 MediaUpload object passed in was flagged as using resumable upload.
298
299 Example:
300
301 media = MediaFileUpload('smiley.png', mimetype='image/png', chunksize=1000,
302 resumable=True)
303 request = service.objects().insert(
304 bucket=buckets['items'][0]['id'],
305 name='smiley.png',
306 media_body=media)
307
308 response = None
309 while response is None:
310 status, response = request.next_chunk()
311 if status:
312 print "Upload %d%% complete." % int(status.progress() * 100)
313
314
315 Returns:
316 (status, body): (ResumableMediaStatus, object)
317 The body will be None until the resumable media is fully uploaded.
318 """
319 if http is None:
320 http = self.http
321
322 if self.resumable_uri is None:
323 start_headers = copy.copy(self.headers)
324 start_headers['X-Upload-Content-Type'] = self.resumable.mimetype()
325 start_headers['X-Upload-Content-Length'] = str(self.resumable.size())
326 start_headers['Content-Length'] = '0'
327 resp, content = http.request(self.uri, self.method,
328 body="",
329 headers=start_headers)
330 if resp.status == 200 and 'location' in resp:
331 self.resumable_uri = resp['location']
332 else:
333 raise ResumableUploadError("Failed to retrieve starting URI.")
334 if self.body:
335 begin = 0
336 data = self.body
337 else:
338 begin = self.resumable_progress - self.multipart_size
339 data = self.resumable.getbytes(begin, self.resumable.chunksize())
340
341 # Tack on the multipart/related boundary if we are at the end of the file.
342 if begin + self.resumable.chunksize() >= self.resumable.size():
343 data += self.multipart_boundary
344 headers = {
345 'Content-Range': 'bytes %d-%d/%d' % (
346 self.resumable_progress, self.resumable_progress + len(data) - 1,
347 self.total_size),
348 }
349 resp, content = http.request(self.resumable_uri, 'PUT',
350 body=data,
351 headers=headers)
352 if resp.status in [200, 201]:
353 return None, self.postproc(resp, content)
354 # A "308 Resume Incomplete" indicates we are not done.
355 elif resp.status == 308:
356 self.resumable_progress = int(resp['range'].split('-')[1]) + 1
357 if self.resumable_progress >= self.multipart_size:
358 self.body = None
359 if 'location' in resp:
360 self.resumable_uri = resp['location']
361 else:
362 raise HttpError(resp, content, self.uri)
363
364 return MediaUploadProgress(self.resumable_progress, self.total_size), None
365
366 def to_json(self):
367 """Returns a JSON representation of the HttpRequest."""
368 d = copy.copy(self.__dict__)
369 if d['resumable'] is not None:
370 d['resumable'] = self.resumable.to_json()
371 del d['http']
372 del d['postproc']
373 return simplejson.dumps(d)
374
375 @staticmethod
376 def from_json(s, http, postproc):
377 """Returns an HttpRequest populated with info from a JSON object."""
378 d = simplejson.loads(s)
379 if d['resumable'] is not None:
380 d['resumable'] = MediaUpload.new_from_json(d['resumable'])
381 return HttpRequest(
382 http,
383 postproc,
384 uri = d['uri'],
385 method= d['method'],
386 body=d['body'],
387 headers=d['headers'],
388 methodId=d['methodId'],
389 resumable=d['resumable'])
390
Joe Gregorioaf276d22010-12-09 14:26:58 -0500391
392class HttpRequestMock(object):
393 """Mock of HttpRequest.
394
395 Do not construct directly, instead use RequestMockBuilder.
396 """
397
398 def __init__(self, resp, content, postproc):
399 """Constructor for HttpRequestMock
400
401 Args:
402 resp: httplib2.Response, the response to emulate coming from the request
403 content: string, the response body
404 postproc: callable, the post processing function usually supplied by
405 the model class. See model.JsonModel.response() as an example.
406 """
407 self.resp = resp
408 self.content = content
409 self.postproc = postproc
410 if resp is None:
Joe Gregorioc6722462010-12-20 14:29:28 -0500411 self.resp = httplib2.Response({'status': 200, 'reason': 'OK'})
Joe Gregorioaf276d22010-12-09 14:26:58 -0500412 if 'reason' in self.resp:
413 self.resp.reason = self.resp['reason']
414
415 def execute(self, http=None):
416 """Execute the request.
417
418 Same behavior as HttpRequest.execute(), but the response is
419 mocked and not really from an HTTP request/response.
420 """
421 return self.postproc(self.resp, self.content)
422
423
424class RequestMockBuilder(object):
425 """A simple mock of HttpRequest
426
427 Pass in a dictionary to the constructor that maps request methodIds to
Joe Gregorioa388ce32011-09-09 17:19:13 -0400428 tuples of (httplib2.Response, content, opt_expected_body) that should be
429 returned when that method is called. None may also be passed in for the
430 httplib2.Response, in which case a 200 OK response will be generated.
431 If an opt_expected_body (str or dict) is provided, it will be compared to
432 the body and UnexpectedBodyError will be raised on inequality.
Joe Gregorioaf276d22010-12-09 14:26:58 -0500433
434 Example:
435 response = '{"data": {"id": "tag:google.c...'
436 requestBuilder = RequestMockBuilder(
437 {
Joe Gregorioc4fc0952011-11-09 12:21:11 -0500438 'plus.activities.get': (None, response),
Joe Gregorioaf276d22010-12-09 14:26:58 -0500439 }
440 )
Joe Gregorioc4fc0952011-11-09 12:21:11 -0500441 apiclient.discovery.build("plus", "v1", requestBuilder=requestBuilder)
Joe Gregorioaf276d22010-12-09 14:26:58 -0500442
443 Methods that you do not supply a response for will return a
Joe Gregorioa388ce32011-09-09 17:19:13 -0400444 200 OK with an empty string as the response content or raise an excpetion if
445 check_unexpected is set to True. The methodId is taken from the rpcName
446 in the discovery document.
Joe Gregorioaf276d22010-12-09 14:26:58 -0500447
448 For more details see the project wiki.
449 """
450
Joe Gregorioa388ce32011-09-09 17:19:13 -0400451 def __init__(self, responses, check_unexpected=False):
Joe Gregorioaf276d22010-12-09 14:26:58 -0500452 """Constructor for RequestMockBuilder
453
454 The constructed object should be a callable object
455 that can replace the class HttpResponse.
456
457 responses - A dictionary that maps methodIds into tuples
458 of (httplib2.Response, content). The methodId
459 comes from the 'rpcName' field in the discovery
460 document.
Joe Gregorioa388ce32011-09-09 17:19:13 -0400461 check_unexpected - A boolean setting whether or not UnexpectedMethodError
462 should be raised on unsupplied method.
Joe Gregorioaf276d22010-12-09 14:26:58 -0500463 """
464 self.responses = responses
Joe Gregorioa388ce32011-09-09 17:19:13 -0400465 self.check_unexpected = check_unexpected
Joe Gregorioaf276d22010-12-09 14:26:58 -0500466
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500467 def __call__(self, http, postproc, uri, method='GET', body=None,
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500468 headers=None, methodId=None, resumable=None):
Joe Gregorioaf276d22010-12-09 14:26:58 -0500469 """Implements the callable interface that discovery.build() expects
470 of requestBuilder, which is to build an object compatible with
471 HttpRequest.execute(). See that method for the description of the
472 parameters and the expected response.
473 """
474 if methodId in self.responses:
Joe Gregorioa388ce32011-09-09 17:19:13 -0400475 response = self.responses[methodId]
476 resp, content = response[:2]
477 if len(response) > 2:
478 # Test the body against the supplied expected_body.
479 expected_body = response[2]
480 if bool(expected_body) != bool(body):
481 # Not expecting a body and provided one
482 # or expecting a body and not provided one.
483 raise UnexpectedBodyError(expected_body, body)
484 if isinstance(expected_body, str):
485 expected_body = simplejson.loads(expected_body)
486 body = simplejson.loads(body)
487 if body != expected_body:
488 raise UnexpectedBodyError(expected_body, body)
Joe Gregorioaf276d22010-12-09 14:26:58 -0500489 return HttpRequestMock(resp, content, postproc)
Joe Gregorioa388ce32011-09-09 17:19:13 -0400490 elif self.check_unexpected:
491 raise UnexpectedMethodError(methodId)
Joe Gregorioaf276d22010-12-09 14:26:58 -0500492 else:
Joe Gregoriod433b2a2011-02-22 10:51:51 -0500493 model = JsonModel(False)
Joe Gregorioaf276d22010-12-09 14:26:58 -0500494 return HttpRequestMock(None, '{}', model.response)
Joe Gregoriocb8103d2011-02-11 23:20:52 -0500495
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500496
Joe Gregoriocb8103d2011-02-11 23:20:52 -0500497class HttpMock(object):
498 """Mock of httplib2.Http"""
499
Joe Gregorioec343652011-02-16 16:52:51 -0500500 def __init__(self, filename, headers=None):
Joe Gregoriocb8103d2011-02-11 23:20:52 -0500501 """
502 Args:
503 filename: string, absolute filename to read response from
504 headers: dict, header to return with response
505 """
Joe Gregorioec343652011-02-16 16:52:51 -0500506 if headers is None:
507 headers = {'status': '200 OK'}
Joe Gregoriocb8103d2011-02-11 23:20:52 -0500508 f = file(filename, 'r')
509 self.data = f.read()
510 f.close()
511 self.headers = headers
512
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500513 def request(self, uri,
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500514 method='GET',
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500515 body=None,
516 headers=None,
517 redirections=1,
518 connection_type=None):
Joe Gregoriocb8103d2011-02-11 23:20:52 -0500519 return httplib2.Response(self.headers), self.data
Joe Gregorioccc79542011-02-19 00:05:26 -0500520
521
522class HttpMockSequence(object):
523 """Mock of httplib2.Http
524
525 Mocks a sequence of calls to request returning different responses for each
526 call. Create an instance initialized with the desired response headers
527 and content and then use as if an httplib2.Http instance.
528
529 http = HttpMockSequence([
530 ({'status': '401'}, ''),
531 ({'status': '200'}, '{"access_token":"1/3w","expires_in":3600}'),
532 ({'status': '200'}, 'echo_request_headers'),
533 ])
534 resp, content = http.request("http://examples.com")
535
536 There are special values you can pass in for content to trigger
537 behavours that are helpful in testing.
538
539 'echo_request_headers' means return the request headers in the response body
Joe Gregorioe9e236f2011-03-21 22:23:14 -0400540 'echo_request_headers_as_json' means return the request headers in
541 the response body
Joe Gregorioccc79542011-02-19 00:05:26 -0500542 'echo_request_body' means return the request body in the response body
Joe Gregorio0bc70912011-05-24 15:30:49 -0400543 'echo_request_uri' means return the request uri in the response body
Joe Gregorioccc79542011-02-19 00:05:26 -0500544 """
545
546 def __init__(self, iterable):
547 """
548 Args:
549 iterable: iterable, a sequence of pairs of (headers, body)
550 """
551 self._iterable = iterable
552
553 def request(self, uri,
554 method='GET',
555 body=None,
556 headers=None,
557 redirections=1,
558 connection_type=None):
559 resp, content = self._iterable.pop(0)
560 if content == 'echo_request_headers':
561 content = headers
Joe Gregoriof4153422011-03-18 22:45:18 -0400562 elif content == 'echo_request_headers_as_json':
563 content = simplejson.dumps(headers)
Joe Gregorioccc79542011-02-19 00:05:26 -0500564 elif content == 'echo_request_body':
565 content = body
Joe Gregorio0bc70912011-05-24 15:30:49 -0400566 elif content == 'echo_request_uri':
567 content = uri
Joe Gregorioccc79542011-02-19 00:05:26 -0500568 return httplib2.Response(resp), content
Joe Gregorio6bcbcea2011-03-10 15:26:05 -0500569
570
571def set_user_agent(http, user_agent):
Joe Gregoriof4153422011-03-18 22:45:18 -0400572 """Set the user-agent on every request.
573
Joe Gregorio6bcbcea2011-03-10 15:26:05 -0500574 Args:
575 http - An instance of httplib2.Http
576 or something that acts like it.
577 user_agent: string, the value for the user-agent header.
578
579 Returns:
580 A modified instance of http that was passed in.
581
582 Example:
583
584 h = httplib2.Http()
585 h = set_user_agent(h, "my-app-name/6.0")
586
587 Most of the time the user-agent will be set doing auth, this is for the rare
588 cases where you are accessing an unauthenticated endpoint.
589 """
590 request_orig = http.request
591
592 # The closure that will replace 'httplib2.Http.request'.
593 def new_request(uri, method='GET', body=None, headers=None,
594 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
595 connection_type=None):
596 """Modify the request headers to add the user-agent."""
597 if headers is None:
598 headers = {}
599 if 'user-agent' in headers:
600 headers['user-agent'] = user_agent + ' ' + headers['user-agent']
601 else:
602 headers['user-agent'] = user_agent
603 resp, content = request_orig(uri, method, body, headers,
604 redirections, connection_type)
605 return resp, content
606
607 http.request = new_request
608 return http
Joe Gregoriof4153422011-03-18 22:45:18 -0400609
610
611def tunnel_patch(http):
612 """Tunnel PATCH requests over POST.
613 Args:
614 http - An instance of httplib2.Http
615 or something that acts like it.
616
617 Returns:
618 A modified instance of http that was passed in.
619
620 Example:
621
622 h = httplib2.Http()
623 h = tunnel_patch(h, "my-app-name/6.0")
624
625 Useful if you are running on a platform that doesn't support PATCH.
626 Apply this last if you are using OAuth 1.0, as changing the method
627 will result in a different signature.
628 """
629 request_orig = http.request
630
631 # The closure that will replace 'httplib2.Http.request'.
632 def new_request(uri, method='GET', body=None, headers=None,
633 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
634 connection_type=None):
635 """Modify the request headers to add the user-agent."""
636 if headers is None:
637 headers = {}
638 if method == 'PATCH':
Joe Gregorio06d852b2011-03-25 15:03:10 -0400639 if 'oauth_token' in headers.get('authorization', ''):
Joe Gregorioe9e236f2011-03-21 22:23:14 -0400640 logging.warning(
Joe Gregorio06d852b2011-03-25 15:03:10 -0400641 'OAuth 1.0 request made with Credentials after tunnel_patch.')
Joe Gregoriof4153422011-03-18 22:45:18 -0400642 headers['x-http-method-override'] = "PATCH"
643 method = 'POST'
644 resp, content = request_orig(uri, method, body, headers,
645 redirections, connection_type)
646 return resp, content
647
648 http.request = new_request
649 return http