blob: b73014a19a3d15cf85b71cb2faf4fa22903c5035 [file] [log] [blame]
Joe Gregorio88f699f2012-06-07 13:36:06 -04001# Copyright (C) 2012 Google Inc.
Joe Gregorio20a5aa92011-04-01 17:44:25 -04002#
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
Joe Gregorio66f57522011-11-30 11:00:00 -050024import StringIO
Ali Afshar6f11ea12012-02-07 10:32:14 -050025import base64
Joe Gregoriod0bd3882011-11-22 09:49:47 -050026import copy
Joe Gregorio66f57522011-11-30 11:00:00 -050027import gzip
Joe Gregorioc6722462010-12-20 14:29:28 -050028import httplib2
Joe Gregoriod0bd3882011-11-22 09:49:47 -050029import mimeparse
30import mimetypes
Joe Gregorio66f57522011-11-30 11:00:00 -050031import os
Joe Gregorioc80ac9d2012-08-21 14:09:09 -040032import sys
Joe Gregorio66f57522011-11-30 11:00:00 -050033import urllib
34import urlparse
35import uuid
Joe Gregoriocb8103d2011-02-11 23:20:52 -050036
Joe Gregorio654f4a22012-02-09 14:15:44 -050037from email.generator import Generator
Joe Gregorio66f57522011-11-30 11:00:00 -050038from email.mime.multipart import MIMEMultipart
39from email.mime.nonmultipart import MIMENonMultipart
40from email.parser import FeedParser
41from errors import BatchError
Joe Gregorio49396552011-03-08 10:39:00 -050042from errors import HttpError
Joe Gregorioc80ac9d2012-08-21 14:09:09 -040043from errors import InvalidChunkSizeError
Joe Gregoriod0bd3882011-11-22 09:49:47 -050044from errors import ResumableUploadError
Joe Gregorioa388ce32011-09-09 17:19:13 -040045from errors import UnexpectedBodyError
46from errors import UnexpectedMethodError
Joe Gregorio66f57522011-11-30 11:00:00 -050047from model import JsonModel
Joe Gregorio68a8cfe2012-08-03 16:17:40 -040048from oauth2client import util
Joe Gregorio549230c2012-01-11 10:38:05 -050049from oauth2client.anyjson import simplejson
Joe Gregorioc5c5a372010-09-22 11:42:32 -040050
51
Joe Gregorio910b9b12012-06-12 09:36:30 -040052DEFAULT_CHUNK_SIZE = 512*1024
53
Joe Gregorio2728ed12012-11-16 15:48:26 -050054MAX_URI_LENGTH = 2048
Joe Gregorioba5c7902012-08-03 12:48:16 -040055
Joe Gregorio910b9b12012-06-12 09:36:30 -040056
Joe Gregoriod0bd3882011-11-22 09:49:47 -050057class MediaUploadProgress(object):
58 """Status of a resumable upload."""
59
60 def __init__(self, resumable_progress, total_size):
61 """Constructor.
62
63 Args:
64 resumable_progress: int, bytes sent so far.
Joe Gregorio910b9b12012-06-12 09:36:30 -040065 total_size: int, total bytes in complete upload, or None if the total
66 upload size isn't known ahead of time.
Joe Gregoriod0bd3882011-11-22 09:49:47 -050067 """
68 self.resumable_progress = resumable_progress
69 self.total_size = total_size
70
71 def progress(self):
Joe Gregorio910b9b12012-06-12 09:36:30 -040072 """Percent of upload completed, as a float.
73
74 Returns:
75 the percentage complete as a float, returning 0.0 if the total size of
76 the upload is unknown.
77 """
78 if self.total_size is not None:
79 return float(self.resumable_progress) / float(self.total_size)
80 else:
81 return 0.0
Joe Gregoriod0bd3882011-11-22 09:49:47 -050082
83
Joe Gregorio708388c2012-06-15 13:43:04 -040084class MediaDownloadProgress(object):
85 """Status of a resumable download."""
86
87 def __init__(self, resumable_progress, total_size):
88 """Constructor.
89
90 Args:
91 resumable_progress: int, bytes received so far.
92 total_size: int, total bytes in complete download.
93 """
94 self.resumable_progress = resumable_progress
95 self.total_size = total_size
96
97 def progress(self):
98 """Percent of download completed, as a float.
99
100 Returns:
101 the percentage complete as a float, returning 0.0 if the total size of
102 the download is unknown.
103 """
104 if self.total_size is not None:
105 return float(self.resumable_progress) / float(self.total_size)
106 else:
107 return 0.0
108
109
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500110class MediaUpload(object):
111 """Describes a media object to upload.
112
113 Base class that defines the interface of MediaUpload subclasses.
Joe Gregorio88f699f2012-06-07 13:36:06 -0400114
115 Note that subclasses of MediaUpload may allow you to control the chunksize
Joe Gregorioc80ac9d2012-08-21 14:09:09 -0400116 when uploading a media object. It is important to keep the size of the chunk
117 as large as possible to keep the upload efficient. Other factors may influence
Joe Gregorio88f699f2012-06-07 13:36:06 -0400118 the size of the chunk you use, particularly if you are working in an
119 environment where individual HTTP requests may have a hardcoded time limit,
120 such as under certain classes of requests under Google App Engine.
Joe Gregorioc80ac9d2012-08-21 14:09:09 -0400121
122 Streams are io.Base compatible objects that support seek(). Some MediaUpload
123 subclasses support using streams directly to upload data. Support for
124 streaming may be indicated by a MediaUpload sub-class and if appropriate for a
125 platform that stream will be used for uploading the media object. The support
126 for streaming is indicated by has_stream() returning True. The stream() method
127 should return an io.Base object that supports seek(). On platforms where the
128 underlying httplib module supports streaming, for example Python 2.6 and
129 later, the stream will be passed into the http library which will result in
130 less memory being used and possibly faster uploads.
131
132 If you need to upload media that can't be uploaded using any of the existing
133 MediaUpload sub-class then you can sub-class MediaUpload for your particular
134 needs.
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500135 """
136
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500137 def chunksize(self):
Joe Gregorio910b9b12012-06-12 09:36:30 -0400138 """Chunk size for resumable uploads.
139
140 Returns:
141 Chunk size in bytes.
142 """
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500143 raise NotImplementedError()
144
145 def mimetype(self):
Joe Gregorio910b9b12012-06-12 09:36:30 -0400146 """Mime type of the body.
147
148 Returns:
149 Mime type.
150 """
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500151 return 'application/octet-stream'
152
Joe Gregorio910b9b12012-06-12 09:36:30 -0400153 def size(self):
154 """Size of upload.
155
156 Returns:
157 Size of the body, or None of the size is unknown.
158 """
159 return None
160
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500161 def resumable(self):
Joe Gregorio910b9b12012-06-12 09:36:30 -0400162 """Whether this upload is resumable.
163
164 Returns:
165 True if resumable upload or False.
166 """
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500167 return False
168
Joe Gregorio910b9b12012-06-12 09:36:30 -0400169 def getbytes(self, begin, end):
170 """Get bytes from the media.
171
172 Args:
173 begin: int, offset from beginning of file.
174 length: int, number of bytes to read, starting at begin.
175
176 Returns:
177 A string of bytes read. May be shorter than length if EOF was reached
178 first.
179 """
180 raise NotImplementedError()
181
Joe Gregorioc80ac9d2012-08-21 14:09:09 -0400182 def has_stream(self):
183 """Does the underlying upload support a streaming interface.
184
185 Streaming means it is an io.IOBase subclass that supports seek, i.e.
186 seekable() returns True.
187
188 Returns:
189 True if the call to stream() will return an instance of a seekable io.Base
190 subclass.
191 """
192 return False
193
194 def stream(self):
195 """A stream interface to the data being uploaded.
196
197 Returns:
198 The returned value is an io.IOBase subclass that supports seek, i.e.
199 seekable() returns True.
200 """
201 raise NotImplementedError()
202
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400203 @util.positional(1)
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500204 def _to_json(self, strip=None):
205 """Utility function for creating a JSON representation of a MediaUpload.
206
207 Args:
208 strip: array, An array of names of members to not include in the JSON.
209
210 Returns:
211 string, a JSON representation of this instance, suitable to pass to
212 from_json().
213 """
214 t = type(self)
215 d = copy.copy(self.__dict__)
216 if strip is not None:
217 for member in strip:
218 del d[member]
219 d['_class'] = t.__name__
220 d['_module'] = t.__module__
221 return simplejson.dumps(d)
222
223 def to_json(self):
224 """Create a JSON representation of an instance of MediaUpload.
225
226 Returns:
227 string, a JSON representation of this instance, suitable to pass to
228 from_json().
229 """
230 return self._to_json()
231
232 @classmethod
233 def new_from_json(cls, s):
234 """Utility class method to instantiate a MediaUpload subclass from a JSON
235 representation produced by to_json().
236
237 Args:
238 s: string, JSON from to_json().
239
240 Returns:
241 An instance of the subclass of MediaUpload that was serialized with
242 to_json().
243 """
244 data = simplejson.loads(s)
245 # Find and call the right classmethod from_json() to restore the object.
246 module = data['_module']
247 m = __import__(module, fromlist=module.split('.')[:-1])
248 kls = getattr(m, data['_class'])
249 from_json = getattr(kls, 'from_json')
250 return from_json(s)
251
Joe Gregorio66f57522011-11-30 11:00:00 -0500252
Joe Gregorioc80ac9d2012-08-21 14:09:09 -0400253class MediaIoBaseUpload(MediaUpload):
254 """A MediaUpload for a io.Base objects.
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500255
Joe Gregorioc80ac9d2012-08-21 14:09:09 -0400256 Note that the Python file object is compatible with io.Base and can be used
257 with this class also.
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500258
Joe Gregorioc80ac9d2012-08-21 14:09:09 -0400259 fh = io.BytesIO('...Some data to upload...')
260 media = MediaIoBaseUpload(fh, mimetype='image/png',
Joe Gregorio910b9b12012-06-12 09:36:30 -0400261 chunksize=1024*1024, resumable=True)
Joe Gregorioc80ac9d2012-08-21 14:09:09 -0400262 farm.animals().insert(
Joe Gregorio7ceb26f2012-06-15 13:57:26 -0400263 id='cow',
264 name='cow.png',
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500265 media_body=media).execute()
Joe Gregorioc80ac9d2012-08-21 14:09:09 -0400266
267 Depending on the platform you are working on, you may pass -1 as the
268 chunksize, which indicates that the entire file should be uploaded in a single
269 request. If the underlying platform supports streams, such as Python 2.6 or
270 later, then this can be very efficient as it avoids multiple connections, and
271 also avoids loading the entire file into memory before sending it. Note that
272 Google App Engine has a 5MB limit on request size, so you should never set
273 your chunksize larger than 5MB, or to -1.
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500274 """
275
Joe Gregorioc80ac9d2012-08-21 14:09:09 -0400276 @util.positional(3)
277 def __init__(self, fd, mimetype, chunksize=DEFAULT_CHUNK_SIZE,
278 resumable=False):
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500279 """Constructor.
280
281 Args:
Joe Gregorioc80ac9d2012-08-21 14:09:09 -0400282 fd: io.Base or file object, The source of the bytes to upload. MUST be
283 opened in blocking mode, do not use streams opened in non-blocking mode.
284 The given stream must be seekable, that is, it must be able to call
285 seek() on fd.
286 mimetype: string, Mime-type of the file.
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500287 chunksize: int, File will be uploaded in chunks of this many bytes. Only
Joe Gregorioc80ac9d2012-08-21 14:09:09 -0400288 used if resumable=True. Pass in a value of -1 if the file is to be
289 uploaded as a single chunk. Note that Google App Engine has a 5MB limit
290 on request size, so you should never set your chunksize larger than 5MB,
291 or to -1.
Joe Gregorio66f57522011-11-30 11:00:00 -0500292 resumable: bool, True if this is a resumable upload. False means upload
293 in a single request.
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500294 """
Joe Gregorioc80ac9d2012-08-21 14:09:09 -0400295 super(MediaIoBaseUpload, self).__init__()
296 self._fd = fd
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500297 self._mimetype = mimetype
Joe Gregorioc80ac9d2012-08-21 14:09:09 -0400298 if not (chunksize == -1 or chunksize > 0):
299 raise InvalidChunkSizeError()
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500300 self._chunksize = chunksize
301 self._resumable = resumable
302
Joe Gregorioc80ac9d2012-08-21 14:09:09 -0400303 self._fd.seek(0, os.SEEK_END)
304 self._size = self._fd.tell()
305
Joe Gregorio910b9b12012-06-12 09:36:30 -0400306 def chunksize(self):
307 """Chunk size for resumable uploads.
308
309 Returns:
310 Chunk size in bytes.
311 """
312 return self._chunksize
313
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500314 def mimetype(self):
Joe Gregorio910b9b12012-06-12 09:36:30 -0400315 """Mime type of the body.
316
317 Returns:
318 Mime type.
319 """
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500320 return self._mimetype
321
322 def size(self):
Joe Gregorio910b9b12012-06-12 09:36:30 -0400323 """Size of upload.
324
325 Returns:
326 Size of the body, or None of the size is unknown.
327 """
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500328 return self._size
329
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500330 def resumable(self):
Joe Gregorio910b9b12012-06-12 09:36:30 -0400331 """Whether this upload is resumable.
332
333 Returns:
334 True if resumable upload or False.
335 """
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500336 return self._resumable
337
338 def getbytes(self, begin, length):
339 """Get bytes from the media.
340
341 Args:
342 begin: int, offset from beginning of file.
343 length: int, number of bytes to read, starting at begin.
344
345 Returns:
346 A string of bytes read. May be shorted than length if EOF was reached
347 first.
348 """
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500349 self._fd.seek(begin)
350 return self._fd.read(length)
351
Joe Gregorioc80ac9d2012-08-21 14:09:09 -0400352 def has_stream(self):
353 """Does the underlying upload support a streaming interface.
354
355 Streaming means it is an io.IOBase subclass that supports seek, i.e.
356 seekable() returns True.
357
358 Returns:
359 True if the call to stream() will return an instance of a seekable io.Base
360 subclass.
361 """
362 return True
363
364 def stream(self):
365 """A stream interface to the data being uploaded.
366
367 Returns:
368 The returned value is an io.IOBase subclass that supports seek, i.e.
369 seekable() returns True.
370 """
371 return self._fd
372
373 def to_json(self):
374 """This upload type is not serializable."""
375 raise NotImplementedError('MediaIoBaseUpload is not serializable.')
376
377
378class MediaFileUpload(MediaIoBaseUpload):
379 """A MediaUpload for a file.
380
381 Construct a MediaFileUpload and pass as the media_body parameter of the
382 method. For example, if we had a service that allowed uploading images:
383
384
385 media = MediaFileUpload('cow.png', mimetype='image/png',
386 chunksize=1024*1024, resumable=True)
387 farm.animals().insert(
388 id='cow',
389 name='cow.png',
390 media_body=media).execute()
391
392 Depending on the platform you are working on, you may pass -1 as the
393 chunksize, which indicates that the entire file should be uploaded in a single
394 request. If the underlying platform supports streams, such as Python 2.6 or
395 later, then this can be very efficient as it avoids multiple connections, and
396 also avoids loading the entire file into memory before sending it. Note that
397 Google App Engine has a 5MB limit on request size, so you should never set
398 your chunksize larger than 5MB, or to -1.
399 """
400
401 @util.positional(2)
402 def __init__(self, filename, mimetype=None, chunksize=DEFAULT_CHUNK_SIZE,
403 resumable=False):
404 """Constructor.
405
406 Args:
407 filename: string, Name of the file.
408 mimetype: string, Mime-type of the file. If None then a mime-type will be
409 guessed from the file extension.
410 chunksize: int, File will be uploaded in chunks of this many bytes. Only
411 used if resumable=True. Pass in a value of -1 if the file is to be
412 uploaded in a single chunk. Note that Google App Engine has a 5MB limit
413 on request size, so you should never set your chunksize larger than 5MB,
414 or to -1.
415 resumable: bool, True if this is a resumable upload. False means upload
416 in a single request.
417 """
418 self._filename = filename
419 fd = open(self._filename, 'rb')
420 if mimetype is None:
421 (mimetype, encoding) = mimetypes.guess_type(filename)
422 super(MediaFileUpload, self).__init__(fd, mimetype, chunksize=chunksize,
423 resumable=resumable)
424
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500425 def to_json(self):
Joe Gregorio708388c2012-06-15 13:43:04 -0400426 """Creating a JSON representation of an instance of MediaFileUpload.
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500427
428 Returns:
429 string, a JSON representation of this instance, suitable to pass to
430 from_json().
431 """
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400432 return self._to_json(strip=['_fd'])
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500433
434 @staticmethod
435 def from_json(s):
436 d = simplejson.loads(s)
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400437 return MediaFileUpload(d['_filename'], mimetype=d['_mimetype'],
438 chunksize=d['_chunksize'], resumable=d['_resumable'])
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500439
440
Joe Gregorioc80ac9d2012-08-21 14:09:09 -0400441class MediaInMemoryUpload(MediaIoBaseUpload):
Ali Afshar6f11ea12012-02-07 10:32:14 -0500442 """MediaUpload for a chunk of bytes.
443
Joe Gregorioc80ac9d2012-08-21 14:09:09 -0400444 DEPRECATED: Use MediaIoBaseUpload with either io.TextIOBase or StringIO for
445 the stream.
Ali Afshar6f11ea12012-02-07 10:32:14 -0500446 """
447
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400448 @util.positional(2)
Ali Afshar6f11ea12012-02-07 10:32:14 -0500449 def __init__(self, body, mimetype='application/octet-stream',
Joe Gregorio910b9b12012-06-12 09:36:30 -0400450 chunksize=DEFAULT_CHUNK_SIZE, resumable=False):
Joe Gregorioc80ac9d2012-08-21 14:09:09 -0400451 """Create a new MediaInMemoryUpload.
Ali Afshar6f11ea12012-02-07 10:32:14 -0500452
Joe Gregorioc80ac9d2012-08-21 14:09:09 -0400453 DEPRECATED: Use MediaIoBaseUpload with either io.TextIOBase or StringIO for
454 the stream.
455
456 Args:
457 body: string, Bytes of body content.
458 mimetype: string, Mime-type of the file or default of
459 'application/octet-stream'.
460 chunksize: int, File will be uploaded in chunks of this many bytes. Only
461 used if resumable=True.
462 resumable: bool, True if this is a resumable upload. False means upload
463 in a single request.
Ali Afshar6f11ea12012-02-07 10:32:14 -0500464 """
Joe Gregorioc80ac9d2012-08-21 14:09:09 -0400465 fd = StringIO.StringIO(body)
466 super(MediaInMemoryUpload, self).__init__(fd, mimetype, chunksize=chunksize,
467 resumable=resumable)
Ali Afshar6f11ea12012-02-07 10:32:14 -0500468
469
Joe Gregorio708388c2012-06-15 13:43:04 -0400470class MediaIoBaseDownload(object):
471 """"Download media resources.
472
473 Note that the Python file object is compatible with io.Base and can be used
474 with this class also.
475
476
477 Example:
Joe Gregorio7ceb26f2012-06-15 13:57:26 -0400478 request = farms.animals().get_media(id='cow')
479 fh = io.FileIO('cow.png', mode='wb')
Joe Gregorio708388c2012-06-15 13:43:04 -0400480 downloader = MediaIoBaseDownload(fh, request, chunksize=1024*1024)
481
482 done = False
483 while done is False:
484 status, done = downloader.next_chunk()
485 if status:
486 print "Download %d%%." % int(status.progress() * 100)
487 print "Download Complete!"
488 """
489
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400490 @util.positional(3)
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400491 def __init__(self, fd, request, chunksize=DEFAULT_CHUNK_SIZE):
Joe Gregorio708388c2012-06-15 13:43:04 -0400492 """Constructor.
493
494 Args:
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400495 fd: io.Base or file object, The stream in which to write the downloaded
Joe Gregorio708388c2012-06-15 13:43:04 -0400496 bytes.
497 request: apiclient.http.HttpRequest, the media request to perform in
498 chunks.
499 chunksize: int, File will be downloaded in chunks of this many bytes.
500 """
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400501 self._fd = fd
502 self._request = request
503 self._uri = request.uri
504 self._chunksize = chunksize
505 self._progress = 0
506 self._total_size = None
507 self._done = False
Joe Gregorio97ef1cc2013-06-13 14:47:10 -0400508 self._original_follow_redirects = request.http.follow_redirects
509 request.http.follow_redirects = False
Joe Gregorio708388c2012-06-15 13:43:04 -0400510
511 def next_chunk(self):
512 """Get the next chunk of the download.
513
514 Returns:
515 (status, done): (MediaDownloadStatus, boolean)
516 The value of 'done' will be True when the media has been fully
517 downloaded.
518
519 Raises:
520 apiclient.errors.HttpError if the response was not a 2xx.
Joe Gregorio77af30a2012-08-01 14:54:40 -0400521 httplib2.HttpLib2Error if a transport error has occured.
Joe Gregorio708388c2012-06-15 13:43:04 -0400522 """
523 headers = {
524 'range': 'bytes=%d-%d' % (
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400525 self._progress, self._progress + self._chunksize)
Joe Gregorio708388c2012-06-15 13:43:04 -0400526 }
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400527 http = self._request.http
Joe Gregorio708388c2012-06-15 13:43:04 -0400528
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400529 resp, content = http.request(self._uri, headers=headers)
Joe Gregorio708388c2012-06-15 13:43:04 -0400530 if resp.status in [301, 302, 303, 307, 308] and 'location' in resp:
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400531 self._uri = resp['location']
532 resp, content = http.request(self._uri, headers=headers)
Joe Gregorio708388c2012-06-15 13:43:04 -0400533 if resp.status in [200, 206]:
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400534 self._progress += len(content)
535 self._fd.write(content)
Joe Gregorio708388c2012-06-15 13:43:04 -0400536
537 if 'content-range' in resp:
538 content_range = resp['content-range']
539 length = content_range.rsplit('/', 1)[1]
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400540 self._total_size = int(length)
Joe Gregorio708388c2012-06-15 13:43:04 -0400541
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400542 if self._progress == self._total_size:
543 self._done = True
Joe Gregorio97ef1cc2013-06-13 14:47:10 -0400544 self._request.http.follow_redirects = self._original_follow_redirects
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400545 return MediaDownloadProgress(self._progress, self._total_size), self._done
Joe Gregorio708388c2012-06-15 13:43:04 -0400546 else:
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400547 raise HttpError(resp, content, uri=self._uri)
Joe Gregorio708388c2012-06-15 13:43:04 -0400548
549
Joe Gregorio5c120db2012-08-23 09:13:55 -0400550class _StreamSlice(object):
551 """Truncated stream.
552
553 Takes a stream and presents a stream that is a slice of the original stream.
554 This is used when uploading media in chunks. In later versions of Python a
555 stream can be passed to httplib in place of the string of data to send. The
556 problem is that httplib just blindly reads to the end of the stream. This
557 wrapper presents a virtual stream that only reads to the end of the chunk.
558 """
559
560 def __init__(self, stream, begin, chunksize):
561 """Constructor.
562
563 Args:
564 stream: (io.Base, file object), the stream to wrap.
565 begin: int, the seek position the chunk begins at.
566 chunksize: int, the size of the chunk.
567 """
568 self._stream = stream
569 self._begin = begin
570 self._chunksize = chunksize
571 self._stream.seek(begin)
572
573 def read(self, n=-1):
574 """Read n bytes.
575
576 Args:
577 n, int, the number of bytes to read.
578
579 Returns:
580 A string of length 'n', or less if EOF is reached.
581 """
582 # The data left available to read sits in [cur, end)
583 cur = self._stream.tell()
584 end = self._begin + self._chunksize
585 if n == -1 or cur + n > end:
586 n = end - cur
587 return self._stream.read(n)
588
589
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400590class HttpRequest(object):
Joe Gregorio66f57522011-11-30 11:00:00 -0500591 """Encapsulates a single HTTP request."""
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400592
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400593 @util.positional(4)
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500594 def __init__(self, http, postproc, uri,
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500595 method='GET',
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500596 body=None,
597 headers=None,
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500598 methodId=None,
599 resumable=None):
Joe Gregorioaf276d22010-12-09 14:26:58 -0500600 """Constructor for an HttpRequest.
601
Joe Gregorioaf276d22010-12-09 14:26:58 -0500602 Args:
603 http: httplib2.Http, the transport object to use to make a request
Joe Gregorioabda96f2011-02-11 20:19:33 -0500604 postproc: callable, called on the HTTP response and content to transform
605 it into a data object before returning, or raising an exception
606 on an error.
Joe Gregorioaf276d22010-12-09 14:26:58 -0500607 uri: string, the absolute URI to send the request to
608 method: string, the HTTP method to use
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500609 body: string, the request body of the HTTP request,
Joe Gregorioaf276d22010-12-09 14:26:58 -0500610 headers: dict, the HTTP request headers
Joe Gregorioaf276d22010-12-09 14:26:58 -0500611 methodId: string, a unique identifier for the API method being called.
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500612 resumable: MediaUpload, None if this is not a resumbale request.
Joe Gregorioaf276d22010-12-09 14:26:58 -0500613 """
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400614 self.uri = uri
615 self.method = method
616 self.body = body
617 self.headers = headers or {}
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500618 self.methodId = methodId
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400619 self.http = http
620 self.postproc = postproc
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500621 self.resumable = resumable
Ali Afshar164f37e2013-01-07 14:05:45 -0800622 self.response_callbacks = []
Joe Gregorio910b9b12012-06-12 09:36:30 -0400623 self._in_error_state = False
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500624
Joe Gregorio66f57522011-11-30 11:00:00 -0500625 # Pull the multipart boundary out of the content-type header.
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500626 major, minor, params = mimeparse.parse_mime_type(
627 headers.get('content-type', 'application/json'))
Joe Gregoriobd512b52011-12-06 15:39:26 -0500628
Joe Gregorio945be3e2012-01-27 17:01:06 -0500629 # The size of the non-media part of the request.
630 self.body_size = len(self.body or '')
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500631
632 # The resumable URI to send chunks to.
633 self.resumable_uri = None
634
635 # The bytes that have been uploaded.
636 self.resumable_progress = 0
637
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400638 @util.positional(1)
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400639 def execute(self, http=None):
640 """Execute the request.
641
Joe Gregorioaf276d22010-12-09 14:26:58 -0500642 Args:
643 http: httplib2.Http, an http object to be used in place of the
644 one the HttpRequest request object was constructed with.
645
646 Returns:
647 A deserialized object model of the response body as determined
648 by the postproc.
649
650 Raises:
651 apiclient.errors.HttpError if the response was not a 2xx.
Joe Gregorio77af30a2012-08-01 14:54:40 -0400652 httplib2.HttpLib2Error if a transport error has occured.
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400653 """
654 if http is None:
655 http = self.http
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500656 if self.resumable:
657 body = None
658 while body is None:
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400659 _, body = self.next_chunk(http=http)
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500660 return body
661 else:
Joe Gregorio884e2b22012-02-24 09:37:00 -0500662 if 'content-length' not in self.headers:
663 self.headers['content-length'] = str(self.body_size)
Joe Gregorioba5c7902012-08-03 12:48:16 -0400664 # If the request URI is too long then turn it into a POST request.
665 if len(self.uri) > MAX_URI_LENGTH and self.method == 'GET':
666 self.method = 'POST'
667 self.headers['x-http-method-override'] = 'GET'
668 self.headers['content-type'] = 'application/x-www-form-urlencoded'
669 parsed = urlparse.urlparse(self.uri)
670 self.uri = urlparse.urlunparse(
671 (parsed.scheme, parsed.netloc, parsed.path, parsed.params, None,
672 None)
673 )
674 self.body = parsed.query
675 self.headers['content-length'] = str(len(self.body))
676
Joe Gregorio83f2ee62012-12-06 15:25:54 -0500677 resp, content = http.request(str(self.uri), method=str(self.method),
678 body=self.body, headers=self.headers)
Ali Afshar164f37e2013-01-07 14:05:45 -0800679 for callback in self.response_callbacks:
680 callback(resp)
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500681 if resp.status >= 300:
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400682 raise HttpError(resp, content, uri=self.uri)
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400683 return self.postproc(resp, content)
Joe Gregorioaf276d22010-12-09 14:26:58 -0500684
Ali Afshar164f37e2013-01-07 14:05:45 -0800685 @util.positional(2)
686 def add_response_callback(self, cb):
687 """add_response_headers_callback
688
689 Args:
690 cb: Callback to be called on receiving the response headers, of signature:
691
692 def cb(resp):
693 # Where resp is an instance of httplib2.Response
694 """
695 self.response_callbacks.append(cb)
696
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400697 @util.positional(1)
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500698 def next_chunk(self, http=None):
699 """Execute the next step of a resumable upload.
700
Joe Gregorio66f57522011-11-30 11:00:00 -0500701 Can only be used if the method being executed supports media uploads and
702 the MediaUpload object passed in was flagged as using resumable upload.
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500703
704 Example:
705
Joe Gregorio7ceb26f2012-06-15 13:57:26 -0400706 media = MediaFileUpload('cow.png', mimetype='image/png',
Joe Gregorio66f57522011-11-30 11:00:00 -0500707 chunksize=1000, resumable=True)
Joe Gregorio7ceb26f2012-06-15 13:57:26 -0400708 request = farm.animals().insert(
709 id='cow',
710 name='cow.png',
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500711 media_body=media)
712
713 response = None
714 while response is None:
715 status, response = request.next_chunk()
716 if status:
717 print "Upload %d%% complete." % int(status.progress() * 100)
718
719
720 Returns:
721 (status, body): (ResumableMediaStatus, object)
722 The body will be None until the resumable media is fully uploaded.
Joe Gregorio910b9b12012-06-12 09:36:30 -0400723
724 Raises:
725 apiclient.errors.HttpError if the response was not a 2xx.
Joe Gregorio77af30a2012-08-01 14:54:40 -0400726 httplib2.HttpLib2Error if a transport error has occured.
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500727 """
728 if http is None:
729 http = self.http
730
Joe Gregorio910b9b12012-06-12 09:36:30 -0400731 if self.resumable.size() is None:
732 size = '*'
733 else:
734 size = str(self.resumable.size())
735
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500736 if self.resumable_uri is None:
737 start_headers = copy.copy(self.headers)
738 start_headers['X-Upload-Content-Type'] = self.resumable.mimetype()
Joe Gregorio910b9b12012-06-12 09:36:30 -0400739 if size != '*':
740 start_headers['X-Upload-Content-Length'] = size
Joe Gregorio945be3e2012-01-27 17:01:06 -0500741 start_headers['content-length'] = str(self.body_size)
742
Joe Gregorio28f34e72013-04-30 16:29:33 -0400743 resp, content = http.request(self.uri, method=self.method,
Joe Gregorio945be3e2012-01-27 17:01:06 -0500744 body=self.body,
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500745 headers=start_headers)
746 if resp.status == 200 and 'location' in resp:
747 self.resumable_uri = resp['location']
748 else:
Joe Gregoriobaf04802013-03-01 12:27:06 -0500749 raise ResumableUploadError(resp, content)
Joe Gregorio910b9b12012-06-12 09:36:30 -0400750 elif self._in_error_state:
751 # If we are in an error state then query the server for current state of
752 # the upload by sending an empty PUT and reading the 'range' header in
753 # the response.
754 headers = {
755 'Content-Range': 'bytes */%s' % size,
756 'content-length': '0'
757 }
758 resp, content = http.request(self.resumable_uri, 'PUT',
759 headers=headers)
760 status, body = self._process_response(resp, content)
761 if body:
762 # The upload was complete.
763 return (status, body)
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500764
Joe Gregorioc80ac9d2012-08-21 14:09:09 -0400765 # The httplib.request method can take streams for the body parameter, but
766 # only in Python 2.6 or later. If a stream is available under those
767 # conditions then use it as the body argument.
768 if self.resumable.has_stream() and sys.version_info[1] >= 6:
769 data = self.resumable.stream()
Joe Gregorioc80ac9d2012-08-21 14:09:09 -0400770 if self.resumable.chunksize() == -1:
Joe Gregorio5c120db2012-08-23 09:13:55 -0400771 data.seek(self.resumable_progress)
772 chunk_end = self.resumable.size() - self.resumable_progress - 1
Joe Gregorioc80ac9d2012-08-21 14:09:09 -0400773 else:
Joe Gregorio5c120db2012-08-23 09:13:55 -0400774 # Doing chunking with a stream, so wrap a slice of the stream.
775 data = _StreamSlice(data, self.resumable_progress,
776 self.resumable.chunksize())
Joe Gregorioc80ac9d2012-08-21 14:09:09 -0400777 chunk_end = min(
778 self.resumable_progress + self.resumable.chunksize() - 1,
779 self.resumable.size() - 1)
780 else:
781 data = self.resumable.getbytes(
782 self.resumable_progress, self.resumable.chunksize())
Joe Gregorio44454e42012-06-15 08:38:53 -0400783
Joe Gregorioc80ac9d2012-08-21 14:09:09 -0400784 # A short read implies that we are at EOF, so finish the upload.
785 if len(data) < self.resumable.chunksize():
786 size = str(self.resumable_progress + len(data))
787
788 chunk_end = self.resumable_progress + len(data) - 1
Joe Gregorio44454e42012-06-15 08:38:53 -0400789
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500790 headers = {
Joe Gregorio910b9b12012-06-12 09:36:30 -0400791 'Content-Range': 'bytes %d-%d/%s' % (
Joe Gregorio5c120db2012-08-23 09:13:55 -0400792 self.resumable_progress, chunk_end, size),
793 # Must set the content-length header here because httplib can't
794 # calculate the size when working with _StreamSlice.
795 'Content-Length': str(chunk_end - self.resumable_progress + 1)
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500796 }
Joe Gregorio910b9b12012-06-12 09:36:30 -0400797 try:
Joe Gregorio28f34e72013-04-30 16:29:33 -0400798 resp, content = http.request(self.resumable_uri, method='PUT',
Joe Gregorio910b9b12012-06-12 09:36:30 -0400799 body=data,
800 headers=headers)
801 except:
802 self._in_error_state = True
803 raise
804
805 return self._process_response(resp, content)
806
807 def _process_response(self, resp, content):
808 """Process the response from a single chunk upload.
809
810 Args:
811 resp: httplib2.Response, the response object.
812 content: string, the content of the response.
813
814 Returns:
815 (status, body): (ResumableMediaStatus, object)
816 The body will be None until the resumable media is fully uploaded.
817
818 Raises:
819 apiclient.errors.HttpError if the response was not a 2xx or a 308.
820 """
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500821 if resp.status in [200, 201]:
Joe Gregorio910b9b12012-06-12 09:36:30 -0400822 self._in_error_state = False
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500823 return None, self.postproc(resp, content)
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500824 elif resp.status == 308:
Joe Gregorio910b9b12012-06-12 09:36:30 -0400825 self._in_error_state = False
Joe Gregorio66f57522011-11-30 11:00:00 -0500826 # A "308 Resume Incomplete" indicates we are not done.
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500827 self.resumable_progress = int(resp['range'].split('-')[1]) + 1
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500828 if 'location' in resp:
829 self.resumable_uri = resp['location']
830 else:
Joe Gregorio910b9b12012-06-12 09:36:30 -0400831 self._in_error_state = True
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400832 raise HttpError(resp, content, uri=self.uri)
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500833
Joe Gregorio945be3e2012-01-27 17:01:06 -0500834 return (MediaUploadProgress(self.resumable_progress, self.resumable.size()),
835 None)
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500836
837 def to_json(self):
838 """Returns a JSON representation of the HttpRequest."""
839 d = copy.copy(self.__dict__)
840 if d['resumable'] is not None:
841 d['resumable'] = self.resumable.to_json()
842 del d['http']
843 del d['postproc']
Joe Gregorio910b9b12012-06-12 09:36:30 -0400844
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500845 return simplejson.dumps(d)
846
847 @staticmethod
848 def from_json(s, http, postproc):
849 """Returns an HttpRequest populated with info from a JSON object."""
850 d = simplejson.loads(s)
851 if d['resumable'] is not None:
852 d['resumable'] = MediaUpload.new_from_json(d['resumable'])
853 return HttpRequest(
854 http,
855 postproc,
Joe Gregorio66f57522011-11-30 11:00:00 -0500856 uri=d['uri'],
857 method=d['method'],
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500858 body=d['body'],
859 headers=d['headers'],
860 methodId=d['methodId'],
861 resumable=d['resumable'])
862
Joe Gregorioaf276d22010-12-09 14:26:58 -0500863
Joe Gregorio66f57522011-11-30 11:00:00 -0500864class BatchHttpRequest(object):
Joe Gregorioebd0b842012-06-15 14:14:17 -0400865 """Batches multiple HttpRequest objects into a single HTTP request.
866
867 Example:
868 from apiclient.http import BatchHttpRequest
869
Joe Gregorioe7a0c472012-07-12 11:46:04 -0400870 def list_animals(request_id, response, exception):
Joe Gregorioebd0b842012-06-15 14:14:17 -0400871 \"\"\"Do something with the animals list response.\"\"\"
Joe Gregorioe7a0c472012-07-12 11:46:04 -0400872 if exception is not None:
873 # Do something with the exception.
874 pass
875 else:
876 # Do something with the response.
877 pass
Joe Gregorioebd0b842012-06-15 14:14:17 -0400878
Joe Gregorioe7a0c472012-07-12 11:46:04 -0400879 def list_farmers(request_id, response, exception):
Joe Gregorioebd0b842012-06-15 14:14:17 -0400880 \"\"\"Do something with the farmers list response.\"\"\"
Joe Gregorioe7a0c472012-07-12 11:46:04 -0400881 if exception is not None:
882 # Do something with the exception.
883 pass
884 else:
885 # Do something with the response.
886 pass
Joe Gregorioebd0b842012-06-15 14:14:17 -0400887
888 service = build('farm', 'v2')
889
890 batch = BatchHttpRequest()
891
892 batch.add(service.animals().list(), list_animals)
893 batch.add(service.farmers().list(), list_farmers)
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400894 batch.execute(http=http)
Joe Gregorioebd0b842012-06-15 14:14:17 -0400895 """
Joe Gregorio66f57522011-11-30 11:00:00 -0500896
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400897 @util.positional(1)
Joe Gregorio66f57522011-11-30 11:00:00 -0500898 def __init__(self, callback=None, batch_uri=None):
899 """Constructor for a BatchHttpRequest.
900
901 Args:
902 callback: callable, A callback to be called for each response, of the
Joe Gregorio4fbde1c2012-07-11 14:47:39 -0400903 form callback(id, response, exception). The first parameter is the
904 request id, and the second is the deserialized response object. The
905 third is an apiclient.errors.HttpError exception object if an HTTP error
906 occurred while processing the request, or None if no error occurred.
Joe Gregorio66f57522011-11-30 11:00:00 -0500907 batch_uri: string, URI to send batch requests to.
908 """
909 if batch_uri is None:
910 batch_uri = 'https://www.googleapis.com/batch'
911 self._batch_uri = batch_uri
912
913 # Global callback to be called for each individual response in the batch.
914 self._callback = callback
915
Joe Gregorio654f4a22012-02-09 14:15:44 -0500916 # A map from id to request.
Joe Gregorio66f57522011-11-30 11:00:00 -0500917 self._requests = {}
918
Joe Gregorio654f4a22012-02-09 14:15:44 -0500919 # A map from id to callback.
920 self._callbacks = {}
921
Joe Gregorio66f57522011-11-30 11:00:00 -0500922 # List of request ids, in the order in which they were added.
923 self._order = []
924
925 # The last auto generated id.
926 self._last_auto_id = 0
927
928 # Unique ID on which to base the Content-ID headers.
929 self._base_id = None
930
Joe Gregorioc752e332012-07-11 14:43:52 -0400931 # A map from request id to (httplib2.Response, content) response pairs
Joe Gregorio654f4a22012-02-09 14:15:44 -0500932 self._responses = {}
933
934 # A map of id(Credentials) that have been refreshed.
935 self._refreshed_credentials = {}
936
937 def _refresh_and_apply_credentials(self, request, http):
938 """Refresh the credentials and apply to the request.
939
940 Args:
941 request: HttpRequest, the request.
942 http: httplib2.Http, the global http object for the batch.
943 """
944 # For the credentials to refresh, but only once per refresh_token
945 # If there is no http per the request then refresh the http passed in
946 # via execute()
947 creds = None
948 if request.http is not None and hasattr(request.http.request,
949 'credentials'):
950 creds = request.http.request.credentials
951 elif http is not None and hasattr(http.request, 'credentials'):
952 creds = http.request.credentials
953 if creds is not None:
954 if id(creds) not in self._refreshed_credentials:
955 creds.refresh(http)
956 self._refreshed_credentials[id(creds)] = 1
957
958 # Only apply the credentials if we are using the http object passed in,
959 # otherwise apply() will get called during _serialize_request().
960 if request.http is None or not hasattr(request.http.request,
961 'credentials'):
962 creds.apply(request.headers)
963
Joe Gregorio66f57522011-11-30 11:00:00 -0500964 def _id_to_header(self, id_):
965 """Convert an id to a Content-ID header value.
966
967 Args:
968 id_: string, identifier of individual request.
969
970 Returns:
971 A Content-ID header with the id_ encoded into it. A UUID is prepended to
972 the value because Content-ID headers are supposed to be universally
973 unique.
974 """
975 if self._base_id is None:
976 self._base_id = uuid.uuid4()
977
978 return '<%s+%s>' % (self._base_id, urllib.quote(id_))
979
980 def _header_to_id(self, header):
981 """Convert a Content-ID header value to an id.
982
983 Presumes the Content-ID header conforms to the format that _id_to_header()
984 returns.
985
986 Args:
987 header: string, Content-ID header value.
988
989 Returns:
990 The extracted id value.
991
992 Raises:
993 BatchError if the header is not in the expected format.
994 """
995 if header[0] != '<' or header[-1] != '>':
996 raise BatchError("Invalid value for Content-ID: %s" % header)
997 if '+' not in header:
998 raise BatchError("Invalid value for Content-ID: %s" % header)
999 base, id_ = header[1:-1].rsplit('+', 1)
1000
1001 return urllib.unquote(id_)
1002
1003 def _serialize_request(self, request):
1004 """Convert an HttpRequest object into a string.
1005
1006 Args:
1007 request: HttpRequest, the request to serialize.
1008
1009 Returns:
1010 The request as a string in application/http format.
1011 """
1012 # Construct status line
1013 parsed = urlparse.urlparse(request.uri)
1014 request_line = urlparse.urlunparse(
1015 (None, None, parsed.path, parsed.params, parsed.query, None)
1016 )
1017 status_line = request.method + ' ' + request_line + ' HTTP/1.1\n'
Joe Gregorio5d1171b2012-01-05 10:48:24 -05001018 major, minor = request.headers.get('content-type', 'application/json').split('/')
Joe Gregorio66f57522011-11-30 11:00:00 -05001019 msg = MIMENonMultipart(major, minor)
1020 headers = request.headers.copy()
1021
Joe Gregorio654f4a22012-02-09 14:15:44 -05001022 if request.http is not None and hasattr(request.http.request,
1023 'credentials'):
1024 request.http.request.credentials.apply(headers)
1025
Joe Gregorio66f57522011-11-30 11:00:00 -05001026 # MIMENonMultipart adds its own Content-Type header.
1027 if 'content-type' in headers:
1028 del headers['content-type']
1029
1030 for key, value in headers.iteritems():
1031 msg[key] = value
1032 msg['Host'] = parsed.netloc
1033 msg.set_unixfrom(None)
1034
1035 if request.body is not None:
1036 msg.set_payload(request.body)
Joe Gregorio5d1171b2012-01-05 10:48:24 -05001037 msg['content-length'] = str(len(request.body))
Joe Gregorio66f57522011-11-30 11:00:00 -05001038
Joe Gregorio654f4a22012-02-09 14:15:44 -05001039 # Serialize the mime message.
1040 fp = StringIO.StringIO()
1041 # maxheaderlen=0 means don't line wrap headers.
1042 g = Generator(fp, maxheaderlen=0)
1043 g.flatten(msg, unixfrom=False)
1044 body = fp.getvalue()
1045
Joe Gregorio66f57522011-11-30 11:00:00 -05001046 # Strip off the \n\n that the MIME lib tacks onto the end of the payload.
1047 if request.body is None:
1048 body = body[:-2]
1049
Joe Gregoriodd813822012-01-25 10:32:47 -05001050 return status_line.encode('utf-8') + body
Joe Gregorio66f57522011-11-30 11:00:00 -05001051
1052 def _deserialize_response(self, payload):
1053 """Convert string into httplib2 response and content.
1054
1055 Args:
1056 payload: string, headers and body as a string.
1057
1058 Returns:
Joe Gregorioc752e332012-07-11 14:43:52 -04001059 A pair (resp, content), such as would be returned from httplib2.request.
Joe Gregorio66f57522011-11-30 11:00:00 -05001060 """
1061 # Strip off the status line
1062 status_line, payload = payload.split('\n', 1)
Joe Gregorio5d1171b2012-01-05 10:48:24 -05001063 protocol, status, reason = status_line.split(' ', 2)
Joe Gregorio66f57522011-11-30 11:00:00 -05001064
1065 # Parse the rest of the response
1066 parser = FeedParser()
1067 parser.feed(payload)
1068 msg = parser.close()
1069 msg['status'] = status
1070
1071 # Create httplib2.Response from the parsed headers.
1072 resp = httplib2.Response(msg)
1073 resp.reason = reason
1074 resp.version = int(protocol.split('/', 1)[1].replace('.', ''))
1075
1076 content = payload.split('\r\n\r\n', 1)[1]
1077
1078 return resp, content
1079
1080 def _new_id(self):
1081 """Create a new id.
1082
1083 Auto incrementing number that avoids conflicts with ids already used.
1084
1085 Returns:
1086 string, a new unique id.
1087 """
1088 self._last_auto_id += 1
1089 while str(self._last_auto_id) in self._requests:
1090 self._last_auto_id += 1
1091 return str(self._last_auto_id)
1092
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001093 @util.positional(2)
Joe Gregorio66f57522011-11-30 11:00:00 -05001094 def add(self, request, callback=None, request_id=None):
1095 """Add a new request.
1096
1097 Every callback added will be paired with a unique id, the request_id. That
1098 unique id will be passed back to the callback when the response comes back
1099 from the server. The default behavior is to have the library generate it's
1100 own unique id. If the caller passes in a request_id then they must ensure
1101 uniqueness for each request_id, and if they are not an exception is
1102 raised. Callers should either supply all request_ids or nevery supply a
1103 request id, to avoid such an error.
1104
1105 Args:
1106 request: HttpRequest, Request to add to the batch.
1107 callback: callable, A callback to be called for this response, of the
Joe Gregorio4fbde1c2012-07-11 14:47:39 -04001108 form callback(id, response, exception). The first parameter is the
1109 request id, and the second is the deserialized response object. The
1110 third is an apiclient.errors.HttpError exception object if an HTTP error
1111 occurred while processing the request, or None if no errors occurred.
Joe Gregorio66f57522011-11-30 11:00:00 -05001112 request_id: string, A unique id for the request. The id will be passed to
1113 the callback with the response.
1114
1115 Returns:
1116 None
1117
1118 Raises:
Joe Gregorioebd0b842012-06-15 14:14:17 -04001119 BatchError if a media request is added to a batch.
Joe Gregorio66f57522011-11-30 11:00:00 -05001120 KeyError is the request_id is not unique.
1121 """
1122 if request_id is None:
1123 request_id = self._new_id()
1124 if request.resumable is not None:
Joe Gregorioebd0b842012-06-15 14:14:17 -04001125 raise BatchError("Media requests cannot be used in a batch request.")
Joe Gregorio66f57522011-11-30 11:00:00 -05001126 if request_id in self._requests:
1127 raise KeyError("A request with this ID already exists: %s" % request_id)
Joe Gregorio654f4a22012-02-09 14:15:44 -05001128 self._requests[request_id] = request
1129 self._callbacks[request_id] = callback
Joe Gregorio66f57522011-11-30 11:00:00 -05001130 self._order.append(request_id)
1131
Joe Gregorio654f4a22012-02-09 14:15:44 -05001132 def _execute(self, http, order, requests):
1133 """Serialize batch request, send to server, process response.
1134
1135 Args:
1136 http: httplib2.Http, an http object to be used to make the request with.
1137 order: list, list of request ids in the order they were added to the
1138 batch.
1139 request: list, list of request objects to send.
1140
1141 Raises:
Joe Gregorio77af30a2012-08-01 14:54:40 -04001142 httplib2.HttpLib2Error if a transport error has occured.
Joe Gregorio654f4a22012-02-09 14:15:44 -05001143 apiclient.errors.BatchError if the response is the wrong format.
1144 """
1145 message = MIMEMultipart('mixed')
1146 # Message should not write out it's own headers.
1147 setattr(message, '_write_headers', lambda self: None)
1148
1149 # Add all the individual requests.
1150 for request_id in order:
1151 request = requests[request_id]
1152
1153 msg = MIMENonMultipart('application', 'http')
1154 msg['Content-Transfer-Encoding'] = 'binary'
1155 msg['Content-ID'] = self._id_to_header(request_id)
1156
1157 body = self._serialize_request(request)
1158 msg.set_payload(body)
1159 message.attach(msg)
1160
1161 body = message.as_string()
1162
1163 headers = {}
1164 headers['content-type'] = ('multipart/mixed; '
1165 'boundary="%s"') % message.get_boundary()
1166
Joe Gregorio28f34e72013-04-30 16:29:33 -04001167 resp, content = http.request(self._batch_uri, method='POST', body=body,
Joe Gregorio654f4a22012-02-09 14:15:44 -05001168 headers=headers)
1169
1170 if resp.status >= 300:
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001171 raise HttpError(resp, content, uri=self._batch_uri)
Joe Gregorio654f4a22012-02-09 14:15:44 -05001172
1173 # Now break out the individual responses and store each one.
1174 boundary, _ = content.split(None, 1)
1175
1176 # Prepend with a content-type header so FeedParser can handle it.
1177 header = 'content-type: %s\r\n\r\n' % resp['content-type']
1178 for_parser = header + content
1179
1180 parser = FeedParser()
1181 parser.feed(for_parser)
1182 mime_response = parser.close()
1183
1184 if not mime_response.is_multipart():
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001185 raise BatchError("Response not in multipart/mixed format.", resp=resp,
1186 content=content)
Joe Gregorio654f4a22012-02-09 14:15:44 -05001187
1188 for part in mime_response.get_payload():
1189 request_id = self._header_to_id(part['Content-ID'])
Joe Gregorioc752e332012-07-11 14:43:52 -04001190 response, content = self._deserialize_response(part.get_payload())
1191 self._responses[request_id] = (response, content)
Joe Gregorio654f4a22012-02-09 14:15:44 -05001192
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001193 @util.positional(1)
Joe Gregorio66f57522011-11-30 11:00:00 -05001194 def execute(self, http=None):
1195 """Execute all the requests as a single batched HTTP request.
1196
1197 Args:
1198 http: httplib2.Http, an http object to be used in place of the one the
Joe Gregorioe2233cd2013-01-24 15:46:23 -05001199 HttpRequest request object was constructed with. If one isn't supplied
Joe Gregorio66f57522011-11-30 11:00:00 -05001200 then use a http object from the requests in this batch.
1201
1202 Returns:
1203 None
1204
1205 Raises:
Joe Gregorio77af30a2012-08-01 14:54:40 -04001206 httplib2.HttpLib2Error if a transport error has occured.
Joe Gregorio5d1171b2012-01-05 10:48:24 -05001207 apiclient.errors.BatchError if the response is the wrong format.
Joe Gregorio66f57522011-11-30 11:00:00 -05001208 """
Joe Gregorio654f4a22012-02-09 14:15:44 -05001209
1210 # If http is not supplied use the first valid one given in the requests.
Joe Gregorio66f57522011-11-30 11:00:00 -05001211 if http is None:
1212 for request_id in self._order:
Joe Gregorio654f4a22012-02-09 14:15:44 -05001213 request = self._requests[request_id]
Joe Gregorio66f57522011-11-30 11:00:00 -05001214 if request is not None:
1215 http = request.http
1216 break
Joe Gregorio654f4a22012-02-09 14:15:44 -05001217
Joe Gregorio66f57522011-11-30 11:00:00 -05001218 if http is None:
1219 raise ValueError("Missing a valid http object.")
1220
Joe Gregorio654f4a22012-02-09 14:15:44 -05001221 self._execute(http, self._order, self._requests)
Joe Gregorio66f57522011-11-30 11:00:00 -05001222
Joe Gregorio654f4a22012-02-09 14:15:44 -05001223 # Loop over all the requests and check for 401s. For each 401 request the
1224 # credentials should be refreshed and then sent again in a separate batch.
1225 redo_requests = {}
1226 redo_order = []
Joe Gregorio66f57522011-11-30 11:00:00 -05001227
Joe Gregorio66f57522011-11-30 11:00:00 -05001228 for request_id in self._order:
Joe Gregorioc752e332012-07-11 14:43:52 -04001229 resp, content = self._responses[request_id]
1230 if resp['status'] == '401':
Joe Gregorio654f4a22012-02-09 14:15:44 -05001231 redo_order.append(request_id)
1232 request = self._requests[request_id]
1233 self._refresh_and_apply_credentials(request, http)
1234 redo_requests[request_id] = request
Joe Gregorio66f57522011-11-30 11:00:00 -05001235
Joe Gregorio654f4a22012-02-09 14:15:44 -05001236 if redo_requests:
1237 self._execute(http, redo_order, redo_requests)
Joe Gregorio66f57522011-11-30 11:00:00 -05001238
Joe Gregorio654f4a22012-02-09 14:15:44 -05001239 # Now process all callbacks that are erroring, and raise an exception for
1240 # ones that return a non-2xx response? Or add extra parameter to callback
1241 # that contains an HttpError?
Joe Gregorio66f57522011-11-30 11:00:00 -05001242
Joe Gregorio654f4a22012-02-09 14:15:44 -05001243 for request_id in self._order:
Joe Gregorioc752e332012-07-11 14:43:52 -04001244 resp, content = self._responses[request_id]
Joe Gregorio66f57522011-11-30 11:00:00 -05001245
Joe Gregorio654f4a22012-02-09 14:15:44 -05001246 request = self._requests[request_id]
1247 callback = self._callbacks[request_id]
Joe Gregorio66f57522011-11-30 11:00:00 -05001248
Joe Gregorio654f4a22012-02-09 14:15:44 -05001249 response = None
1250 exception = None
1251 try:
Joe Gregorio3fb93672012-07-25 11:31:11 -04001252 if resp.status >= 300:
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001253 raise HttpError(resp, content, uri=request.uri)
Joe Gregorioc752e332012-07-11 14:43:52 -04001254 response = request.postproc(resp, content)
Joe Gregorio654f4a22012-02-09 14:15:44 -05001255 except HttpError, e:
1256 exception = e
Joe Gregorio66f57522011-11-30 11:00:00 -05001257
Joe Gregorio654f4a22012-02-09 14:15:44 -05001258 if callback is not None:
1259 callback(request_id, response, exception)
Joe Gregorio66f57522011-11-30 11:00:00 -05001260 if self._callback is not None:
Joe Gregorio654f4a22012-02-09 14:15:44 -05001261 self._callback(request_id, response, exception)
Joe Gregorio66f57522011-11-30 11:00:00 -05001262
1263
Joe Gregorioaf276d22010-12-09 14:26:58 -05001264class HttpRequestMock(object):
1265 """Mock of HttpRequest.
1266
1267 Do not construct directly, instead use RequestMockBuilder.
1268 """
1269
1270 def __init__(self, resp, content, postproc):
1271 """Constructor for HttpRequestMock
1272
1273 Args:
1274 resp: httplib2.Response, the response to emulate coming from the request
1275 content: string, the response body
1276 postproc: callable, the post processing function usually supplied by
1277 the model class. See model.JsonModel.response() as an example.
1278 """
1279 self.resp = resp
1280 self.content = content
1281 self.postproc = postproc
1282 if resp is None:
Joe Gregorioc6722462010-12-20 14:29:28 -05001283 self.resp = httplib2.Response({'status': 200, 'reason': 'OK'})
Joe Gregorioaf276d22010-12-09 14:26:58 -05001284 if 'reason' in self.resp:
1285 self.resp.reason = self.resp['reason']
1286
1287 def execute(self, http=None):
1288 """Execute the request.
1289
1290 Same behavior as HttpRequest.execute(), but the response is
1291 mocked and not really from an HTTP request/response.
1292 """
1293 return self.postproc(self.resp, self.content)
1294
1295
1296class RequestMockBuilder(object):
1297 """A simple mock of HttpRequest
1298
1299 Pass in a dictionary to the constructor that maps request methodIds to
Joe Gregorioa388ce32011-09-09 17:19:13 -04001300 tuples of (httplib2.Response, content, opt_expected_body) that should be
1301 returned when that method is called. None may also be passed in for the
1302 httplib2.Response, in which case a 200 OK response will be generated.
1303 If an opt_expected_body (str or dict) is provided, it will be compared to
1304 the body and UnexpectedBodyError will be raised on inequality.
Joe Gregorioaf276d22010-12-09 14:26:58 -05001305
1306 Example:
1307 response = '{"data": {"id": "tag:google.c...'
1308 requestBuilder = RequestMockBuilder(
1309 {
Joe Gregorioc4fc0952011-11-09 12:21:11 -05001310 'plus.activities.get': (None, response),
Joe Gregorioaf276d22010-12-09 14:26:58 -05001311 }
1312 )
Joe Gregorioc4fc0952011-11-09 12:21:11 -05001313 apiclient.discovery.build("plus", "v1", requestBuilder=requestBuilder)
Joe Gregorioaf276d22010-12-09 14:26:58 -05001314
1315 Methods that you do not supply a response for will return a
Joe Gregorio66f57522011-11-30 11:00:00 -05001316 200 OK with an empty string as the response content or raise an excpetion
1317 if check_unexpected is set to True. The methodId is taken from the rpcName
Joe Gregorioa388ce32011-09-09 17:19:13 -04001318 in the discovery document.
Joe Gregorioaf276d22010-12-09 14:26:58 -05001319
1320 For more details see the project wiki.
1321 """
1322
Joe Gregorioa388ce32011-09-09 17:19:13 -04001323 def __init__(self, responses, check_unexpected=False):
Joe Gregorioaf276d22010-12-09 14:26:58 -05001324 """Constructor for RequestMockBuilder
1325
1326 The constructed object should be a callable object
1327 that can replace the class HttpResponse.
1328
1329 responses - A dictionary that maps methodIds into tuples
1330 of (httplib2.Response, content). The methodId
1331 comes from the 'rpcName' field in the discovery
1332 document.
Joe Gregorioa388ce32011-09-09 17:19:13 -04001333 check_unexpected - A boolean setting whether or not UnexpectedMethodError
1334 should be raised on unsupplied method.
Joe Gregorioaf276d22010-12-09 14:26:58 -05001335 """
1336 self.responses = responses
Joe Gregorioa388ce32011-09-09 17:19:13 -04001337 self.check_unexpected = check_unexpected
Joe Gregorioaf276d22010-12-09 14:26:58 -05001338
Joe Gregorio7c22ab22011-02-16 15:32:39 -05001339 def __call__(self, http, postproc, uri, method='GET', body=None,
Joe Gregoriod0bd3882011-11-22 09:49:47 -05001340 headers=None, methodId=None, resumable=None):
Joe Gregorioaf276d22010-12-09 14:26:58 -05001341 """Implements the callable interface that discovery.build() expects
1342 of requestBuilder, which is to build an object compatible with
1343 HttpRequest.execute(). See that method for the description of the
1344 parameters and the expected response.
1345 """
1346 if methodId in self.responses:
Joe Gregorioa388ce32011-09-09 17:19:13 -04001347 response = self.responses[methodId]
1348 resp, content = response[:2]
1349 if len(response) > 2:
1350 # Test the body against the supplied expected_body.
1351 expected_body = response[2]
1352 if bool(expected_body) != bool(body):
1353 # Not expecting a body and provided one
1354 # or expecting a body and not provided one.
1355 raise UnexpectedBodyError(expected_body, body)
1356 if isinstance(expected_body, str):
1357 expected_body = simplejson.loads(expected_body)
1358 body = simplejson.loads(body)
1359 if body != expected_body:
1360 raise UnexpectedBodyError(expected_body, body)
Joe Gregorioaf276d22010-12-09 14:26:58 -05001361 return HttpRequestMock(resp, content, postproc)
Joe Gregorioa388ce32011-09-09 17:19:13 -04001362 elif self.check_unexpected:
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001363 raise UnexpectedMethodError(methodId=methodId)
Joe Gregorioaf276d22010-12-09 14:26:58 -05001364 else:
Joe Gregoriod433b2a2011-02-22 10:51:51 -05001365 model = JsonModel(False)
Joe Gregorioaf276d22010-12-09 14:26:58 -05001366 return HttpRequestMock(None, '{}', model.response)
Joe Gregoriocb8103d2011-02-11 23:20:52 -05001367
Joe Gregoriodeeb0202011-02-15 14:49:57 -05001368
Joe Gregoriocb8103d2011-02-11 23:20:52 -05001369class HttpMock(object):
1370 """Mock of httplib2.Http"""
1371
Joe Gregorio83f2ee62012-12-06 15:25:54 -05001372 def __init__(self, filename=None, headers=None):
Joe Gregoriocb8103d2011-02-11 23:20:52 -05001373 """
1374 Args:
1375 filename: string, absolute filename to read response from
1376 headers: dict, header to return with response
1377 """
Joe Gregorioec343652011-02-16 16:52:51 -05001378 if headers is None:
1379 headers = {'status': '200 OK'}
Joe Gregorio83f2ee62012-12-06 15:25:54 -05001380 if filename:
1381 f = file(filename, 'r')
1382 self.data = f.read()
1383 f.close()
1384 else:
1385 self.data = None
1386 self.response_headers = headers
1387 self.headers = None
1388 self.uri = None
1389 self.method = None
1390 self.body = None
1391 self.headers = None
1392
Joe Gregoriocb8103d2011-02-11 23:20:52 -05001393
Joe Gregoriodeeb0202011-02-15 14:49:57 -05001394 def request(self, uri,
Joe Gregorio7c22ab22011-02-16 15:32:39 -05001395 method='GET',
Joe Gregoriodeeb0202011-02-15 14:49:57 -05001396 body=None,
1397 headers=None,
1398 redirections=1,
1399 connection_type=None):
Joe Gregorio83f2ee62012-12-06 15:25:54 -05001400 self.uri = uri
1401 self.method = method
1402 self.body = body
1403 self.headers = headers
1404 return httplib2.Response(self.response_headers), self.data
Joe Gregorioccc79542011-02-19 00:05:26 -05001405
1406
1407class HttpMockSequence(object):
1408 """Mock of httplib2.Http
1409
1410 Mocks a sequence of calls to request returning different responses for each
1411 call. Create an instance initialized with the desired response headers
1412 and content and then use as if an httplib2.Http instance.
1413
1414 http = HttpMockSequence([
1415 ({'status': '401'}, ''),
1416 ({'status': '200'}, '{"access_token":"1/3w","expires_in":3600}'),
1417 ({'status': '200'}, 'echo_request_headers'),
1418 ])
1419 resp, content = http.request("http://examples.com")
1420
1421 There are special values you can pass in for content to trigger
1422 behavours that are helpful in testing.
1423
1424 'echo_request_headers' means return the request headers in the response body
Joe Gregorioe9e236f2011-03-21 22:23:14 -04001425 'echo_request_headers_as_json' means return the request headers in
1426 the response body
Joe Gregorioccc79542011-02-19 00:05:26 -05001427 'echo_request_body' means return the request body in the response body
Joe Gregorio0bc70912011-05-24 15:30:49 -04001428 'echo_request_uri' means return the request uri in the response body
Joe Gregorioccc79542011-02-19 00:05:26 -05001429 """
1430
1431 def __init__(self, iterable):
1432 """
1433 Args:
1434 iterable: iterable, a sequence of pairs of (headers, body)
1435 """
1436 self._iterable = iterable
Joe Gregorio708388c2012-06-15 13:43:04 -04001437 self.follow_redirects = True
Joe Gregorioccc79542011-02-19 00:05:26 -05001438
1439 def request(self, uri,
1440 method='GET',
1441 body=None,
1442 headers=None,
1443 redirections=1,
1444 connection_type=None):
1445 resp, content = self._iterable.pop(0)
1446 if content == 'echo_request_headers':
1447 content = headers
Joe Gregoriof4153422011-03-18 22:45:18 -04001448 elif content == 'echo_request_headers_as_json':
1449 content = simplejson.dumps(headers)
Joe Gregorioccc79542011-02-19 00:05:26 -05001450 elif content == 'echo_request_body':
Joe Gregorioc80ac9d2012-08-21 14:09:09 -04001451 if hasattr(body, 'read'):
1452 content = body.read()
1453 else:
1454 content = body
Joe Gregorio0bc70912011-05-24 15:30:49 -04001455 elif content == 'echo_request_uri':
1456 content = uri
Joe Gregorioccc79542011-02-19 00:05:26 -05001457 return httplib2.Response(resp), content
Joe Gregorio6bcbcea2011-03-10 15:26:05 -05001458
1459
1460def set_user_agent(http, user_agent):
Joe Gregoriof4153422011-03-18 22:45:18 -04001461 """Set the user-agent on every request.
1462
Joe Gregorio6bcbcea2011-03-10 15:26:05 -05001463 Args:
1464 http - An instance of httplib2.Http
1465 or something that acts like it.
1466 user_agent: string, the value for the user-agent header.
1467
1468 Returns:
1469 A modified instance of http that was passed in.
1470
1471 Example:
1472
1473 h = httplib2.Http()
1474 h = set_user_agent(h, "my-app-name/6.0")
1475
1476 Most of the time the user-agent will be set doing auth, this is for the rare
1477 cases where you are accessing an unauthenticated endpoint.
1478 """
1479 request_orig = http.request
1480
1481 # The closure that will replace 'httplib2.Http.request'.
1482 def new_request(uri, method='GET', body=None, headers=None,
1483 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
1484 connection_type=None):
1485 """Modify the request headers to add the user-agent."""
1486 if headers is None:
1487 headers = {}
1488 if 'user-agent' in headers:
1489 headers['user-agent'] = user_agent + ' ' + headers['user-agent']
1490 else:
1491 headers['user-agent'] = user_agent
1492 resp, content = request_orig(uri, method, body, headers,
1493 redirections, connection_type)
1494 return resp, content
1495
1496 http.request = new_request
1497 return http
Joe Gregoriof4153422011-03-18 22:45:18 -04001498
1499
1500def tunnel_patch(http):
1501 """Tunnel PATCH requests over POST.
1502 Args:
1503 http - An instance of httplib2.Http
1504 or something that acts like it.
1505
1506 Returns:
1507 A modified instance of http that was passed in.
1508
1509 Example:
1510
1511 h = httplib2.Http()
1512 h = tunnel_patch(h, "my-app-name/6.0")
1513
1514 Useful if you are running on a platform that doesn't support PATCH.
1515 Apply this last if you are using OAuth 1.0, as changing the method
1516 will result in a different signature.
1517 """
1518 request_orig = http.request
1519
1520 # The closure that will replace 'httplib2.Http.request'.
1521 def new_request(uri, method='GET', body=None, headers=None,
1522 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
1523 connection_type=None):
1524 """Modify the request headers to add the user-agent."""
1525 if headers is None:
1526 headers = {}
1527 if method == 'PATCH':
Joe Gregorio06d852b2011-03-25 15:03:10 -04001528 if 'oauth_token' in headers.get('authorization', ''):
Joe Gregorioe9e236f2011-03-21 22:23:14 -04001529 logging.warning(
Joe Gregorio06d852b2011-03-25 15:03:10 -04001530 'OAuth 1.0 request made with Credentials after tunnel_patch.')
Joe Gregoriof4153422011-03-18 22:45:18 -04001531 headers['x-http-method-override'] = "PATCH"
1532 method = 'POST'
1533 resp, content = request_orig(uri, method, body, headers,
1534 redirections, connection_type)
1535 return resp, content
1536
1537 http.request = new_request
1538 return http